Compare commits

...

5 Commits

Author SHA1 Message Date
4e11b218be added information being pulled from anilist 2024-07-24 09:18:45 -04:00
b1880690dc added go keyring to handle storing jwt 2024-07-24 09:18:09 -04:00
59fe3d32ff added tailwind and cleaned up page
Cleaned up page to allow Tailwind to start handling the look and feel of
everything
2024-07-24 09:17:39 -04:00
64def1a763 separated our current watchlist types 2024-07-24 09:16:13 -04:00
dcf7322b0c updated bruno files 2024-07-24 09:15:52 -04:00
18 changed files with 987 additions and 225 deletions

View File

@ -2,38 +2,22 @@ package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"github.com/wailsapp/wails/v2/pkg/runtime"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
"sync"
)
type JWT struct {
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
var jwt JWT
func AniListQuery(body interface{}, login bool) interface{} {
func AniListQuery(body interface{}, login bool) (json.RawMessage, string) {
reader, _ := json.Marshal(body)
response, err := http.NewRequest("POST", "https://graphql.anilist.co", bytes.NewBuffer(reader))
if err != nil {
log.Printf("Failed at response, %s\n", err)
}
if login && (JWT{}) != jwt {
if login && (AniListJWT{}) != jwt {
response.Header.Add("Authorization", "Bearer "+jwt.AccessToken)
} else if login {
return "Please login to AniList to make this request"
return nil, "Please login to anilist to make this request"
}
response.Header.Add("Content-Type", "application/json")
response.Header.Add("Accept", "application/json")
@ -48,13 +32,7 @@ func AniListQuery(body interface{}, login bool) interface{} {
returnedBody, err := io.ReadAll(res.Body)
var post interface{}
err = json.Unmarshal(returnedBody, &post)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
return post
return returnedBody, ""
}
func (a *App) GetAniListItem(aniId int) any {
@ -101,7 +79,15 @@ func (a *App) GetAniListItem(aniId int) any {
},
}
return AniListQuery(body, false)
returnedBody, _ := AniListQuery(body, false)
var post interface{}
err := json.Unmarshal(returnedBody, &post)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
return post
}
func (a *App) AniListSearch(query string) any {
@ -143,95 +129,131 @@ func (a *App) AniListSearch(query string) any {
ListType: "ANIME",
},
}
return AniListQuery(body, false)
}
returnedBody, _ := AniListQuery(body, false)
var ctxShutdown, cancel = context.WithCancel(context.Background())
func (a *App) AniListLogin() {
getAniListCodeUrl := "https://anilist.co/api/v2/oauth/authorize?client_id=" + os.Getenv("ANILIST_APP_ID") + "&redirect_uri=" + os.Getenv("ANILIST_CALLBACK_URI") + "&response_type=code"
runtime.BrowserOpenURL(a.ctx, getAniListCodeUrl)
serverDone := &sync.WaitGroup{}
serverDone.Add(1)
handleAniListCallback(serverDone)
serverDone.Wait()
}
func handleAniListCallback(wg *sync.WaitGroup) {
srv := &http.Server{Addr: ":6734"}
http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
select {
case <-ctxShutdown.Done():
fmt.Println("Shutting down...")
return
default:
}
content := r.FormValue("code")
if content != "" {
jwt = getAniListAuthorizationToken(content)
fmt.Println("Shutting down...")
cancel()
err := srv.Shutdown(context.Background())
if err != nil {
log.Println("server.Shutdown:", err)
}
} else {
_, err := fmt.Fprintf(w, "Getting code failed.")
if err != nil {
return
}
}
})
go func() {
defer wg.Done()
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
fmt.Println("Shutting down...")
}()
}
func getAniListAuthorizationToken(content string) JWT {
apiUrl := "https://anilist.co/api/v2/oauth/token"
resource := "/api/v2/oauth/token"
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("client_id", os.Getenv("ANILIST_APP_ID"))
data.Set("client_secret", os.Getenv("ANILIST_SECRET_TOKEN"))
data.Set("redirect_uri", os.Getenv("ANILIST_CALLBACK_URI"))
data.Set("code", content)
u, _ := url.ParseRequestURI(apiUrl)
u.Path = resource
urlStr := u.String()
response, err := http.NewRequest("POST", urlStr, strings.NewReader(data.Encode()))
if err != nil {
log.Printf("Failed at response, %s\n", err)
}
response.Header.Add("content-type", "application/x-www-form-urlencoded")
response.Header.Add("Content-Type", "application/json")
response.Header.Add("Accept", "application/json")
client := &http.Client{}
res, reserr := client.Do(response)
if reserr != nil {
log.Printf("Failed at res, %s\n", err)
}
defer res.Body.Close()
returnedBody, err := io.ReadAll(res.Body)
var post JWT
err = json.Unmarshal(returnedBody, &post)
var post interface{}
err := json.Unmarshal(returnedBody, &post)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
return post
}
func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) AniListCurrentUserWatchList {
var user = a.GetAniListLoggedInUserId()
type Variables struct {
Page int `json:"page"`
PerPage int `json:"perPage"`
UserId int `json:"userId"`
ListType string `json:"listType"`
Status string `json:"status"`
Sort string `json:"sort"`
}
body := struct {
Query string `json:"query"`
Variables Variables `json:"variables"`
}{
Query: `
query(
$page: Int
$perPage: Int
$userId: Int
$listType: MediaType
$status: MediaListStatus
$sort:[MediaListSort]
) {
Page(page: $page, perPage: $perPage) {
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
mediaList(userId: $userId, type: $listType, status: $status, sort: $sort) {
id
mediaId
userId
media {
id
idMal
title {
romaji
english
native
}
description
coverImage {
large
}
season
seasonYear
status
episodes
nextAiringEpisode{
airingAt
timeUntilAiring
episode
}
}
status
notes
progress
score
repeat
user {
id
name
avatar{
large
medium
}
statistics {
anime {
count
statuses {
status
count
}
}
}
}
}
}
}
`,
Variables: Variables{
Page: page,
PerPage: perPage,
UserId: user.Data.Viewer.ID,
ListType: "ANIME",
Status: "CURRENT",
Sort: sort,
},
}
returnedBody, _ := AniListQuery(body, true)
var post AniListCurrentUserWatchList
err := json.Unmarshal(returnedBody, &post)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
// Getting the real total, finding the real last page and storing that in the Page info
statuses := post.Data.Page.MediaList[0].User.Statistics.Anime.Statuses
var total int
for _, status := range statuses {
if status.Status == "CURRENT" {
total = status.Count
}
}
lastPage := total / perPage
post.Data.Page.PageInfo.Total = total
post.Data.Page.PageInfo.LastPage = lastPage
return post
}

144
AniListTypes.go Normal file
View File

@ -0,0 +1,144 @@
package main
type AniListJWT struct {
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
}
type AniListUser struct {
Data struct {
Viewer struct {
ID int `json:"id"`
Name string `json:"name"`
} `json:"Viewer"`
} `json:"data"`
}
type AniListCurrentUserWatchList struct {
Data struct {
Page struct {
PageInfo struct {
Total int `json:"total"`
PerPage int `json:"perPage"`
CurrentPage int `json:"currentPage"`
LastPage int `json:"lastPage"`
HasNextPage bool `json:"hasNextPage"`
} `json:"pageInfo"`
MediaList []struct {
ID int `json:"id"`
MediaID int `json:"mediaId"`
UserID int `json:"userId"`
Media struct {
ID int `json:"id"`
IDMal int `json:"idMal"`
Title struct {
Romaji string `json:"romaji"`
English string `json:"english"`
Native string `json:"native"`
} `json:"title"`
Description string `json:"description"`
CoverImage struct {
Large string `json:"large"`
} `json:"coverImage"`
Season string `json:"season"`
SeasonYear int `json:"seasonYear"`
Status string `json:"status"`
Episodes int `json:"episodes"`
NextAiringEpisode struct {
AiringAt int `json:"airingAt"`
TimeUntilAiring int `json:"timeUntilAiring"`
Episode int `json:"episode"`
} `json:"nextAiringEpisode"`
} `json:"media"`
Status string `json:"status"`
Notes string `json:"notes"`
Progress int `json:"progress"`
Score int `json:"score"`
Repeat int `json:"repeat"`
User struct {
ID int `json:"id"`
Name string `json:"name"`
Avatar struct {
Large string `json:"large"`
Medium string `json:"medium"`
} `json:"avatar"`
Statistics struct {
Anime struct {
Count int `json:"count"`
Statuses []struct {
Status string `json:"status"`
Count int `json:"count"`
} `json:"statuses"`
} `json:"anime"`
} `json:"statistics"`
} `json:"user"`
} `json:"mediaList"`
} `json:"Page"`
} `json:"data"`
}
var MediaListSort = struct {
MediaId string
MediaIdDesc string
Score string
ScoreDesc string
Status string
StatusDesc string
Progress string
ProgressDesc string
ProgressVolumes string
ProgressVolumesDesc string
Repeat string
RepeatDesc string
Priority string
PriorityDesc string
StartedOn string
StartedOnDesc string
FinishedOn string
FinishedOnDesc string
AddedTime string
AddedTimeDesc string
UpdatedTime string
UpdatedTimeDesc string
MediaTitleRomaji string
MediaTitleRomajiDesc string
MediaTitleEnglish string
MediaTitleEnglishDesc string
MediaTitleNative string
MediaTitleNativeDesc string
MediaPopularity string
MediaPopularityDesc string
}{
MediaId: "MEDIA_ID",
MediaIdDesc: "MEDIA_ID_DESC",
Score: "SCORE",
ScoreDesc: "SCORE_DESC",
Status: "STATUS",
StatusDesc: "STATUS_DESC",
Progress: "PROGRESS",
ProgressDesc: "PROGRESS_DESC",
ProgressVolumes: "PROGRESS_VOLUMES",
ProgressVolumesDesc: "PROGRESS_VOLUMES_DESC",
Repeat: "REPEAT",
RepeatDesc: "REPEAT_DESC",
Priority: "PRIORITY",
PriorityDesc: "PRIORITY_DESC",
StartedOn: "STARTED_ON",
StartedOnDesc: "STARTED_ON_DESC",
FinishedOn: "FINISHED_ON",
FinishedOnDesc: "FINISHED_ON_DESC",
AddedTime: "ADDED_TIME",
AddedTimeDesc: "ADDED_TIME_DESC",
UpdatedTime: "UPDATED_TIME",
UpdatedTimeDesc: "UPDATED_TIME_DESC",
MediaTitleRomaji: "MEDIA_TITLE_ROMAJI",
MediaTitleRomajiDesc: "MEDIA_TITLE_ROMAJI_DESC",
MediaTitleEnglish: "MEDIA_TITLE_ENGLISH",
MediaTitleEnglishDesc: "MEDIA_TITLE_ENGLISH_DESC",
MediaTitleNative: "MEDIA_TITLE_NATIVE",
MediaTitleNativeDesc: "MEDIA_TITLE_NATIVE_DESC",
MediaPopularity: "MEDIA_POPULARITY",
MediaPopularityDesc: "MEDIA_POPULARITY_DESC",
}

170
AniListUserFunctions.go Normal file
View File

@ -0,0 +1,170 @@
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/99designs/keyring"
"github.com/wailsapp/wails/v2/pkg/runtime"
"io"
"log"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
)
var jwt AniListJWT
var ring, _ = keyring.Open(keyring.Config{
ServiceName: "AniTrack",
})
var ctxShutdown, cancel = context.WithCancel(context.Background())
func (a *App) AniListLogin() {
if (AniListJWT{}) == jwt {
tokenType, err := ring.Get("anilistTokenType")
expiresIn, err := ring.Get("anilistTokenExpiresIn")
accessToken, err := ring.Get("anilistAccessToken")
refreshToken, err := ring.Get("anilistRefreshToken")
if err != nil {
getAniListCodeUrl := "https://anilist.co/api/v2/oauth/authorize?client_id=" + os.Getenv("ANILIST_APP_ID") + "&redirect_uri=" + os.Getenv("ANILIST_CALLBACK_URI") + "&response_type=code"
runtime.BrowserOpenURL(a.ctx, getAniListCodeUrl)
serverDone := &sync.WaitGroup{}
serverDone.Add(1)
handleAniListCallback(serverDone)
serverDone.Wait()
} else {
jwt.TokenType = string(tokenType.Data)
jwt.AccessToken = string(accessToken.Data)
jwt.RefreshToken = string(refreshToken.Data)
expiresInString := string(expiresIn.Data)
jwt.ExpiresIn, _ = strconv.Atoi(expiresInString)
}
}
}
func handleAniListCallback(wg *sync.WaitGroup) {
srv := &http.Server{Addr: ":6734"}
http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
select {
case <-ctxShutdown.Done():
fmt.Println("Shutting down...")
return
default:
}
content := r.FormValue("code")
if content != "" {
jwt = getAniListAuthorizationToken(content)
_ = ring.Set(keyring.Item{
Key: "anilistTokenType",
Data: []byte(jwt.TokenType),
})
_ = ring.Set(keyring.Item{
Key: "anilistTokenExpiresIn",
Data: []byte(string(jwt.ExpiresIn)),
})
_ = ring.Set(keyring.Item{
Key: "anilistAccessToken",
Data: []byte(jwt.AccessToken),
})
_ = ring.Set(keyring.Item{
Key: "anilistRefreshToken",
Data: []byte(jwt.RefreshToken),
})
fmt.Println("Shutting down...")
cancel()
err := srv.Shutdown(context.Background())
if err != nil {
log.Println("server.Shutdown:", err)
}
} else {
_, err := fmt.Fprintf(w, "Getting code failed.")
if err != nil {
return
}
}
})
go func() {
defer wg.Done()
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %s\n", err)
}
fmt.Println("Shutting down...")
}()
}
func getAniListAuthorizationToken(content string) AniListJWT {
apiUrl := "https://anilist.co/api/v2/oauth/token"
resource := "/api/v2/oauth/token"
data := url.Values{}
data.Set("grant_type", "authorization_code")
data.Set("client_id", os.Getenv("ANILIST_APP_ID"))
data.Set("client_secret", os.Getenv("ANILIST_SECRET_TOKEN"))
data.Set("redirect_uri", os.Getenv("ANILIST_CALLBACK_URI"))
data.Set("code", content)
u, _ := url.ParseRequestURI(apiUrl)
u.Path = resource
urlStr := u.String()
response, err := http.NewRequest("POST", urlStr, strings.NewReader(data.Encode()))
if err != nil {
log.Printf("Failed at response, %s\n", err)
}
response.Header.Add("content-type", "application/x-www-form-urlencoded")
response.Header.Add("Content-Type", "application/json")
response.Header.Add("Accept", "application/json")
client := &http.Client{}
res, reserr := client.Do(response)
if reserr != nil {
log.Printf("Failed at res, %s\n", err)
}
defer res.Body.Close()
returnedBody, err := io.ReadAll(res.Body)
var post AniListJWT
err = json.Unmarshal(returnedBody, &post)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
return post
}
func (a *App) GetAniListLoggedInUserId() AniListUser {
a.AniListLogin()
body := struct {
Query string `json:"query"`
}{
Query: `
query {
Viewer {
id
name
}
}
`,
}
user, _ := AniListQuery(body, true)
var post AniListUser
err := json.Unmarshal(user, &post)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
return post
}

View File

@ -29,16 +29,6 @@ body:graphql {
coverImage {
large
}
tags {
id
name
description
category
rank
isGeneralSpoiler
isMediaSpoiler
isAdult
}
}
}
@ -50,3 +40,19 @@ body:graphql:vars {
"listType": "ANIME"
}
}
docs {
Title
Image
Description
Episodes
Status
Season
External & Streaming Links
}

View File

@ -0,0 +1,103 @@
meta {
name: AniList MediaList User Query
type: graphql
seq: 6
}
post {
url: https://graphql.anilist.co
body: graphql
auth: none
}
headers {
Content-Type: "application/json"
Accept: "application/json"
}
body:graphql {
# Write your query or mutation here
query(
$page: Int
$perPage: Int
$userId: Int
$listType: MediaType
$status: MediaListStatus
) {
Page(page: $page, perPage: $perPage) {
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
mediaList(userId: $userId, type: $listType, status: $status) {
id
mediaId
userId
media {
id
idMal
title {
romaji
english
native
}
description
coverImage {
large
}
season
seasonYear
episodes
}
status
notes
progress
score
repeat
user {
id
statistics {
anime {
count
statuses {
status
count
}
}
}
}
}
}
}
}
body:graphql:vars {
{
"page": 1,
"perPage": 20,
"userId": 413504,
"listType": "ANIME",
"status": "CURRENT"
}
}
docs {
Title
Image
Description
Episodes
Status
Season
External & Streaming Links
User Stuff Per Item
Status
Score
Episode Progress
Total Rewatches
Notes
}

View File

@ -0,0 +1,28 @@
meta {
name: AniList MediaList User
type: http
seq: 5
}
post {
url: https://graphql.anilist.co
body: none
auth: none
}
docs {
Title
Image
Description
Episodes
Status
Season
External & Streaming Links
User Stuff Per Item
Status
Score
Episode Progress
Total Rewatches
Notes
}

View File

@ -12,11 +12,17 @@
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^1.0.1",
"@tsconfig/svelte": "^3.0.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.39",
"svelte": "^3.49.0",
"svelte-check": "^2.8.0",
"svelte-preprocess": "^4.10.7",
"tailwindcss": "^3.4.6",
"tslib": "^2.4.0",
"typescript": "^4.6.4",
"vite": "^3.0.7"
},
"dependencies": {
"@tailwindcss/aspect-ratio": "^0.4.2"
}
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -1,125 +1,134 @@
<script lang="ts">
import logo from './assets/images/logo-universal.png'
import {Greet} from '../wailsjs/go/main/App.js'
import {AniListLogin, AniListSearch, GetAniListItem} from "../wailsjs/go/main/App";
import type {AniListItem, AniSearchList} from "./AniListTypes";
import {
AniListSearch,
GetAniListItem,
GetAniListLoggedInUserId,
GetAniListUserWatchingList
} from "../wailsjs/go/main/App";
import {type AniListItem, type AniSearchList, MediaListSort} from "./anilist/types/AniListTypes";
import type {AniListCurrentUserWatchList} from "./anilist/types/AniListCurrentUserWatchListType"
let resultText: string = "Please enter your name below 👇"
let name: string
let aniId = "157371"
let aniSearch = ""
let aniListItem: AniListItem
let aniListSearch: AniSearchList
let aniListLoggedIn = false
let aniId = "157371"
let aniSearch = ""
let aniListItem: AniListItem
let aniListSearch: AniSearchList
let aniListLoggedIn = false
let aniListWatchlist: AniListCurrentUserWatchList
let page = 1
let perPage = 20
function greet(): void {
Greet(name).then(result => resultText = result)
}
function getAniListitem(): void {
GetAniListItem(Number(aniId)).then(result => aniListItem = result)
}
function getAniListitem(): void {
GetAniListItem(Number(aniId)).then(result => aniListItem = result)
}
function runAniListSearch(): void {
AniListSearch(aniSearch).then(result => aniListSearch = result)
}
function runAniListSearch(): void {
AniListSearch(aniSearch).then(result => aniListSearch = result)
}
function loginToAniList(): void {
AniListLogin().then(() => aniListLoggedIn = true)
}
function anilistGetUserWatchlist(): void {
GetAniListUserWatchingList(page, perPage, MediaListSort.UpdatedTimeDesc).then((result) => {
aniListWatchlist = result
aniListLoggedIn = true
})
}
</script>
<main>
<img alt="Wails logo" id="logo" src="{aniListItem === undefined ? logo : aniListItem.data.Media.coverImage.extraLarge}">
<!-- <div class="result" id="result">{resultText}</div>-->
<!-- <div class="input-box" id="input">-->
<!-- <input autocomplete="off" bind:value={name} class="input" id="name" type="text"/>-->
<!-- <button class="btn" on:click={greet}>Greet</button>-->
<!-- </div>-->
<div class="input-box" id="aniSearch">
<input autocomplete="off" bind:value={aniSearch} class="input" id="aniSearchInput" type="text"/>
<button class="btn" on:click={runAniListSearch}>Search AniList</button>
</div>
<div class="result" id="AniTest">{aniListItem !== undefined ? aniListItem.data.Media.title.english : ""}</div>
<div class="input-box" id="aniButton">
<input autocomplete="off" bind:value={aniId} class="input" id="aniId" type="text"/>
<button class="btn" on:click={getAniListitem}>Get AniList Item</button>
</div>
<button class="btn" on:click={anilistGetUserWatchlist}>Login to AniList</button>
{#if aniListLoggedIn}
<div>You are logged in {aniListWatchlist.data.Page.mediaList[0].user.name}!</div>
{/if}
<button class="btn" on:click={loginToAniList}>Login to AniList</button>
{#if aniListLoggedIn}
<div>You are logged in!</div>
{/if}
{#if aniListLoggedIn}
<div class="mx-auto max-w-2xl px-4 py-16 sm:px-6 sm:py-24 lg:max-w-7xl lg:px-8">
<h1>Your Watching List</h1>
<div class="input-box" id="aniSearch">
<input autocomplete="off" bind:value={aniSearch} class="input" id="aniSearchInput" type="text"/>
<button class="btn" on:click={runAniListSearch}>Search AniList</button>
</div>
{#if aniListSearch !== undefined}
<ul>
{#each aniListSearch.data.Page.media as media, index (media.id)}
<li>
<div>{media.title.english !== null ? media.title.english : media.title.romaji}</div>
</li>
{:else}
<div>No Results Yet...</div>
{/each}
</ul>
{/if}
<div class="grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
{#each aniListWatchlist.data.Page.mediaList as media}
<a href="#" class="group">
<!-- <div class="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-lg bg-gray-200 xl:aspect-h-8 xl:aspect-w-7">-->
<div class="justify-center rounded-lg bg-gray-200">
<img src={media.media.coverImage.large} alt="anime cover"/>
</div>
<h3 class="mt-4 text-sm text-white-700">{
media.media.title.english === "" ?
media.media.title.romaji :
media.media.title.english
}</h3>
<p class="mt-1 text-lg font-medium text-white-900">{media.progress}
/ {media.media.nextAiringEpisode.episode !== 0 ?
media.media.nextAiringEpisode.episode - 1 : media.media.episodes}</p>
{#if media.media.episodes > 0}
<p class="mt-1 text-lg font-medium text-white-900">Total
Episodes: {media.media.episodes}</p>
{/if}
</a>
{/each}
</div>
</div>
{/if}
</main>
<style>
#logo {
display: block;
width: 50%;
height: 50%;
margin: auto;
padding: 10% 0 0;
background-position: center;
background-repeat: no-repeat;
background-size: 100% 100%;
background-origin: content-box;
}
#logo {
display: block;
width: 50%;
height: 50%;
margin: auto;
padding: 10% 0 0;
background-position: center;
background-repeat: no-repeat;
background-size: 100% 100%;
background-origin: content-box;
}
.result {
height: 20px;
line-height: 20px;
margin: 1.5rem auto;
}
.result {
height: 20px;
line-height: 20px;
margin: 1.5rem auto;
}
.input-box .btn {
/*width: 60px;*/
height: 30px;
line-height: 30px;
border-radius: 3px;
border: none;
margin: 0 0 0 20px;
padding: 0 8px;
cursor: pointer;
}
.input-box .btn {
/*width: 60px;*/
height: 30px;
line-height: 30px;
border-radius: 3px;
border: none;
margin: 0 0 0 20px;
padding: 0 8px;
cursor: pointer;
}
.input-box .btn:hover {
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
color: #333333;
}
.input-box .btn:hover {
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
color: #333333;
}
.input-box .input {
border: none;
border-radius: 3px;
outline: none;
height: 30px;
line-height: 30px;
padding: 0 10px;
background-color: rgba(240, 240, 240, 1);
-webkit-font-smoothing: antialiased;
}
.input-box .input {
border: none;
border-radius: 3px;
outline: none;
height: 30px;
line-height: 30px;
padding: 0 10px;
background-color: rgba(240, 240, 240, 1);
-webkit-font-smoothing: antialiased;
}
.input-box .input:hover {
border: none;
background-color: rgba(255, 255, 255, 1);
}
.input-box .input:hover {
border: none;
background-color: rgba(255, 255, 255, 1);
}
.input-box .input:focus {
border: none;
background-color: rgba(255, 255, 255, 1);
}
.input-box .input:focus {
border: none;
background-color: rgba(255, 255, 255, 1);
}
</style>

View File

@ -0,0 +1,88 @@
export interface AniListCurrentUserWatchList {
data: Data
}
export interface Data {
Page: Page
}
export interface Page {
pageInfo: PageInfo
mediaList: MediaList[]
}
export interface PageInfo {
total: number
perPage: number
currentPage: number
lastPage: number
hasNextPage: boolean
}
export interface MediaList {
id: number
mediaId: number
userId: number
media: Media
status: string
notes?: string
progress: number
score: number
repeat: number
user: User
}
export interface Media {
id: number
idMal: number
title: Title
description: string
coverImage: CoverImage
season: string
seasonYear: number
status: string
episodes?: number
nextAiringEpisode?: NextAiringEpisode
}
export interface Title {
romaji: string
english?: string
native: string
}
export interface CoverImage {
large: string
}
export interface NextAiringEpisode {
airingAt: number
timeUntilAiring: number
episode: number
}
export interface User {
id: number
name: string
avatar: Avatar
statistics: Statistics
}
export interface Avatar {
large: string
medium: string
}
export interface Statistics {
anime: Anime
}
export interface Anime {
count: number
statuses: Status[]
}
export interface Status {
status: string
count: number
}

View File

@ -44,6 +44,7 @@ export interface AniSearchList {
title: {
romaji: string,
english: string,
native: string,
},
coverImage: {
extraLarge: string,
@ -52,4 +53,46 @@ export interface AniSearchList {
}],
},
}
}
export interface AniListUser {
"data": {
"Viewer": {
id: number,
name: string,
}
}
}
export enum MediaListSort {
MediaId = "MEDIA_ID",
MediaIdDesc = "MEDIA_ID_DESC",
Score = "SCORE",
ScoreDesc = "SCORE_DESC",
Status = "STATUS",
StatusDesc = "STATUS_DESC",
Progress = "PROGRESS",
ProgressDesc = "PROGRESS_DESC",
ProgressVolumes = "PROGRESS_VOLUMES",
ProgressVolumesDesc = "PROGRESS_VOLUMES_DESC",
Repeat = "REPEAT",
RepeatDesc = "REPEAT_DESC",
Priority = "PRIORITY",
PriorityDesc = "PRIORITY_DESC",
StartedOn = "STARTED_ON",
StartedOnDesc = "STARTED_ON_DESC",
FinishedOn = "FINISHED_ON",
FinishedOnDesc = "FINISHED_ON_DESC",
AddedTime = "ADDED_TIME",
AddedTimeDesc = "ADDED_TIME_DESC",
UpdatedTime = "UPDATED_TIME",
UpdatedTimeDesc = "UPDATED_TIME_DESC",
MediaTitleRomaji = "MEDIA_TITLE_ROMAJI",
MediaTitleRomajiDesc = "MEDIA_TITLE_ROMAJI_DESC",
MediaTitleEnglish = "MEDIA_TITLE_ENGLISH",
MediaTitleEnglishDesc = "MEDIA_TITLE_ENGLISH_DESC",
MediaTitleNative = "MEDIA_TITLE_NATIVE",
MediaTitleNativeDesc = "MEDIA_TITLE_NATIVE_DESC",
MediaPopularity = "MEDIA_POPULARITY",
MediaPopularityDesc = "MEDIA_POPULARITY_DESC"
}

View File

@ -1,3 +1,7 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
background-color: rgba(27, 38, 54, 1);
text-align: center;

View File

@ -0,0 +1,14 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{svelte,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/aspect-ratio'),
],
}

View File

@ -1,5 +1,6 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
import {main} from '../models';
export function AniListLogin():Promise<void>;
@ -7,4 +8,8 @@ export function AniListSearch(arg1:string):Promise<any>;
export function GetAniListItem(arg1:number):Promise<any>;
export function GetAniListLoggedInUserId():Promise<main.AniListUser>;
export function GetAniListUserWatchingList(arg1:number,arg2:number,arg3:string):Promise<main.AniListCurrentUserWatchList>;
export function Greet(arg1:string):Promise<string>;

View File

@ -14,6 +14,14 @@ export function GetAniListItem(arg1) {
return window['go']['main']['App']['GetAniListItem'](arg1);
}
export function GetAniListLoggedInUserId() {
return window['go']['main']['App']['GetAniListLoggedInUserId']();
}
export function GetAniListUserWatchingList(arg1, arg2, arg3) {
return window['go']['main']['App']['GetAniListUserWatchingList'](arg1, arg2, arg3);
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}

67
frontend/wailsjs/go/models.ts Executable file
View File

@ -0,0 +1,67 @@
export namespace main {
export class AniListCurrentUserWatchList {
// Go type: struct { Page struct { PageInfo struct { Total int "json:\"total\""; PerPage int "json:\"perPage\""; CurrentPage int "json:\"currentPage\""; LastPage int "json:\"lastPage\""; HasNextPage bool "json:\"hasNextPage\"" } "json:\"pageInfo\""; MediaList []struct { ID int "json:\"id\""; MediaID int "json:\"mediaId\""; UserID int "json:\"userId\""; Media struct { ID int "json:\"id\""; IDMal int "json:\"idMal\""; Title struct { Romaji string "json:\"romaji\""; English string "json:\"english\""; Native string "json:\"native\"" } "json:\"title\""; Description string "json:\"description\""; CoverImage struct { Large string "json:\"large\"" } "json:\"coverImage\""; Season string "json:\"season\""; SeasonYear int "json:\"seasonYear\""; Status string "json:\"status\""; Episodes int "json:\"episodes\""; NextAiringEpisode struct { AiringAt int "json:\"airingAt\""; TimeUntilAiring int "json:\"timeUntilAiring\""; Episode int "json:\"episode\"" } "json:\"nextAiringEpisode\"" } "json:\"media\""; Status string "json:\"status\""; Notes string "json:\"notes\""; Progress int "json:\"progress\""; Score int "json:\"score\""; Repeat int "json:\"repeat\""; User struct { ID int "json:\"id\""; Name string "json:\"name\""; Avatar struct { Large string "json:\"large\""; Medium string "json:\"medium\"" } "json:\"avatar\""; Statistics struct { Anime struct { Count int "json:\"count\""; Statuses []struct { Status string "json:\"status\""; Count int "json:\"count\"" } "json:\"statuses\"" } "json:\"anime\"" } "json:\"statistics\"" } "json:\"user\"" } "json:\"mediaList\"" } "json:\"Page\"" }
data: any;
static createFrom(source: any = {}) {
return new AniListCurrentUserWatchList(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.data = this.convertValues(source["data"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
export class AniListUser {
// Go type: struct { Viewer struct { ID int "json:\"id\""; Name string "json:\"name\"" } "json:\"Viewer\"" }
data: any;
static createFrom(source: any = {}) {
return new AniListUser(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.data = this.convertValues(source["data"], Object);
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}

8
go.mod
View File

@ -5,15 +5,21 @@ go 1.21
toolchain go1.21.11
require (
github.com/99designs/keyring v1.2.2
github.com/joho/godotenv v1.5.1
github.com/wailsapp/wails/v2 v2.9.1
)
require (
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/danieljoos/wincred v1.1.2 // indirect
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.12.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
@ -23,6 +29,7 @@ require (
github.com/leaanthony/u v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
@ -35,6 +42,7 @@ require (
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
)

31
go.sum
View File

@ -1,17 +1,33 @@
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0=
github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM=
github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU=
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
@ -34,6 +50,10 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -45,6 +65,10 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ=
github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tkrajina/go-reflector v0.5.6 h1:hKQ0gyocG7vgMD2M3dRlYN6WBBOmdoOzJ6njQSepKdE=
@ -68,15 +92,22 @@ golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=