Compare commits
99 Commits
aa81102194
...
0.7.0
| Author | SHA1 | Date | |
|---|---|---|---|
| a3b4857175 | |||
| 6e4b9c041a | |||
| ae54fd20dd | |||
| 2c7e2d0eff | |||
| 9a9f055c38 | |||
| bc497521e7 | |||
| 48a6005725 | |||
| 93a94d0797 | |||
| a2576b044c | |||
| 2ee2d85e9e | |||
| 1090f112f3 | |||
| 58c9f449e0 | |||
| f016c90353 | |||
| 6bbe0f0f48 | |||
| d841fee1e7 | |||
| f29d8f378e | |||
| 35e93c0ca9 | |||
| 8c169d549a | |||
| b2a8a504f3 | |||
| c85a53a278 | |||
| 2cf3844e76 | |||
| 6ed5fe8b71 | |||
| 8a8baf7f8f | |||
| ca8c8beaf3 | |||
| 3e7f7d1c95 | |||
| b0ca864dfe | |||
| 5ed6dedeab | |||
| 3271af445a | |||
| e7e9e5b826 | |||
| 5337758dee | |||
| 426793e56a | |||
| a794b77654 | |||
| c510c2a138 | |||
| 8cbf5cb20c | |||
| 54a8924384 | |||
| d70153064f | |||
| 7960f8e26d | |||
| 2e5a4a4493 | |||
| 874b3952ee | |||
| 60eac10545 | |||
| 6d66d711ff | |||
| cd62e6c658 | |||
| 063c5016f3 | |||
| 5c4caf68e6 | |||
| c10e853564 | |||
| cd043d093f | |||
| 6db01f7f9f | |||
| 8460d56d55 | |||
| 8fedbe4607 | |||
| e069c47242 | |||
| aba0f2d1d5 | |||
| af6cb7f08a | |||
| 487e5ee5a8 | |||
| 631bd8b885 | |||
| b35be6926a | |||
| 5a9f4391dc | |||
| 72004c98b4 | |||
| 3db25bc33a | |||
| f3a1536953 | |||
| dc01aa314c | |||
| baed9a4a67 | |||
| 0e00120778 | |||
| 697692c277 | |||
| 3e82677c2c | |||
| b81549e5a6 | |||
| fd806df0a9 | |||
| 26f85dd412 | |||
| 4d9c54a116 | |||
| 068e568ec6 | |||
| 4e6f910e74 | |||
| b39f19f03a | |||
| 1a083deb54 | |||
| 61f8f5dd84 | |||
| 4509e479bc | |||
| ee8dd2e866 | |||
| 0c6a8a40c3 | |||
| 45845c2a69 | |||
| 3ec5eb1a03 | |||
| a2aa90edec | |||
| d08283bd2d | |||
| 73d349ee1a | |||
| c9c6650829 | |||
| 896c6640e2 | |||
| 18c744c1cf | |||
| 0475d39c6c | |||
| dde5d20537 | |||
| 314646e7f5 | |||
| 72dfbf4a03 | |||
| d4ad4bc430 | |||
| 49681f3ffb | |||
| d98d0e77c1 | |||
| 8e57b4b259 | |||
| 1e1c891173 | |||
| 5ee9c42352 | |||
| 23ec111c60 | |||
| f24ee9edfd | |||
| 1fd453f399 | |||
| 3ab77ea8d3 | |||
| 3edfed6272 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -30,3 +30,9 @@ package-lock.json
|
|||||||
.idea
|
.idea
|
||||||
.env
|
.env
|
||||||
environment.go
|
environment.go
|
||||||
|
|
||||||
|
# REST (http files)
|
||||||
|
http-client.private.env.json
|
||||||
|
|
||||||
|
# Build artifacts
|
||||||
|
build/*.tar.gz
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -17,26 +18,29 @@ func AniListQuery(body interface{}, login bool) (json.RawMessage, string) {
|
|||||||
if login && (AniListJWT{}) != aniListJwt {
|
if login && (AniListJWT{}) != aniListJwt {
|
||||||
response.Header.Add("Authorization", "Bearer "+aniListJwt.AccessToken)
|
response.Header.Add("Authorization", "Bearer "+aniListJwt.AccessToken)
|
||||||
} else if login {
|
} else if login {
|
||||||
return nil, "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("Content-Type", "application/json")
|
||||||
response.Header.Add("Accept", "application/json")
|
response.Header.Add("Accept", "application/json")
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
res, reserr := client.Do(response)
|
res, resErr := client.Do(response)
|
||||||
if reserr != nil {
|
if resErr != nil {
|
||||||
log.Printf("Failed at res, %s\n", err)
|
log.Printf("Failed at res, %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
returnedBody, err := io.ReadAll(res.Body)
|
returnedBody, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "Could not read the returned body."
|
||||||
|
}
|
||||||
|
|
||||||
return returnedBody, res.Status
|
return returnedBody, res.Status
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
|
func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
|
||||||
var user = a.GetAniListLoggedInUser()
|
user := a.GetAniListLoggedInUser()
|
||||||
|
|
||||||
var neededVariables interface{}
|
var neededVariables interface{}
|
||||||
|
|
||||||
@@ -90,6 +94,7 @@ func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
|
|||||||
timeUntilAiring
|
timeUntilAiring
|
||||||
episode
|
episode
|
||||||
}
|
}
|
||||||
|
genres
|
||||||
tags{
|
tags{
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -141,11 +146,11 @@ func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
|
|||||||
returnedBody, status := AniListQuery(body, login)
|
returnedBody, status := AniListQuery(body, login)
|
||||||
var post AniListGetSingleAnime
|
var post AniListGetSingleAnime
|
||||||
|
|
||||||
if status == "404 Not Found" && login == false {
|
if status == "404 Not Found" && !login {
|
||||||
return post
|
return post
|
||||||
}
|
}
|
||||||
|
|
||||||
if status == "404 Not Found" && login {
|
if status == "404 Not Found" {
|
||||||
post = a.GetAniListItem(aniId, false)
|
post = a.GetAniListItem(aniId, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +159,7 @@ func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
|
|||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal, %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if login == false {
|
if !login {
|
||||||
post.Data.MediaList.UserID = user.Data.Viewer.ID
|
post.Data.MediaList.UserID = user.Data.Viewer.ID
|
||||||
post.Data.MediaList.Status = ""
|
post.Data.MediaList.Status = ""
|
||||||
post.Data.MediaList.StartedAt.Year = 0
|
post.Data.MediaList.StartedAt.Year = 0
|
||||||
@@ -179,7 +184,7 @@ func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
|
|||||||
return post
|
return post
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) AniListSearch(query string) any {
|
func (a *App) AniListSearch(query string) (interface{}, error) {
|
||||||
type Variables struct {
|
type Variables struct {
|
||||||
Search string `json:"search"`
|
Search string `json:"search"`
|
||||||
ListType string `json:"listType"`
|
ListType string `json:"listType"`
|
||||||
@@ -219,6 +224,7 @@ func (a *App) AniListSearch(query string) any {
|
|||||||
timeUntilAiring
|
timeUntilAiring
|
||||||
episode
|
episode
|
||||||
}
|
}
|
||||||
|
genres
|
||||||
tags{
|
tags{
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -237,19 +243,21 @@ func (a *App) AniListSearch(query string) any {
|
|||||||
ListType: "ANIME",
|
ListType: "ANIME",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
returnedBody, _ := AniListQuery(body, false)
|
returnedBody, status := AniListQuery(body, false)
|
||||||
|
if status != "200 OK" {
|
||||||
|
return nil, fmt.Errorf("API search failed with status: %s", status)
|
||||||
|
}
|
||||||
var post interface{}
|
var post interface{}
|
||||||
err := json.Unmarshal(returnedBody, &post)
|
err := json.Unmarshal(returnedBody, &post)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal, %s\n", err)
|
||||||
|
return nil, fmt.Errorf("Failed to parse search results")
|
||||||
|
}
|
||||||
|
return post, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return post
|
func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) (AniListCurrentUserWatchList, error) {
|
||||||
}
|
user := a.GetAniListLoggedInUser()
|
||||||
|
|
||||||
func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) AniListCurrentUserWatchList {
|
|
||||||
var user = a.GetAniListLoggedInUser()
|
|
||||||
type Variables struct {
|
type Variables struct {
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
PerPage int `json:"perPage"`
|
PerPage int `json:"perPage"`
|
||||||
@@ -304,6 +312,7 @@ func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) Ani
|
|||||||
timeUntilAiring
|
timeUntilAiring
|
||||||
episode
|
episode
|
||||||
}
|
}
|
||||||
|
genres
|
||||||
tags{
|
tags{
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
@@ -398,11 +407,15 @@ func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) Ani
|
|||||||
err := json.Unmarshal(returnedBody, &badPost)
|
err := json.Unmarshal(returnedBody, &badPost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal, %s\n", err)
|
||||||
|
return post, fmt.Errorf("API authentication error")
|
||||||
}
|
}
|
||||||
log.Fatal(badPost.Errors[0].Message)
|
return post, fmt.Errorf("AniList API error: %s", badPost.Errors[0].Message)
|
||||||
|
}
|
||||||
|
if status != "200 OK" {
|
||||||
|
return post, fmt.Errorf("API request failed with status: %s", status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return post
|
return post, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSingleAnime {
|
func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSingleAnime {
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ type MediaList struct {
|
|||||||
TimeUntilAiring int `json:"timeUntilAiring"`
|
TimeUntilAiring int `json:"timeUntilAiring"`
|
||||||
Episode int `json:"episode"`
|
Episode int `json:"episode"`
|
||||||
} `json:"nextAiringEpisode"`
|
} `json:"nextAiringEpisode"`
|
||||||
|
Genres []string `json:"genres"`
|
||||||
Tags []struct {
|
Tags []struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -30,11 +31,11 @@ var aniCtxShutdown, aniCancel = context.WithCancel(context.Background())
|
|||||||
|
|
||||||
func (a *App) CheckIfAniListLoggedIn() bool {
|
func (a *App) CheckIfAniListLoggedIn() bool {
|
||||||
if (AniListJWT{} == aniListJwt) {
|
if (AniListJWT{} == aniListJwt) {
|
||||||
tokenType, err := aniRing.Get("anilistTokenType")
|
tokenType, tokenErr := aniRing.Get("anilistTokenType")
|
||||||
expiresIn, err := aniRing.Get("anilistTokenExpiresIn")
|
expiresIn, expiresInErr := aniRing.Get("anilistTokenExpiresIn")
|
||||||
accessToken, err := aniRing.Get("anilistAccessToken")
|
refreshToken, refreshTokenErr := aniRing.Get("anilistRefreshToken")
|
||||||
refreshToken, err := aniRing.Get("anilistRefreshToken")
|
accessToken, accessTokenErr := aniRing.Get("anilistAccessToken")
|
||||||
if err != nil || len(accessToken.Data) == 0 {
|
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
aniListJwt.TokenType = string(tokenType.Data)
|
aniListJwt.TokenType = string(tokenType.Data)
|
||||||
@@ -50,11 +51,11 @@ func (a *App) CheckIfAniListLoggedIn() bool {
|
|||||||
|
|
||||||
func (a *App) AniListLogin() {
|
func (a *App) AniListLogin() {
|
||||||
if (AniListJWT{} == aniListJwt) {
|
if (AniListJWT{} == aniListJwt) {
|
||||||
tokenType, err := aniRing.Get("anilistTokenType")
|
tokenType, tokenErr := aniRing.Get("anilistTokenType")
|
||||||
expiresIn, err := aniRing.Get("anilistTokenExpiresIn")
|
expiresIn, expiresInErr := aniRing.Get("anilistTokenExpiresIn")
|
||||||
accessToken, err := aniRing.Get("anilistAccessToken")
|
refreshToken, refreshTokenErr := aniRing.Get("anilistRefreshToken")
|
||||||
refreshToken, err := aniRing.Get("anilistRefreshToken")
|
accessToken, accessTokenErr := aniRing.Get("anilistAccessToken")
|
||||||
if err != nil || len(accessToken.Data) == 0 {
|
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
|
||||||
getAniListCodeUrl := "https://anilist.co/api/v2/oauth/authorize?client_id=" + Environment.ANILIST_APP_ID + "&redirect_uri=" + Environment.ANILIST_CALLBACK_URI + "&response_type=code"
|
getAniListCodeUrl := "https://anilist.co/api/v2/oauth/authorize?client_id=" + Environment.ANILIST_APP_ID + "&redirect_uri=" + Environment.ANILIST_CALLBACK_URI + "&response_type=code"
|
||||||
runtime.BrowserOpenURL(*wailsContext, getAniListCodeUrl)
|
runtime.BrowserOpenURL(*wailsContext, getAniListCodeUrl)
|
||||||
|
|
||||||
@@ -123,7 +124,7 @@ func (a *App) handleAniListCallback(wg *sync.WaitGroup) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Fatalf("listen: %s\n", err)
|
log.Fatalf("listen: %s\n", err)
|
||||||
}
|
}
|
||||||
fmt.Println("Shutting down...")
|
fmt.Println("Shutting down...")
|
||||||
@@ -152,14 +153,17 @@ func getAniListAuthorizationToken(content string) AniListJWT {
|
|||||||
response.Header.Add("Accept", "application/json")
|
response.Header.Add("Accept", "application/json")
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
res, reserr := client.Do(response)
|
res, resErr := client.Do(response)
|
||||||
if reserr != nil {
|
if resErr != nil {
|
||||||
log.Printf("Failed at res, %s\n", err)
|
log.Printf("Failed at res, %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
returnedBody, err := io.ReadAll(res.Body)
|
returnedBody, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not read returned body, %s\n.", err)
|
||||||
|
}
|
||||||
|
|
||||||
var post AniListJWT
|
var post AniListJWT
|
||||||
err = json.Unmarshal(returnedBody, &post)
|
err = json.Unmarshal(returnedBody, &post)
|
||||||
@@ -204,13 +208,12 @@ func (a *App) GetAniListLoggedInUser() AniListUser {
|
|||||||
|
|
||||||
func (a *App) LogoutAniList() string {
|
func (a *App) LogoutAniList() string {
|
||||||
if (AniListJWT{} != aniListJwt) {
|
if (AniListJWT{} != aniListJwt) {
|
||||||
err := aniRing.Remove("anilistTokenType")
|
typeErr := aniRing.Remove("anilistTokenType")
|
||||||
err = aniRing.Remove("anilistTokenExpiresIn")
|
expiresInErr := aniRing.Remove("anilistTokenExpiresIn")
|
||||||
err = aniRing.Remove("anilistAccessToken")
|
accessTokenErr := aniRing.Remove("anilistAccessToken")
|
||||||
err = aniRing.Remove("anilistRefreshToken")
|
refreshTokenErr := aniRing.Remove("anilistRefreshToken")
|
||||||
|
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil {
|
||||||
if err != nil {
|
fmt.Println("AniList Logout Failed")
|
||||||
fmt.Println("AniList Logout Failed", err)
|
|
||||||
}
|
}
|
||||||
aniListJwt = AniListJWT{}
|
aniListJwt = AniListJWT{}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,19 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func MALHelper(method string, malUrl string, body url.Values) (json.RawMessage, string) {
|
func MALHelper(method string, malUrl string, body url.Values) (json.RawMessage, string, error) {
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
|
||||||
req, _ := http.NewRequest(method, malUrl, strings.NewReader(body.Encode()))
|
req, err := http.NewRequest(method, malUrl, strings.NewReader(body.Encode()))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
message, _ := json.Marshal(struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
}{
|
||||||
|
Message: "Failed to create request: " + err.Error(),
|
||||||
|
})
|
||||||
|
return message, "", fmt.Errorf("failed to create request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
req.Header.Add("Authorization", "Bearer "+myAnimeListJwt.AccessToken)
|
req.Header.Add("Authorization", "Bearer "+myAnimeListJwt.AccessToken)
|
||||||
@@ -22,47 +31,57 @@ func MALHelper(method string, malUrl string, body url.Values) (json.RawMessage,
|
|||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Errored when sending request to the server")
|
|
||||||
message, _ := json.Marshal(struct {
|
message, _ := json.Marshal(struct {
|
||||||
Message string `json:"message" ts_type:"message"`
|
Message string `json:"message"`
|
||||||
}{
|
}{
|
||||||
Message: "Errored when sending request to the server" + err.Error(),
|
Message: "Network error: " + err.Error(),
|
||||||
})
|
})
|
||||||
|
return message, "", fmt.Errorf("network error: %w", err)
|
||||||
return message, resp.Status
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, resp.Status, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if resp.Status == "401 Unauthorized" {
|
if resp.Status == "401 Unauthorized" {
|
||||||
refreshMyAnimeListAuthorizationToken()
|
refreshMyAnimeListAuthorizationToken()
|
||||||
MALHelper(method, malUrl, body)
|
return MALHelper(method, malUrl, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
return respBody, resp.Status
|
if resp.Status != "200 OK" && resp.Status != "201 Created" && resp.Status != "202 Accepted" {
|
||||||
|
return respBody, resp.Status, fmt.Errorf("API returned status: %s", resp.Status)
|
||||||
|
}
|
||||||
|
return respBody, resp.Status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetMyAnimeList(count int) MALWatchlist {
|
func (a *App) GetMyAnimeList(count int) (MALWatchlist, error) {
|
||||||
limit := strconv.Itoa(count)
|
limit := strconv.Itoa(count)
|
||||||
user := a.GetMyAnimeListLoggedInUser()
|
user := a.GetMyAnimeListLoggedInUser()
|
||||||
malUrl := "https://api.myanimelist.net/v2/users/" + user.Name + "/animelist?fields=list_status&status=watching&limit=" + limit
|
malUrl := "https://api.myanimelist.net/v2/users/" + user.Name + "/animelist?fields=list_status&status=watching&limit=" + limit
|
||||||
|
|
||||||
var malList MALWatchlist
|
var malList MALWatchlist
|
||||||
|
|
||||||
respBody, resStatus := MALHelper("GET", malUrl, nil)
|
respBody, resStatus, err := MALHelper("GET", malUrl, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return malList, fmt.Errorf("failed to get MAL watchlist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if resStatus == "200 OK" {
|
if resStatus == "200 OK" {
|
||||||
err := json.Unmarshal(respBody, &malList)
|
err := json.Unmarshal(respBody, &malList)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to unmarshal json response, %s\n", err)
|
log.Printf("Failed to unmarshal json response, %s\n", err)
|
||||||
|
return malList, fmt.Errorf("failed to parse response: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return malList
|
return malList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) MyAnimeListUpdate(anime MALAnime, update MALUploadStatus) MalListStatus {
|
func (a *App) MyAnimeListUpdate(anime MALAnime, update MALUploadStatus) (MalListStatus, error) {
|
||||||
if update.NumTimesRewatched >= 1 {
|
if update.NumTimesRewatched >= 1 {
|
||||||
update.IsRewatching = true
|
update.IsRewatching = true
|
||||||
} else {
|
} else {
|
||||||
@@ -78,39 +97,52 @@ func (a *App) MyAnimeListUpdate(anime MALAnime, update MALUploadStatus) MalListS
|
|||||||
|
|
||||||
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(anime.Id) + "/my_list_status"
|
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(anime.Id) + "/my_list_status"
|
||||||
var status MalListStatus
|
var status MalListStatus
|
||||||
respBody, respStatus := MALHelper("PATCH", malUrl, body)
|
respBody, respStatus, err := MALHelper("PATCH", malUrl, body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return status, fmt.Errorf("failed to update MAL entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if respStatus == "200 OK" {
|
if respStatus == "200 OK" {
|
||||||
err := json.Unmarshal(respBody, &status)
|
err := json.Unmarshal(respBody, &status)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to unmarshal json response, %s\n", err)
|
log.Printf("Failed to unmarshal json response, %s\n", err)
|
||||||
|
return status, fmt.Errorf("failed to parse response: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return status, nil
|
||||||
|
}
|
||||||
|
|
||||||
return status
|
func (a *App) GetMyAnimeListAnime(id int) (MALAnime, error) {
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) GetMyAnimeListAnime(id int) MALAnime {
|
|
||||||
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(id) + "?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,genres,created_at,updated_at,media_type,status,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,recommendations,studios,statistics"
|
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(id) + "?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,genres,created_at,updated_at,media_type,status,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,recommendations,studios,statistics"
|
||||||
respBody, respStatus := MALHelper("GET", malUrl, nil)
|
|
||||||
var malAnime MALAnime
|
var malAnime MALAnime
|
||||||
|
respBody, respStatus, err := MALHelper("GET", malUrl, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return malAnime, fmt.Errorf("failed to get MAL anime: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if respStatus == "200 OK" {
|
if respStatus == "200 OK" {
|
||||||
err := json.Unmarshal(respBody, &malAnime)
|
err := json.Unmarshal(respBody, &malAnime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to unmarshal json response, %s\n", err)
|
log.Printf("Failed to unmarshal json response, %s\n", err)
|
||||||
|
return malAnime, fmt.Errorf("failed to parse response: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return malAnime, nil
|
||||||
|
}
|
||||||
|
|
||||||
return malAnime
|
func (a *App) DeleteMyAnimeListEntry(id int) (bool, error) {
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) DeleteMyAnimeListEntry(id int) bool {
|
|
||||||
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(id) + "/my_list_status"
|
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(id) + "/my_list_status"
|
||||||
_, respStatus := MALHelper("DELETE", malUrl, nil)
|
_, respStatus, err := MALHelper("DELETE", malUrl, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("failed to delete MAL entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if respStatus == "200 OK" {
|
if respStatus == "200 OK" {
|
||||||
return true
|
return true, nil
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false, fmt.Errorf("delete failed with status: %s", respStatus)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
MALTypes.go
16
MALTypes.go
@@ -46,10 +46,10 @@ type MALWatchlist struct {
|
|||||||
Id int `json:"id" ts_type:"id"`
|
Id int `json:"id" ts_type:"id"`
|
||||||
Title string `json:"title" ts_type:"title"`
|
Title string `json:"title" ts_type:"title"`
|
||||||
MainPicture struct {
|
MainPicture struct {
|
||||||
Medium string `json:"medium" json:"medium"`
|
Medium string `json:"medium" ts_type:"medium"`
|
||||||
Large string `json:"large" json:"large"`
|
Large string `json:"large" ts_type:"large"`
|
||||||
} `json:"main_picture" json:"mainPicture"`
|
} `json:"main_picture" ts_type:"mainPicture"`
|
||||||
} `json:"node" json:"node"`
|
} `json:"node" ts_type:"node"`
|
||||||
ListStatus struct {
|
ListStatus struct {
|
||||||
Status string `json:"status" ts_type:"status"`
|
Status string `json:"status" ts_type:"status"`
|
||||||
Score int `json:"score" ts_type:"score"`
|
Score int `json:"score" ts_type:"score"`
|
||||||
@@ -59,7 +59,7 @@ type MALWatchlist struct {
|
|||||||
StartDate string `json:"start_date" ts_type:"startDate"`
|
StartDate string `json:"start_date" ts_type:"startDate"`
|
||||||
FinishDate string `json:"finish_date" ts_type:"finishDate"`
|
FinishDate string `json:"finish_date" ts_type:"finishDate"`
|
||||||
} `json:"list_status" ts_type:"listStatus"`
|
} `json:"list_status" ts_type:"listStatus"`
|
||||||
} `json:"data" json:"data"`
|
} `json:"data" ts_type:"data"`
|
||||||
Paging struct {
|
Paging struct {
|
||||||
Previous string `json:"previous" ts_type:"previous"`
|
Previous string `json:"previous" ts_type:"previous"`
|
||||||
Next string `json:"next" ts_type:"next"`
|
Next string `json:"next" ts_type:"next"`
|
||||||
@@ -70,9 +70,9 @@ type MALAnime struct {
|
|||||||
Id int `json:"id" ts_type:"id"`
|
Id int `json:"id" ts_type:"id"`
|
||||||
Title string `json:"title" ts_type:"title"`
|
Title string `json:"title" ts_type:"title"`
|
||||||
MainPicture struct {
|
MainPicture struct {
|
||||||
Large string `json:"large" json:"large"`
|
Large string `json:"large" ts_type:"large"`
|
||||||
Medium string `json:"medium" json:"medium"`
|
Medium string `json:"medium" ts_type:"medium"`
|
||||||
} `json:"main_picture" json:"mainPicture"`
|
} `json:"main_picture" ts_type:"mainPicture"`
|
||||||
AlternativeTitles struct {
|
AlternativeTitles struct {
|
||||||
Synonyms []string `json:"synonyms" ts_type:"synonyms"`
|
Synonyms []string `json:"synonyms" ts_type:"synonyms"`
|
||||||
En string `json:"en" ts_type:"en"`
|
En string `json:"en" ts_type:"en"`
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -50,7 +51,7 @@ func base64URLEncode(str []byte) string {
|
|||||||
|
|
||||||
func verifier() (*CodeVerifier, error) {
|
func verifier() (*CodeVerifier, error) {
|
||||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||||
b := make([]byte, length, length)
|
b := make([]byte, length)
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < length; i++ {
|
||||||
b[i] = byte(r.Intn(255))
|
b[i] = byte(r.Intn(255))
|
||||||
}
|
}
|
||||||
@@ -71,16 +72,17 @@ func (v *CodeVerifier) CodeChallengeS256() string {
|
|||||||
|
|
||||||
func (a *App) CheckIfMyAnimeListLoggedIn() bool {
|
func (a *App) CheckIfMyAnimeListLoggedIn() bool {
|
||||||
if (MyAnimeListJWT{} == myAnimeListJwt) {
|
if (MyAnimeListJWT{} == myAnimeListJwt) {
|
||||||
tokenType, err := myAnimeListRing.Get("MyAnimeListTokenType")
|
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType")
|
||||||
expiresIn, err := myAnimeListRing.Get("MyAnimeListExpiresIn")
|
expiresIn, expiresInErr := myAnimeListRing.Get("MyAnimeListExpiresIn")
|
||||||
accessToken, err := myAnimeListRing.Get("MyAnimeListAccessToken")
|
refreshToken, refreshTokenErr := myAnimeListRing.Get("MyAnimeListAccessToken")
|
||||||
refreshToken, err := myAnimeListRing.Get("MyAnimeListRefreshToken")
|
accessToken, accessTokenErr := myAnimeListRing.Get("MyAnimeListRefreshToken")
|
||||||
if err != nil || len(accessToken.Data) == 0 {
|
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
|
var expiresInConvertErr error
|
||||||
myAnimeListJwt.TokenType = string(tokenType.Data)
|
myAnimeListJwt.TokenType = string(tokenType.Data)
|
||||||
myAnimeListJwt.ExpiresIn, err = strconv.Atoi(string(expiresIn.Data))
|
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data))
|
||||||
if err != nil {
|
if expiresInConvertErr != nil {
|
||||||
fmt.Println("unable to convert string to int")
|
fmt.Println("unable to convert string to int")
|
||||||
}
|
}
|
||||||
myAnimeListJwt.AccessToken = string(accessToken.Data)
|
myAnimeListJwt.AccessToken = string(accessToken.Data)
|
||||||
@@ -93,12 +95,13 @@ func (a *App) CheckIfMyAnimeListLoggedIn() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) MyAnimeListLogin() {
|
func (a *App) MyAnimeListLogin() {
|
||||||
if a.CheckIfMyAnimeListLoggedIn() == false {
|
if !a.CheckIfMyAnimeListLoggedIn() {
|
||||||
tokenType, err := myAnimeListRing.Get("MyAnimeListTokenType")
|
fmt.Println("check logged in function failed")
|
||||||
expiresIn, err := myAnimeListRing.Get("MyAnimeListExpiresIn")
|
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType")
|
||||||
accessToken, err := myAnimeListRing.Get("MyAnimeListAccessToken")
|
expiresIn, expiresInErr := myAnimeListRing.Get("MyAnimeListExpiresIn")
|
||||||
refreshToken, err := myAnimeListRing.Get("MyAnimeListRefreshToken")
|
refreshToken, refreshTokenErr := myAnimeListRing.Get("MyAnimeListAccessToken")
|
||||||
if err != nil || len(accessToken.Data) == 0 {
|
accessToken, accessTokenErr := myAnimeListRing.Get("MyAnimeListRefreshToken")
|
||||||
|
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
|
||||||
verifier, _ := verifier()
|
verifier, _ := verifier()
|
||||||
getMyAnimeListCodeUrl := "https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=" + Environment.MAL_CLIENT_ID + "&redirect_uri=" + Environment.MAL_CALLBACK_URI + "&code_challenge=" + verifier.Value + "&code_challenge_method=plain"
|
getMyAnimeListCodeUrl := "https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=" + Environment.MAL_CLIENT_ID + "&redirect_uri=" + Environment.MAL_CALLBACK_URI + "&code_challenge=" + verifier.Value + "&code_challenge_method=plain"
|
||||||
runtime.BrowserOpenURL(*wailsContext, getMyAnimeListCodeUrl)
|
runtime.BrowserOpenURL(*wailsContext, getMyAnimeListCodeUrl)
|
||||||
@@ -107,9 +110,10 @@ func (a *App) MyAnimeListLogin() {
|
|||||||
a.handleMyAnimeListCallback(serverDone, verifier)
|
a.handleMyAnimeListCallback(serverDone, verifier)
|
||||||
serverDone.Wait()
|
serverDone.Wait()
|
||||||
} else {
|
} else {
|
||||||
|
var expiresInConvertErr error
|
||||||
myAnimeListJwt.TokenType = string(tokenType.Data)
|
myAnimeListJwt.TokenType = string(tokenType.Data)
|
||||||
myAnimeListJwt.ExpiresIn, err = strconv.Atoi(string(expiresIn.Data))
|
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data))
|
||||||
if err != nil {
|
if expiresInConvertErr != nil {
|
||||||
fmt.Println("unable to convert string to int in Login function")
|
fmt.Println("unable to convert string to int in Login function")
|
||||||
}
|
}
|
||||||
myAnimeListJwt.AccessToken = string(accessToken.Data)
|
myAnimeListJwt.AccessToken = string(accessToken.Data)
|
||||||
@@ -171,8 +175,8 @@ func (a *App) handleMyAnimeListCallback(wg *sync.WaitGroup, verifier *CodeVerifi
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Fatalf("listen: %s\n", err)
|
log.Printf("Server error: %s\n", err)
|
||||||
}
|
}
|
||||||
fmt.Println("Shutting down...")
|
fmt.Println("Shutting down...")
|
||||||
}()
|
}()
|
||||||
@@ -210,14 +214,17 @@ func getMyAnimeListAuthorizationToken(content string, verifier *CodeVerifier) My
|
|||||||
response.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
response.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
res, reserr := client.Do(response)
|
res, resErr := client.Do(response)
|
||||||
if reserr != nil {
|
if resErr != nil {
|
||||||
log.Printf("Failed at res, %s\n", err)
|
log.Printf("Failed at res, %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
returnedBody, err := io.ReadAll(res.Body)
|
returnedBody, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not read returned body, %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
var post MyAnimeListJWT
|
var post MyAnimeListJWT
|
||||||
err = json.Unmarshal(returnedBody, &post)
|
err = json.Unmarshal(returnedBody, &post)
|
||||||
@@ -257,14 +264,17 @@ func refreshMyAnimeListAuthorizationToken() {
|
|||||||
response.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
response.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
res, reserr := client.Do(response)
|
res, resErr := client.Do(response)
|
||||||
if reserr != nil {
|
if resErr != nil {
|
||||||
log.Printf("Failed at res, %s\n", err)
|
log.Printf("Failed at res, %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
returnedBody, err := io.ReadAll(res.Body)
|
returnedBody, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not read returned body, %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(returnedBody, &myAnimeListJwt)
|
err = json.Unmarshal(returnedBody, &myAnimeListJwt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -291,17 +301,23 @@ func refreshMyAnimeListAuthorizationToken() {
|
|||||||
Title: "MyAnimeList Authorization",
|
Title: "MyAnimeList Authorization",
|
||||||
Message: "It is now safe to close your browser tab",
|
Message: "It is now safe to close your browser tab",
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser {
|
func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser {
|
||||||
a.MyAnimeListLogin()
|
a.MyAnimeListLogin()
|
||||||
|
user := createUser()
|
||||||
|
if user.Name == "" {
|
||||||
|
refreshMyAnimeListAuthorizationToken()
|
||||||
|
user = createUser()
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func createUser() MyAnimeListUser {
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
|
||||||
req, _ := http.NewRequest("GET", "https://api.myanimelist.net/v2/users/@me?fields=anime_statistics", nil)
|
req, _ := http.NewRequest("GET", "https://api.myanimelist.net/v2/users/@me?fields=anime_statistics", nil)
|
||||||
@@ -311,7 +327,6 @@ func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser {
|
|||||||
req.Header.Add("myAnimeList-api-key", Environment.MAL_CLIENT_ID)
|
req.Header.Add("myAnimeList-api-key", Environment.MAL_CLIENT_ID)
|
||||||
|
|
||||||
response, err := client.Do(req)
|
response, err := client.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at request, %s\n", err)
|
log.Printf("Failed at request, %s\n", err)
|
||||||
return MyAnimeListUser{}
|
return MyAnimeListUser{}
|
||||||
@@ -329,17 +344,17 @@ func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) LogoutMyAnimeList() string {
|
func (a *App) LogoutMyAnimeList() string {
|
||||||
if (MyAnimeListJWT{} != myAnimeListJwt) {
|
if (MyAnimeListJWT{} != myAnimeListJwt) {
|
||||||
err := myAnimeListRing.Remove("MyAnimeListTokenType")
|
typeErr := myAnimeListRing.Remove("MyAnimeListTokenType")
|
||||||
err = myAnimeListRing.Remove("MyAnimeListExpiresIn")
|
expiresInErr := myAnimeListRing.Remove("MyAnimeListExpiresIn")
|
||||||
err = myAnimeListRing.Remove("MyAnimeListAccessToken")
|
accessTokenErr := myAnimeListRing.Remove("MyAnimeListAccessToken")
|
||||||
err = myAnimeListRing.Remove("MyAnimeListRefreshToken")
|
refreshTokenErr := myAnimeListRing.Remove("MyAnimeListRefreshToken")
|
||||||
|
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil {
|
||||||
if err != nil {
|
fmt.Println("MAL Logout Failed")
|
||||||
fmt.Println("MAL Logout Failed", err)
|
|
||||||
}
|
}
|
||||||
myAnimeListJwt = MyAnimeListJWT{}
|
myAnimeListJwt = MyAnimeListJWT{}
|
||||||
}
|
}
|
||||||
|
|||||||
166
RELEASE_NOTES_v0.7.0.md
Normal file
166
RELEASE_NOTES_v0.7.0.md
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# 🎉 AniTrack v0.7.0 Release Notes
|
||||||
|
|
||||||
|
## 🚨 Major Feature: Comprehensive Error Handling
|
||||||
|
|
||||||
|
This release brings **massive improvements** to application stability and user experience. The app will **no longer crash** when API services are down or experiencing errors!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ What's New
|
||||||
|
|
||||||
|
### 🛡️ Graceful API Error Handling
|
||||||
|
- **Application Stability**: App now remains running even when AniList, MAL, or Simkl APIs are down
|
||||||
|
- **Visual Error Feedback**: Beautiful modal dialogs display error details instead of silent crashes
|
||||||
|
- **Manual Retry**: Users can retry failed API connections with a single button click
|
||||||
|
- **Continue Using App**: Dismiss error messages and continue with limited functionality
|
||||||
|
|
||||||
|
### 🔧 Backend Improvements
|
||||||
|
|
||||||
|
#### AniList Integration
|
||||||
|
- Replaced all `log.Fatal()` calls with proper error returns
|
||||||
|
- Added error handling for API failures, network issues, and authentication errors
|
||||||
|
- `AniListSearch` now returns detailed error messages
|
||||||
|
- `GetAniListUserWatchingList` gracefully handles 403/404 responses
|
||||||
|
|
||||||
|
#### MyAnimeList Integration
|
||||||
|
- Updated `MALHelper` to return structured errors
|
||||||
|
- All MAL API functions now return errors instead of crashing:
|
||||||
|
- `GetMyAnimeList`
|
||||||
|
- `MyAnimeListUpdate`
|
||||||
|
- `GetMyAnimeListAnime`
|
||||||
|
- `DeleteMyAnimeListEntry`
|
||||||
|
- Fixed OAuth callback server to prevent shutdown on errors
|
||||||
|
|
||||||
|
#### Simkl Integration
|
||||||
|
- Updated `SimklHelper` with status code validation (200-299 range)
|
||||||
|
- All Simkl API functions now return errors:
|
||||||
|
- `SimklGetUserWatchlist`
|
||||||
|
- `SimklSyncEpisodes`
|
||||||
|
- `SimklSyncRating`
|
||||||
|
- `SimklSyncStatus`
|
||||||
|
- `SimklSearch`
|
||||||
|
- `SimklSyncRemove`
|
||||||
|
- Replaced `log.Fatal()` with proper error logging
|
||||||
|
|
||||||
|
### 💻 Frontend Improvements
|
||||||
|
|
||||||
|
#### New Error Management System
|
||||||
|
- **Centralized Error State**: New reactive stores for API errors
|
||||||
|
- `apiError` - Tracks current error with service, message, and status
|
||||||
|
- `isApiDown` - Quick flag for API availability
|
||||||
|
- **Error Helper Functions**:
|
||||||
|
- `setApiError()` - Set error state from any component
|
||||||
|
- `clearApiError()` - Reset error state
|
||||||
|
|
||||||
|
#### New Error Modal Component
|
||||||
|
- Auto-displays when API errors occur
|
||||||
|
- Shows service name, error message, and HTTP status code
|
||||||
|
- **"Retry Connection"** button - Attempts to reconnect to the failed service
|
||||||
|
- **"Dismiss"** button - Close modal and continue using the app
|
||||||
|
- Beautiful red-themed styling with Tailwind CSS
|
||||||
|
- Supports all three services (AniList, MAL, Simkl)
|
||||||
|
|
||||||
|
#### Updated User Interface
|
||||||
|
- Home page now displays "API Unavailable" state when services are down
|
||||||
|
- Helpful warning messages guide users during outages
|
||||||
|
- App structure remains visible and functional during errors
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🐛 Bug Fixes
|
||||||
|
|
||||||
|
- **Critical**: Fixed app crashes when AniList API returns 403 Forbidden errors
|
||||||
|
- **Critical**: Fixed app crashes when AniList API is down or unreachable
|
||||||
|
- **Critical**: Fixed MAL OAuth server crashing on connection errors
|
||||||
|
- **Critical**: Fixed Simkl JSON marshaling errors crashing the app
|
||||||
|
- **Improved**: Better error messages for all API failures
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📊 Technical Details
|
||||||
|
|
||||||
|
### Go Backend Changes
|
||||||
|
- **AniListFunctions.go**: Updated 3 functions with error handling
|
||||||
|
- **MALFunctions.go**: Updated 5 functions with error handling
|
||||||
|
- **MALUserFunctions.go**: Fixed server error handling
|
||||||
|
- **SimklFunctions.go**: Updated 7 functions with error handling
|
||||||
|
- **SimklUserFunctions.go**: Fixed 2 critical error handling issues
|
||||||
|
|
||||||
|
### Frontend Changes
|
||||||
|
- **ErrorModal.svelte**: New component (73 lines)
|
||||||
|
- **GlobalVariablesAndHelperFunctions.svelte**: Added error state system (27 lines)
|
||||||
|
- **App.svelte**: Integrated ErrorModal into main layout
|
||||||
|
- **CheckIfAniListLoggedInAndLoadWatchList.svelte**: Added error handling
|
||||||
|
- **Home.svelte**: Added API down state display
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Impact
|
||||||
|
|
||||||
|
### Before (v0.6.5)
|
||||||
|
- ❌ App crashes to desktop when AniList API is down
|
||||||
|
- ❌ Users see only console error messages
|
||||||
|
- ❌ Must restart app after API failures
|
||||||
|
- ❌ No way to retry failed connections
|
||||||
|
|
||||||
|
### After (v0.7.0)
|
||||||
|
- ✅ App stays open during any API failures
|
||||||
|
- ✅ Beautiful modal dialogs explain what went wrong
|
||||||
|
- ✅ One-click retry to reconnect services
|
||||||
|
- ✅ Continue using app with limited functionality
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 Upgrading
|
||||||
|
|
||||||
|
No migration needed! This release is fully backward compatible.
|
||||||
|
|
||||||
|
Simply run:
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
wails build
|
||||||
|
```
|
||||||
|
|
||||||
|
Or download the latest release from the releases page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙏 Acknowledgments
|
||||||
|
|
||||||
|
This release addresses a critical stability issue that was affecting users during API service outages. Special thanks to the community for reporting these crashes and providing detailed error logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Full Changelog
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Error modal component with retry/dismiss functionality
|
||||||
|
- Centralized error state management system
|
||||||
|
- API availability status tracking
|
||||||
|
- Visual feedback for all API failures
|
||||||
|
- Manual retry functionality for all services
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- AniList API functions to return errors instead of crashing
|
||||||
|
- MAL API functions to return errors instead of crashing
|
||||||
|
- Simkl API functions to return errors instead of crashing
|
||||||
|
- Error handling from console-only to UI modal dialogs
|
||||||
|
- App behavior from crash-on-error to graceful-degradation
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- App crashes on AniList 403 errors
|
||||||
|
- App crashes on network failures
|
||||||
|
- MAL OAuth server shutdown on errors
|
||||||
|
- Simkl JSON marshaling crashes
|
||||||
|
- All `log.Fatal()` calls that terminated the app
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `alert()` calls in favor of modal system
|
||||||
|
- Vite timestamp file (cleanup)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version**: 0.7.0
|
||||||
|
**Release Date**: March 2026
|
||||||
|
**Upgrade Difficulty**: ⭐ Easy (backward compatible)
|
||||||
|
**Recommended**: ⭐⭐⭐⭐⭐ Highly recommended for all users
|
||||||
@@ -14,16 +14,24 @@ import (
|
|||||||
|
|
||||||
var SimklWatchList SimklWatchListType
|
var SimklWatchList SimklWatchListType
|
||||||
|
|
||||||
func SimklHelper(method string, url string, body interface{}) json.RawMessage {
|
func SimklHelper(method string, url string, body interface{}) (json.RawMessage, error) {
|
||||||
reader, _ := json.Marshal(body)
|
reader, err := json.Marshal(body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal body: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var req *http.Request
|
var req *http.Request
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
|
|
||||||
if body != nil {
|
if body != nil {
|
||||||
req, _ = http.NewRequest(method, url, bytes.NewBuffer(reader))
|
req, err = http.NewRequest(method, url, bytes.NewBuffer(reader))
|
||||||
} else {
|
} else {
|
||||||
req, _ = http.NewRequest(method, url, nil)
|
req, err = http.NewRequest(method, url, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
@@ -31,45 +39,46 @@ func SimklHelper(method string, url string, body interface{}) json.RawMessage {
|
|||||||
req.Header.Add("simkl-api-key", Environment.SIMKL_CLIENT_ID)
|
req.Header.Add("simkl-api-key", Environment.SIMKL_CLIENT_ID)
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Errored when sending request to the server")
|
return nil, fmt.Errorf("network error: %w", err)
|
||||||
message, _ := json.Marshal(struct {
|
|
||||||
Message string `json:"message"`
|
|
||||||
}{
|
|
||||||
Message: "Errored when sending request to the server" + err.Error(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return message
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
respBody, err := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
return respBody
|
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||||
|
}
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return respBody, fmt.Errorf("API returned status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
return respBody, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SimklGetUserWatchlist() SimklWatchListType {
|
func (a *App) SimklGetUserWatchlist() (SimklWatchListType, error) {
|
||||||
method := "GET"
|
method := "GET"
|
||||||
url := "https://api.simkl.com/sync/all-items/anime"
|
url := "https://api.simkl.com/sync/all-items/anime"
|
||||||
|
|
||||||
respBody := SimklHelper(method, url, nil)
|
respBody, err := SimklHelper(method, url, nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return SimklWatchListType{}, fmt.Errorf("failed to get Simkl watchlist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var errCheck struct {
|
var errCheck struct {
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
err := json.Unmarshal(respBody, &errCheck)
|
err = json.Unmarshal(respBody, &errCheck)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal, %s\n", err)
|
||||||
|
return SimklWatchListType{}, fmt.Errorf("failed to parse error response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if errCheck.Error != "" {
|
if errCheck.Error != "" {
|
||||||
a.LogoutSimkl()
|
a.LogoutSimkl()
|
||||||
return SimklWatchListType{}
|
return SimklWatchListType{}, fmt.Errorf("Simkl API error: %s", errCheck.Message)
|
||||||
}
|
}
|
||||||
|
|
||||||
var watchlist SimklWatchListType
|
var watchlist SimklWatchListType
|
||||||
@@ -77,15 +86,15 @@ func (a *App) SimklGetUserWatchlist() SimklWatchListType {
|
|||||||
err = json.Unmarshal(respBody, &watchlist)
|
err = json.Unmarshal(respBody, &watchlist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal, %s\n", err)
|
||||||
|
return SimklWatchListType{}, fmt.Errorf("failed to parse watchlist: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
SimklWatchList = watchlist
|
SimklWatchList = watchlist
|
||||||
|
|
||||||
return watchlist
|
return watchlist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SimklSyncEpisodes(anime SimklAnime, progress int) SimklAnime {
|
func (a *App) SimklSyncEpisodes(anime SimklAnime, progress int) (SimklAnime, error) {
|
||||||
|
|
||||||
var episodes []Episode
|
var episodes []Episode
|
||||||
var url string
|
var url string
|
||||||
var shows []SimklPostShow
|
var shows []SimklPostShow
|
||||||
@@ -116,13 +125,19 @@ func (a *App) SimklSyncEpisodes(anime SimklAnime, progress int) SimklAnime {
|
|||||||
|
|
||||||
simklSync := SimklSyncHistoryType{shows}
|
simklSync := SimklSyncHistoryType{shows}
|
||||||
|
|
||||||
respBody := SimklHelper("POST", url, simklSync)
|
respBody, err := SimklHelper("POST", url, simklSync)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to sync episodes: %s\n", err)
|
||||||
|
return anime, fmt.Errorf("failed to sync episodes: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var success interface{}
|
var success interface{}
|
||||||
|
|
||||||
err := json.Unmarshal(respBody, &success)
|
err = json.Unmarshal(respBody, &success)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal, %s\n", err)
|
||||||
|
return anime, fmt.Errorf("failed to parse response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, simklAnime := range SimklWatchList.Anime {
|
for i, simklAnime := range SimklWatchList.Anime {
|
||||||
@@ -135,12 +150,12 @@ func (a *App) SimklSyncEpisodes(anime SimklAnime, progress int) SimklAnime {
|
|||||||
|
|
||||||
WatchListUpdate(anime)
|
WatchListUpdate(anime)
|
||||||
|
|
||||||
return anime
|
return anime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SimklSyncRating(anime SimklAnime, rating int) SimklAnime {
|
func (a *App) SimklSyncRating(anime SimklAnime, rating int) (SimklAnime, error) {
|
||||||
var url string
|
var url string
|
||||||
var showWithRating = ShowWithRating{
|
showWithRating := ShowWithRating{
|
||||||
Title: anime.Show.Title,
|
Title: anime.Show.Title,
|
||||||
Ids: Ids{
|
Ids: Ids{
|
||||||
Simkl: anime.Show.Ids.Simkl,
|
Simkl: anime.Show.Ids.Simkl,
|
||||||
@@ -150,7 +165,7 @@ func (a *App) SimklSyncRating(anime SimklAnime, rating int) SimklAnime {
|
|||||||
Rating: rating,
|
Rating: rating,
|
||||||
}
|
}
|
||||||
|
|
||||||
var showWithoutRating = ShowWithoutRating{
|
showWithoutRating := ShowWithoutRating{
|
||||||
Title: anime.Show.Title,
|
Title: anime.Show.Title,
|
||||||
Ids: Ids{
|
Ids: Ids{
|
||||||
Simkl: anime.Show.Ids.Simkl,
|
Simkl: anime.Show.Ids.Simkl,
|
||||||
@@ -173,13 +188,17 @@ func (a *App) SimklSyncRating(anime SimklAnime, rating int) SimklAnime {
|
|||||||
Shows []interface{} `json:"shows" ts_type:"shows"`
|
Shows []interface{} `json:"shows" ts_type:"shows"`
|
||||||
}{shows}
|
}{shows}
|
||||||
|
|
||||||
respBody := SimklHelper("POST", url, simklSync)
|
respBody, err := SimklHelper("POST", url, simklSync)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to sync rating: %s\n", err)
|
||||||
|
return anime, fmt.Errorf("failed to sync rating: %w", err)
|
||||||
|
}
|
||||||
var success interface{}
|
var success interface{}
|
||||||
|
|
||||||
err := json.Unmarshal(respBody, &success)
|
err = json.Unmarshal(respBody, &success)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal, %s\n", err)
|
||||||
|
return anime, fmt.Errorf("failed to parse response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, simklAnime := range SimklWatchList.Anime {
|
for i, simklAnime := range SimklWatchList.Anime {
|
||||||
@@ -192,12 +211,12 @@ func (a *App) SimklSyncRating(anime SimklAnime, rating int) SimklAnime {
|
|||||||
|
|
||||||
WatchListUpdate(anime)
|
WatchListUpdate(anime)
|
||||||
|
|
||||||
return anime
|
return anime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SimklSyncStatus(anime SimklAnime, status string) SimklAnime {
|
func (a *App) SimklSyncStatus(anime SimklAnime, status string) (SimklAnime, error) {
|
||||||
url := "https://api.simkl.com/sync/add-to-list"
|
url := "https://api.simkl.com/sync/add-to-list"
|
||||||
var show = SimklShowStatus{
|
show := SimklShowStatus{
|
||||||
Title: anime.Show.Title,
|
Title: anime.Show.Title,
|
||||||
Ids: Ids{
|
Ids: Ids{
|
||||||
Simkl: anime.Show.Ids.Simkl,
|
Simkl: anime.Show.Ids.Simkl,
|
||||||
@@ -215,13 +234,17 @@ func (a *App) SimklSyncStatus(anime SimklAnime, status string) SimklAnime {
|
|||||||
Shows []SimklShowStatus `json:"shows" ts_type:"shows"`
|
Shows []SimklShowStatus `json:"shows" ts_type:"shows"`
|
||||||
}{shows}
|
}{shows}
|
||||||
|
|
||||||
respBody := SimklHelper("POST", url, simklSync)
|
respBody, err := SimklHelper("POST", url, simklSync)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to sync status: %s\n", err)
|
||||||
|
return anime, fmt.Errorf("failed to sync status: %w", err)
|
||||||
|
}
|
||||||
var success interface{}
|
var success interface{}
|
||||||
|
|
||||||
err := json.Unmarshal(respBody, &success)
|
err = json.Unmarshal(respBody, &success)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal, %s\n", err)
|
||||||
|
return anime, fmt.Errorf("failed to parse response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, simklAnime := range SimklWatchList.Anime {
|
for i, simklAnime := range SimklWatchList.Anime {
|
||||||
@@ -234,15 +257,20 @@ func (a *App) SimklSyncStatus(anime SimklAnime, status string) SimklAnime {
|
|||||||
|
|
||||||
WatchListUpdate(anime)
|
WatchListUpdate(anime)
|
||||||
|
|
||||||
return anime
|
return anime, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SimklSearch(aniListAnime MediaList) SimklAnime {
|
func (a *App) SimklSearch(aniListAnime MediaList) (SimklAnime, error) {
|
||||||
var result SimklAnime
|
var result SimklAnime
|
||||||
|
|
||||||
if reflect.DeepEqual(SimklWatchList, SimklWatchListType{}) {
|
if reflect.DeepEqual(SimklWatchList, SimklWatchListType{}) {
|
||||||
fmt.Println("Watchlist empty. Calling...")
|
fmt.Println("Watchlist empty. Calling...")
|
||||||
SimklWatchList = a.SimklGetUserWatchlist()
|
watchlist, err := a.SimklGetUserWatchlist()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to get watchlist: %s\n", err)
|
||||||
|
return result, fmt.Errorf("failed to load watchlist for search: %w", err)
|
||||||
|
}
|
||||||
|
SimklWatchList = watchlist
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, anime := range SimklWatchList.Anime {
|
for _, anime := range SimklWatchList.Anime {
|
||||||
@@ -259,23 +287,30 @@ func (a *App) SimklSearch(aniListAnime MediaList) SimklAnime {
|
|||||||
var anime SimklSearchType
|
var anime SimklSearchType
|
||||||
url := "https://api.simkl.com/search/id?anilist=" + strconv.Itoa(aniListAnime.Media.ID)
|
url := "https://api.simkl.com/search/id?anilist=" + strconv.Itoa(aniListAnime.Media.ID)
|
||||||
|
|
||||||
respBody := SimklHelper("GET", url, nil)
|
respBody, err := SimklHelper("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
err := json.Unmarshal(respBody, &anime)
|
log.Printf("Failed to search Simkl: %s\n", err)
|
||||||
|
return result, fmt.Errorf("failed to search Simkl by AniList ID: %w", err)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(respBody, &anime)
|
||||||
|
|
||||||
if len(anime) == 0 {
|
if len(anime) == 0 {
|
||||||
url = "https://api.simkl.com/search/id?mal=" + strconv.Itoa(aniListAnime.Media.IDMal)
|
url = "https://api.simkl.com/search/id?mal=" + strconv.Itoa(aniListAnime.Media.IDMal)
|
||||||
respBody = SimklHelper("GET", url, nil)
|
respBody, err = SimklHelper("GET", url, nil)
|
||||||
fmt.Println(string(respBody))
|
if err != nil {
|
||||||
|
log.Printf("Failed to search Simkl by MAL ID: %s\n", err)
|
||||||
|
return result, fmt.Errorf("failed to search by MAL ID: %w", err)
|
||||||
|
}
|
||||||
err = json.Unmarshal(respBody, &anime)
|
err = json.Unmarshal(respBody, &anime)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal, %s\n", err)
|
||||||
|
return result, fmt.Errorf("failed to parse search results: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(anime) == 0 {
|
if len(anime) == 0 {
|
||||||
return result
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, watchListAnime := range SimklWatchList.Anime {
|
for _, watchListAnime := range SimklWatchList.Anime {
|
||||||
@@ -293,14 +328,14 @@ func (a *App) SimklSearch(aniListAnime MediaList) SimklAnime {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SimklSyncRemove(anime SimklAnime) bool {
|
func (a *App) SimklSyncRemove(anime SimklAnime) (bool, error) {
|
||||||
url := "https://api.simkl.com/sync/history/remove"
|
url := "https://api.simkl.com/sync/history/remove"
|
||||||
var showArray []SimklShowStatus
|
var showArray []SimklShowStatus
|
||||||
|
|
||||||
var singleShow = SimklShowStatus{
|
singleShow := SimklShowStatus{
|
||||||
Title: anime.Show.Title,
|
Title: anime.Show.Title,
|
||||||
Ids: Ids{
|
Ids: Ids{
|
||||||
Simkl: anime.Show.Ids.Simkl,
|
Simkl: anime.Show.Ids.Simkl,
|
||||||
@@ -317,25 +352,28 @@ func (a *App) SimklSyncRemove(anime SimklAnime) bool {
|
|||||||
Shows: showArray,
|
Shows: showArray,
|
||||||
}
|
}
|
||||||
|
|
||||||
respBody := SimklHelper("POST", url, show)
|
respBody, err := SimklHelper("POST", url, show)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to sync remove: %s\n", err)
|
||||||
|
return false, fmt.Errorf("failed to sync remove: %w", err)
|
||||||
|
}
|
||||||
var success SimklDeleteType
|
var success SimklDeleteType
|
||||||
|
err = json.Unmarshal(respBody, &success)
|
||||||
err := json.Unmarshal(respBody, &success)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal, %s\n", err)
|
||||||
|
return false, fmt.Errorf("failed to parse response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if success.Deleted.Shows >= 1 {
|
if success.Deleted.Shows >= 1 {
|
||||||
for i, simklAnime := range SimklWatchList.Anime {
|
for i, simklAnime := range SimklWatchList.Anime {
|
||||||
if simklAnime.Show.Ids.Simkl == anime.Show.Ids.Simkl {
|
if simklAnime.Show.Ids.Simkl == anime.Show.Ids.Simkl {
|
||||||
SimklWatchList.Anime = slices.Delete(SimklWatchList.Anime, i, i+1)
|
SimklWatchList.Anime = slices.Delete(SimklWatchList.Anime, i, i+1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true, nil
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false, fmt.Errorf("no shows were deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
func WatchListUpdate(anime SimklAnime) {
|
func WatchListUpdate(anime SimklAnime) {
|
||||||
@@ -349,5 +387,4 @@ func WatchListUpdate(anime SimklAnime) {
|
|||||||
if !updated {
|
if !updated {
|
||||||
SimklWatchList.Anime = append(SimklWatchList.Anime, anime)
|
SimklWatchList.Anime = append(SimklWatchList.Anime, anime)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -28,10 +29,10 @@ var simklCtxShutdown, simklCancel = context.WithCancel(context.Background())
|
|||||||
|
|
||||||
func (a *App) CheckIfSimklLoggedIn() bool {
|
func (a *App) CheckIfSimklLoggedIn() bool {
|
||||||
if (SimklJWT{} == simklJwt) {
|
if (SimklJWT{} == simklJwt) {
|
||||||
tokenType, err := simklRing.Get("SimklTokenType")
|
tokenType, tokenTypeErr := simklRing.Get("SimklTokenType")
|
||||||
accessToken, err := simklRing.Get("SimklAccessToken")
|
accessToken, accessTokenErr := simklRing.Get("SimklAccessToken")
|
||||||
scope, err := simklRing.Get("SimklScope")
|
scope, scopeErr := simklRing.Get("SimklScope")
|
||||||
if err != nil || len(accessToken.Data) == 0 {
|
if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken.Data) == 0 {
|
||||||
return false
|
return false
|
||||||
} else {
|
} else {
|
||||||
simklJwt.TokenType = string(tokenType.Data)
|
simklJwt.TokenType = string(tokenType.Data)
|
||||||
@@ -45,11 +46,11 @@ func (a *App) CheckIfSimklLoggedIn() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SimklLogin() {
|
func (a *App) SimklLogin() {
|
||||||
if a.CheckIfSimklLoggedIn() == false {
|
if !a.CheckIfSimklLoggedIn() {
|
||||||
tokenType, err := simklRing.Get("SimklTokenType")
|
tokenType, tokenTypeErr := simklRing.Get("SimklTokenType")
|
||||||
accessToken, err := simklRing.Get("SimklAccessToken")
|
accessToken, accessTokenErr := simklRing.Get("SimklAccessToken")
|
||||||
scope, err := simklRing.Get("SimklScope")
|
scope, scopeErr := simklRing.Get("SimklScope")
|
||||||
if err != nil || len(accessToken.Data) == 0 {
|
if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken.Data) == 0 {
|
||||||
getSimklCodeUrl := "https://simkl.com/oauth/authorize?response_type=code&client_id=" + Environment.SIMKL_CLIENT_ID + "&redirect_uri=" + Environment.SIMKL_CALLBACK_URI
|
getSimklCodeUrl := "https://simkl.com/oauth/authorize?response_type=code&client_id=" + Environment.SIMKL_CLIENT_ID + "&redirect_uri=" + Environment.SIMKL_CALLBACK_URI
|
||||||
runtime.BrowserOpenURL(*wailsContext, getSimklCodeUrl)
|
runtime.BrowserOpenURL(*wailsContext, getSimklCodeUrl)
|
||||||
|
|
||||||
@@ -114,8 +115,8 @@ func (a *App) handleSimklCallback(wg *sync.WaitGroup) {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
log.Fatalf("listen: %s\n", err)
|
log.Printf("Server error: %s\n", err)
|
||||||
}
|
}
|
||||||
fmt.Println("Shutting down...")
|
fmt.Println("Shutting down...")
|
||||||
}()
|
}()
|
||||||
@@ -137,7 +138,8 @@ func getSimklAuthorizationToken(content string) SimklJWT {
|
|||||||
}
|
}
|
||||||
jsonData, err := json.Marshal(data)
|
jsonData, err := json.Marshal(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Printf("Failed to marshal data: %s\n", err)
|
||||||
|
return SimklJWT{}
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := http.NewRequest("POST", "https://api.simkl.com/oauth/token", bytes.NewBuffer(jsonData))
|
response, err := http.NewRequest("POST", "https://api.simkl.com/oauth/token", bytes.NewBuffer(jsonData))
|
||||||
@@ -147,14 +149,17 @@ func getSimklAuthorizationToken(content string) SimklJWT {
|
|||||||
response.Header.Add("Content-Type", "application/json")
|
response.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
res, reserr := client.Do(response)
|
res, resErr := client.Do(response)
|
||||||
if reserr != nil {
|
if resErr != nil {
|
||||||
log.Printf("Failed at res, %s\n", err)
|
log.Printf("Failed at res, %s\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
returnedBody, err := io.ReadAll(res.Body)
|
returnedBody, err := io.ReadAll(res.Body)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not read returned body, %s\n.", err)
|
||||||
|
}
|
||||||
|
|
||||||
var post SimklJWT
|
var post SimklJWT
|
||||||
err = json.Unmarshal(returnedBody, &post)
|
err = json.Unmarshal(returnedBody, &post)
|
||||||
@@ -177,7 +182,6 @@ func (a *App) GetSimklLoggedInUser() SimklUser {
|
|||||||
req.Header.Add("simkl-api-key", Environment.SIMKL_CLIENT_ID)
|
req.Header.Add("simkl-api-key", Environment.SIMKL_CLIENT_ID)
|
||||||
|
|
||||||
response, err := client.Do(req)
|
response, err := client.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at request, %s\n", err)
|
log.Printf("Failed at request, %s\n", err)
|
||||||
return SimklUser{}
|
return SimklUser{}
|
||||||
@@ -193,7 +197,6 @@ func (a *App) GetSimklLoggedInUser() SimklUser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(respBody, &errCheck)
|
err = json.Unmarshal(respBody, &errCheck)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed at unmarshal, %s\n", err)
|
log.Printf("Failed at unmarshal, %s\n", err)
|
||||||
}
|
}
|
||||||
@@ -215,12 +218,12 @@ func (a *App) GetSimklLoggedInUser() SimklUser {
|
|||||||
|
|
||||||
func (a *App) LogoutSimkl() string {
|
func (a *App) LogoutSimkl() string {
|
||||||
if (SimklJWT{} != simklJwt) {
|
if (SimklJWT{} != simklJwt) {
|
||||||
err := simklRing.Remove("SimklTokenType")
|
tokenTypeErr := simklRing.Remove("SimklTokenType")
|
||||||
err = simklRing.Remove("SimklAccessToken")
|
accessTokenErr := simklRing.Remove("SimklAccessToken")
|
||||||
err = simklRing.Remove("SimklScope")
|
scopeErr := simklRing.Remove("SimklScope")
|
||||||
|
|
||||||
if err != nil {
|
if tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil {
|
||||||
fmt.Println("Simkl Logout Failed", err)
|
fmt.Println("Simkl Logout Failed")
|
||||||
}
|
}
|
||||||
simklJwt = SimklJWT{}
|
simklJwt = SimklJWT{}
|
||||||
}
|
}
|
||||||
|
|||||||
15
app.go
15
app.go
@@ -3,9 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/wailsapp/wails/v2/pkg/options"
|
"github.com/wailsapp/wails/v2/pkg/options"
|
||||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
@@ -43,3 +45,14 @@ func (a *App) onSecondInstanceLaunch(secondInstanceData options.SecondInstanceDa
|
|||||||
runtime.Show(*wailsContext)
|
runtime.Show(*wailsContext)
|
||||||
go runtime.EventsEmit(*wailsContext, "launchArgs", secondInstanceArgs)
|
go runtime.EventsEmit(*wailsContext, "launchArgs", secondInstanceArgs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) ShowVersion() {
|
||||||
|
version := gjson.Get(wailsJSON, "info.productVersion")
|
||||||
|
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{
|
||||||
|
Title: "Version",
|
||||||
|
Message: "AniTrack Version: " + version.String(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ body:graphql {
|
|||||||
media {
|
media {
|
||||||
id
|
id
|
||||||
idMal
|
idMal
|
||||||
|
genres
|
||||||
tags {
|
tags {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ body:graphql {
|
|||||||
english
|
english
|
||||||
native
|
native
|
||||||
}
|
}
|
||||||
|
genre
|
||||||
description
|
description
|
||||||
coverImage {
|
coverImage {
|
||||||
large
|
large
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ post {
|
|||||||
|
|
||||||
headers {
|
headers {
|
||||||
Accept: application/json
|
Accept: application/json
|
||||||
Content-Type: application/json
|
Content-Type: application/x-www-form-urlencoded
|
||||||
}
|
}
|
||||||
|
|
||||||
body:form-urlencoded {
|
body:form-urlencoded {
|
||||||
@@ -20,7 +20,7 @@ body:form-urlencoded {
|
|||||||
client_id: {{ANILIST_APP_ID}}
|
client_id: {{ANILIST_APP_ID}}
|
||||||
client_secret: {{ANILIST_SECRET_TOKEN}}
|
client_secret: {{ANILIST_SECRET_TOKEN}}
|
||||||
redirect_uri: http://localhost:6734/callback
|
redirect_uri: http://localhost:6734/callback
|
||||||
code: {{code}}
|
code: {{ANILIST_CODE}}
|
||||||
}
|
}
|
||||||
|
|
||||||
body:multipart-form {
|
body:multipart-form {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ vars {
|
|||||||
}
|
}
|
||||||
vars:secret [
|
vars:secret [
|
||||||
ANILIST_ACCESS_TOKEN,
|
ANILIST_ACCESS_TOKEN,
|
||||||
code,
|
ANILIST_CODE,
|
||||||
SIMKL_AUTH_TOKEN,
|
SIMKL_AUTH_TOKEN,
|
||||||
MAL_CODE,
|
MAL_CODE,
|
||||||
MAL_VERIFIER,
|
MAL_VERIFIER,
|
||||||
|
|||||||
11
build/AniTrack.desktop
Executable file
11
build/AniTrack.desktop
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Name=AniTrack
|
||||||
|
Comment=A manual synchronizer for various Anime trackers.
|
||||||
|
Exec=/home/nymusicman/Applications/AniTrack
|
||||||
|
Icon=AniTrack
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
StartupNotify=true
|
||||||
|
Categories=Internet
|
||||||
|
Keywords=anitrack;anilist;simkl;mal;myanimelist;anime;sync
|
||||||
|
Path=
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB |
BIN
build/icon/128/AniTrack.png
Normal file
BIN
build/icon/128/AniTrack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
BIN
build/icon/32/AniTrack.png
Normal file
BIN
build/icon/32/AniTrack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
build/icon/48/AniTrack.png
Normal file
BIN
build/icon/48/AniTrack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
BIN
build/icon/64/AniTrack.png
Normal file
BIN
build/icon/64/AniTrack.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
26
build/install_linux.sh
Executable file
26
build/install_linux.sh
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# copy desktop file
|
||||||
|
if [ -e "~/.local/share/applications/AniTrack.desktop" ]; then
|
||||||
|
if [ -d "~/.local/share/applications/" ]; then
|
||||||
|
cp ./AniTrack.desktop ~/.local/share/applications/
|
||||||
|
else
|
||||||
|
mkdir -p ~/.local/share/applications/
|
||||||
|
cp ./AniTrack.desktop ~/.local/share/applications/
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# copy icons to xdg folders
|
||||||
|
for size in 32 48 64 128; do
|
||||||
|
xdg-icon-resource install --novendor --context apps --size $size ./icon/$size/AniTrack.png AniTrack
|
||||||
|
done
|
||||||
|
|
||||||
|
# copy AniTrack Binary to $HOME/Applications/
|
||||||
|
if ! [ -d "~/Applications" ]; then
|
||||||
|
mkdir -p ~/Applications
|
||||||
|
cp ./bin/AniTrack ~/Applications/
|
||||||
|
elif ! [[ -e ~/Applications/AniTrack ]]; then
|
||||||
|
cp ./bin/AniTrack ~/Applications/
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "AniTrack has been successfully installed."
|
||||||
5
frontend/.vscode/extensions.json
vendored
5
frontend/.vscode/extensions.json
vendored
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"svelte.svelte-vscode"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script src="./src/main.ts" type="module"></script>
|
<script src="./src/main.ts" type="module"></script>
|
||||||
<script src="./node_modules/flowbite/dist/flowbite.js"></script>
|
<script
|
||||||
|
src="./node_modules/flowbite/dist/flowbite.js"
|
||||||
|
type="module"
|
||||||
|
></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"postcss": "^8.4.45",
|
"postcss": "^8.4.45",
|
||||||
"svelte": "^4.0.0",
|
"svelte": "^4.0.0",
|
||||||
"svelte-check": "^3.4.3",
|
"svelte-check": "^3.4.3",
|
||||||
"svelte-headless-table": "^0.18.2",
|
"svelte-headless-table": "^0.18.3",
|
||||||
"svelte-preprocess": "^5.0.3",
|
"svelte-preprocess": "^5.0.3",
|
||||||
"svelte-spa-router": "^4.0.1",
|
"svelte-spa-router": "^4.0.1",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
|
|||||||
@@ -1,44 +1,72 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
aniListAnime,
|
aniListLoggedIn,
|
||||||
GetAnimeSingleItem,
|
malLoggedIn,
|
||||||
|
simklLoggedIn,
|
||||||
|
watchlistNeedsRefresh,
|
||||||
|
aniListPrimary,
|
||||||
|
malPrimary,
|
||||||
|
simklPrimary,
|
||||||
|
malWatchList,
|
||||||
|
simklWatchList,
|
||||||
} from "./helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
} from "./helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import Router from "svelte-spa-router"
|
import Router from "svelte-spa-router";
|
||||||
import Home from "./routes/Home.svelte";
|
import Home from "./routes/Home.svelte";
|
||||||
import { wrap } from "svelte-spa-router/wrap";
|
import { wrap } from "svelte-spa-router/wrap";
|
||||||
import Spinner from "./helperComponents/Spinner.svelte";
|
import Spinner from "./helperComponents/Spinner.svelte";
|
||||||
import Header from "./helperComponents/Header.svelte";
|
import Header from "./helperComponents/Header.svelte";
|
||||||
import { CheckIfAniListLoggedInAndLoadWatchList } from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
|
import { CheckIfAniListLoggedInAndLoadWatchList } from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
|
||||||
import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte";
|
import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte";
|
||||||
import {CheckIfSimklLoggedInAndSetUser} from "./helperModules/CheckIsSimklLoggedIn.svelte"
|
import { CheckIfSimklLoggedInAndSetUser } from "./helperModules/CheckIsSimklLoggedIn.svelte";
|
||||||
import {CheckIfAniListLoggedIn} from "../wailsjs/go/main/App";
|
import {
|
||||||
import {AniListGetSingleAnimeDefaultData} from "./helperDefaults/AniListGetSingleAnime";
|
CheckIfAniListLoggedIn,
|
||||||
|
GetMyAnimeList,
|
||||||
|
SimklGetUserWatchlist,
|
||||||
|
} from "../wailsjs/go/main/App";
|
||||||
|
import { loc } from "svelte-spa-router";
|
||||||
|
import ErrorModal from "./helperComponents/ErrorModal.svelte";
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await CheckIfAniListLoggedInAndLoadWatchList()
|
let isAniListLoggedIn: boolean;
|
||||||
await CheckIfMALLoggedInAndSetUser()
|
let isMALLoggedIn: boolean;
|
||||||
await CheckIfSimklLoggedInAndSetUser()
|
let isSimklLoggedIn: boolean;
|
||||||
})
|
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
|
||||||
|
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
|
||||||
|
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
|
||||||
|
|
||||||
|
!isAniListLoggedIn && (await CheckIfAniListLoggedInAndLoadWatchList());
|
||||||
|
!isMALLoggedIn && (await CheckIfMALLoggedInAndSetUser());
|
||||||
|
!isSimklLoggedIn && (await CheckIfSimklLoggedInAndSetUser());
|
||||||
|
});
|
||||||
|
|
||||||
|
$: if ($loc?.location === "/" && $watchlistNeedsRefresh) {
|
||||||
|
(async () => {
|
||||||
|
if ($aniListLoggedIn && $aniListPrimary) {
|
||||||
|
await CheckIfAniListLoggedInAndLoadWatchList();
|
||||||
|
}
|
||||||
|
if ($malLoggedIn && $malPrimary) {
|
||||||
|
await GetMyAnimeList(1000).then((w) => malWatchList.set(w));
|
||||||
|
}
|
||||||
|
if ($simklLoggedIn && $simklPrimary) {
|
||||||
|
await SimklGetUserWatchlist().then((w) => simklWatchList.set(w));
|
||||||
|
}
|
||||||
|
|
||||||
|
watchlistNeedsRefresh.set(false);
|
||||||
|
})();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Header />
|
<Header />
|
||||||
<Router routes={{
|
<ErrorModal />
|
||||||
'/': Home,
|
<Router
|
||||||
'/anime/:id': wrap({
|
routes={{
|
||||||
asyncComponent: () => import('./routes/AnimeRoutePage.svelte'),
|
"/": Home,
|
||||||
conditions: [
|
"/anime/:id": wrap({
|
||||||
async () => await CheckIfAniListLoggedIn(),
|
asyncComponent: () => import("./routes/AnimeRoutePage.svelte"),
|
||||||
async (detail) => {
|
conditions: [async () => await CheckIfAniListLoggedIn()],
|
||||||
aniListAnime.update(value => {
|
loadingComponent: Spinner,
|
||||||
value = AniListGetSingleAnimeDefaultData
|
|
||||||
return value
|
|
||||||
})
|
|
||||||
await GetAnimeSingleItem(Number(detail.params.id), true)
|
|
||||||
return Object.keys($aniListAnime).length!==0
|
|
||||||
},
|
|
||||||
],
|
|
||||||
loadingComponent: Spinner
|
|
||||||
}),
|
}),
|
||||||
// '*': "Not Found"
|
// '*': "Not Found"
|
||||||
}} />
|
}}
|
||||||
|
/>
|
||||||
|
|||||||
@@ -2,79 +2,93 @@ export interface AniListCurrentUserWatchList {
|
|||||||
data: {
|
data: {
|
||||||
Page: {
|
Page: {
|
||||||
pageInfo: {
|
pageInfo: {
|
||||||
total: number
|
total: number;
|
||||||
perPage: number
|
perPage: number;
|
||||||
currentPage: number
|
currentPage: number;
|
||||||
lastPage: number
|
lastPage: number;
|
||||||
hasNextPage: boolean
|
hasNextPage: boolean;
|
||||||
},
|
};
|
||||||
mediaList: MediaList[]
|
mediaList: MediaList[];
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AniListGetSingleAnime {
|
export interface AniListGetSingleAnime {
|
||||||
data: {
|
data: {
|
||||||
MediaList: MediaList
|
MediaList: MediaList;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaList {
|
export interface MediaList {
|
||||||
id: number
|
id: number;
|
||||||
mediaId: number
|
mediaId: number;
|
||||||
userId: number
|
userId: number;
|
||||||
media: {
|
media: {
|
||||||
id: number
|
id: number;
|
||||||
idMal: number
|
idMal: number;
|
||||||
title: {
|
title: {
|
||||||
romaji: string
|
romaji: string;
|
||||||
english?: string
|
english?: string;
|
||||||
native: string
|
native: string;
|
||||||
}
|
};
|
||||||
description: string
|
description: string;
|
||||||
coverImage: {
|
coverImage: {
|
||||||
large: string
|
large: string;
|
||||||
}
|
};
|
||||||
season: string
|
season: string;
|
||||||
seasonYear: number
|
seasonYear: number;
|
||||||
status: string
|
status: string;
|
||||||
episodes?: number
|
episodes?: number;
|
||||||
nextAiringEpisode?: {
|
nextAiringEpisode?: {
|
||||||
airingAt: number
|
airingAt: number;
|
||||||
timeUntilAiring: number
|
timeUntilAiring: number;
|
||||||
episode: number
|
episode: number;
|
||||||
}
|
};
|
||||||
}
|
genres: string[];
|
||||||
status: string
|
tags: [
|
||||||
|
{
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
rank: number;
|
||||||
|
isMediaSpoiler: boolean;
|
||||||
|
isAdult: boolean;
|
||||||
|
},
|
||||||
|
];
|
||||||
|
isAdult: boolean;
|
||||||
|
};
|
||||||
|
status: string;
|
||||||
startedAt: {
|
startedAt: {
|
||||||
year: number
|
year: number;
|
||||||
month: number
|
month: number;
|
||||||
day: number
|
day: number;
|
||||||
}
|
};
|
||||||
completedAt: {
|
completedAt: {
|
||||||
year?: number
|
year?: number;
|
||||||
month?: number
|
month?: number;
|
||||||
day?: number
|
day?: number;
|
||||||
}
|
};
|
||||||
notes?: string
|
notes?: string;
|
||||||
progress: number
|
progress: number;
|
||||||
score: number
|
score: number;
|
||||||
repeat: number
|
repeat: number;
|
||||||
user: {
|
user: {
|
||||||
id: number
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
avatar: {
|
avatar: {
|
||||||
large: string
|
large: string;
|
||||||
medium: string
|
medium: string;
|
||||||
}
|
};
|
||||||
statistics: {
|
statistics: {
|
||||||
anime: {
|
anime: {
|
||||||
count: number
|
count: number;
|
||||||
statuses: [{
|
statuses: [
|
||||||
status: string
|
{
|
||||||
count: number
|
status: string;
|
||||||
}]
|
count: number;
|
||||||
}
|
},
|
||||||
}
|
];
|
||||||
}
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -6,12 +6,16 @@
|
|||||||
malLoggedIn,
|
malLoggedIn,
|
||||||
simklAnime,
|
simklAnime,
|
||||||
simklLoggedIn,
|
simklLoggedIn,
|
||||||
|
watchlistNeedsRefresh,
|
||||||
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
||||||
import { push } from "svelte-spa-router";
|
import { push } from "svelte-spa-router";
|
||||||
import { Button } from "flowbite-svelte";
|
import WebsiteLink from "./WebsiteLink.svelte";
|
||||||
import type { AniListGetSingleAnime } from "../anilist/types/AniListCurrentUserWatchListType";
|
import type { AniListGetSingleAnime } from "../anilist/types/AniListCurrentUserWatchListType";
|
||||||
import Rating from "./Rating.svelte";
|
import Rating from "./Rating.svelte";
|
||||||
import convertAniListDateToString from "../helperFunctions/convertAniListDateToString";
|
import {
|
||||||
|
convertAniListDateToString,
|
||||||
|
convertAniListDateToDate,
|
||||||
|
} from "../helperFunctions/convertAniListDateIn";
|
||||||
import AnimeTable from "./AnimeTable.svelte";
|
import AnimeTable from "./AnimeTable.svelte";
|
||||||
import type {
|
import type {
|
||||||
MALAnime,
|
MALAnime,
|
||||||
@@ -20,12 +24,9 @@
|
|||||||
} from "../mal/types/MALTypes";
|
} from "../mal/types/MALTypes";
|
||||||
import type { SimklAnime } from "../simkl/types/simklTypes";
|
import type { SimklAnime } from "../simkl/types/simklTypes";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import type {
|
import type { StatusOption, StatusOptions } from "../helperTypes/StatusTypes";
|
||||||
StatusOption,
|
|
||||||
StatusOptions,
|
|
||||||
} from "../helperTypes/StatusTypes";
|
|
||||||
import type { AniListUpdateVariables } from "../anilist/types/AniListTypes";
|
import type { AniListUpdateVariables } from "../anilist/types/AniListTypes";
|
||||||
import convertDateStringToAniList from "../helperFunctions/convertDateStringToAniList";
|
import { convertDateToAniList } from "../helperFunctions/convertDateToAniList";
|
||||||
import {
|
import {
|
||||||
AniListDeleteEntry,
|
AniListDeleteEntry,
|
||||||
AniListUpdateEntry,
|
AniListUpdateEntry,
|
||||||
@@ -38,6 +39,9 @@
|
|||||||
} from "../../wailsjs/go/main/App";
|
} from "../../wailsjs/go/main/App";
|
||||||
import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte";
|
import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte";
|
||||||
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
|
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
|
||||||
|
import Datepicker from "./Datepicker.svelte";
|
||||||
|
import { Badge, Tooltip } from "flowbite-svelte";
|
||||||
|
const re = /^([0-9]{4})-([0-9]{2})-([0-9]{2})/;
|
||||||
|
|
||||||
let isAniListLoggedIn: boolean;
|
let isAniListLoggedIn: boolean;
|
||||||
let isMalLoggedIn: boolean;
|
let isMalLoggedIn: boolean;
|
||||||
@@ -75,13 +79,12 @@
|
|||||||
{ id: 5, aniList: "REPEATING", mal: "rewatching", simkl: "watching" },
|
{ id: 5, aniList: "REPEATING", mal: "rewatching", simkl: "watching" },
|
||||||
];
|
];
|
||||||
let startingAnilistStatusOption: StatusOption = statusOptions.filter(
|
let startingAnilistStatusOption: StatusOption = statusOptions.filter(
|
||||||
(option) =>
|
(option) => currentAniListAnime.data.MediaList.status === option.aniList,
|
||||||
currentAniListAnime.data.MediaList.status === option.aniList,
|
|
||||||
)[0];
|
)[0];
|
||||||
const startedAtDate = convertAniListDateToString(
|
let startedAtDate: Date | null = convertAniListDateToDate(
|
||||||
currentAniListAnime.data.MediaList.startedAt,
|
currentAniListAnime.data.MediaList.startedAt,
|
||||||
);
|
);
|
||||||
const completedAtDate = convertAniListDateToString(
|
let completedAtDate: Date | null = convertAniListDateToDate(
|
||||||
currentAniListAnime.data.MediaList.completedAt,
|
currentAniListAnime.data.MediaList.completedAt,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -103,19 +106,30 @@
|
|||||||
notes: currentAniListAnime.data.MediaList.notes,
|
notes: currentAniListAnime.data.MediaList.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMalLoggedIn)
|
if (isMalLoggedIn) {
|
||||||
|
let startDate = "";
|
||||||
|
let finishDate = "";
|
||||||
|
if (currentMalAnime.my_list_status.start_date !== "") {
|
||||||
|
const startArray = re.exec(currentMalAnime.my_list_status.start_date);
|
||||||
|
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
|
||||||
|
}
|
||||||
|
if (currentMalAnime.my_list_status.finish_date !== "") {
|
||||||
|
const finishArray = re.exec(currentMalAnime.my_list_status.finish_date);
|
||||||
|
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
|
||||||
|
}
|
||||||
AddAnimeServiceToTable({
|
AddAnimeServiceToTable({
|
||||||
id: `m-${currentMalAnime.id}`,
|
id: `m-${currentMalAnime.id}`,
|
||||||
title: currentMalAnime.title,
|
title: currentMalAnime.title,
|
||||||
service: "MyAnimeList",
|
service: "MyAnimeList",
|
||||||
progress: currentMalAnime.my_list_status.num_episodes_watched,
|
progress: currentMalAnime.my_list_status.num_episodes_watched,
|
||||||
status: currentMalAnime.my_list_status.status,
|
status: currentMalAnime.my_list_status.status,
|
||||||
startedAt: currentMalAnime.my_list_status.start_date,
|
startedAt: startDate,
|
||||||
completedAt: currentMalAnime.my_list_status.finish_date,
|
completedAt: finishDate,
|
||||||
score: currentMalAnime.my_list_status.score,
|
score: currentMalAnime.my_list_status.score,
|
||||||
repeat: currentMalAnime.my_list_status.num_times_rewatched,
|
repeat: currentMalAnime.my_list_status.num_times_rewatched,
|
||||||
notes: currentMalAnime.my_list_status.comments,
|
notes: currentMalAnime.my_list_status.comments,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (isSimklLoggedIn && Object.keys(currentSimklAnime).length > 0)
|
if (isSimklLoggedIn && Object.keys(currentSimklAnime).length > 0)
|
||||||
AddAnimeServiceToTable({
|
AddAnimeServiceToTable({
|
||||||
@@ -137,8 +151,8 @@
|
|||||||
rating: number;
|
rating: number;
|
||||||
episodes: number;
|
episodes: number;
|
||||||
status: StatusOption;
|
status: StatusOption;
|
||||||
startedAt: string;
|
startedAt: Date | null;
|
||||||
completedAt: string;
|
completedAt: Date | null;
|
||||||
repeat: number;
|
repeat: number;
|
||||||
notes: string;
|
notes: string;
|
||||||
} = {
|
} = {
|
||||||
@@ -150,8 +164,8 @@
|
|||||||
mal: "",
|
mal: "",
|
||||||
simkl: "",
|
simkl: "",
|
||||||
},
|
},
|
||||||
startedAt: "",
|
startedAt: null,
|
||||||
completedAt: "",
|
completedAt: null,
|
||||||
repeat: 0,
|
repeat: 0,
|
||||||
notes: "",
|
notes: "",
|
||||||
};
|
};
|
||||||
@@ -177,6 +191,7 @@
|
|||||||
submitData[key] = value;
|
submitData[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
if (
|
if (
|
||||||
isAniListLoggedIn &&
|
isAniListLoggedIn &&
|
||||||
currentAniListAnime.data.MediaList.mediaId !== 0
|
currentAniListAnime.data.MediaList.mediaId !== 0
|
||||||
@@ -188,14 +203,14 @@
|
|||||||
score: submitData.rating,
|
score: submitData.rating,
|
||||||
repeat: submitData.repeat,
|
repeat: submitData.repeat,
|
||||||
notes: submitData.notes,
|
notes: submitData.notes,
|
||||||
startedAt: convertDateStringToAniList(submitData.startedAt),
|
startedAt: convertDateToAniList(startedAtDate),
|
||||||
completedAt: convertDateStringToAniList(submitData.completedAt),
|
completedAt: convertDateToAniList(completedAtDate),
|
||||||
};
|
};
|
||||||
await AniListUpdateEntry(body).then(
|
await AniListUpdateEntry(body).then((value: AniListGetSingleAnime) => {
|
||||||
(value: AniListGetSingleAnime) => {
|
value.data.MediaList.media.tags =
|
||||||
// in future when you inevitably add tags to typescript, until Anilist fixes the api bug
|
currentAniListAnime.data.MediaList.media.tags;
|
||||||
// where tags break the SaveMediaListEntry return, you'll want to use this delete line
|
value.data.MediaList.media.genres =
|
||||||
// delete value.data.MediaList.media.tags
|
currentAniListAnime.data.MediaList.media.genres;
|
||||||
aniListAnime.update((newValue) => {
|
aniListAnime.update((newValue) => {
|
||||||
newValue = value;
|
newValue = value;
|
||||||
return newValue;
|
return newValue;
|
||||||
@@ -216,8 +231,7 @@
|
|||||||
repeat: currentAniListAnime.data.MediaList.repeat,
|
repeat: currentAniListAnime.data.MediaList.repeat,
|
||||||
notes: currentAniListAnime.data.MediaList.notes,
|
notes: currentAniListAnime.data.MediaList.notes,
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (malLoggedIn && currentMalAnime.id !== 0) {
|
if (malLoggedIn && currentMalAnime.id !== 0) {
|
||||||
@@ -234,8 +248,7 @@
|
|||||||
(malAnimeReturn: MalListStatus) => {
|
(malAnimeReturn: MalListStatus) => {
|
||||||
malAnime.update((value) => {
|
malAnime.update((value) => {
|
||||||
value.my_list_status.status = malAnimeReturn.status;
|
value.my_list_status.status = malAnimeReturn.status;
|
||||||
value.my_list_status.is_rewatching =
|
value.my_list_status.is_rewatching = malAnimeReturn.is_rewatching;
|
||||||
malAnimeReturn.is_rewatching;
|
|
||||||
value.my_list_status.score = malAnimeReturn.score;
|
value.my_list_status.score = malAnimeReturn.score;
|
||||||
value.my_list_status.num_episodes_watched =
|
value.my_list_status.num_episodes_watched =
|
||||||
malAnimeReturn.num_episodes_watched;
|
malAnimeReturn.num_episodes_watched;
|
||||||
@@ -244,18 +257,30 @@
|
|||||||
value.my_list_status.comments = malAnimeReturn.comments;
|
value.my_list_status.comments = malAnimeReturn.comments;
|
||||||
return value;
|
return value;
|
||||||
});
|
});
|
||||||
|
let startDate = "";
|
||||||
|
let finishDate = "";
|
||||||
|
if (currentMalAnime.my_list_status.start_date !== "") {
|
||||||
|
const startArray = re.exec(
|
||||||
|
currentMalAnime.my_list_status.start_date,
|
||||||
|
);
|
||||||
|
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
|
||||||
|
}
|
||||||
|
if (currentMalAnime.my_list_status.finish_date !== "") {
|
||||||
|
const finishArray = re.exec(
|
||||||
|
currentMalAnime.my_list_status.finish_date,
|
||||||
|
);
|
||||||
|
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
|
||||||
|
}
|
||||||
AddAnimeServiceToTable({
|
AddAnimeServiceToTable({
|
||||||
id: `m-${currentMalAnime.id}`,
|
id: `m-${currentMalAnime.id}`,
|
||||||
title: currentMalAnime.title,
|
title: currentMalAnime.title,
|
||||||
service: "MyAnimeList",
|
service: "MyAnimeList",
|
||||||
progress:
|
progress: currentMalAnime.my_list_status.num_episodes_watched,
|
||||||
currentMalAnime.my_list_status.num_episodes_watched,
|
|
||||||
status: currentMalAnime.my_list_status.status,
|
status: currentMalAnime.my_list_status.status,
|
||||||
startedAt: currentMalAnime.my_list_status.start_date,
|
startedAt: startDate,
|
||||||
completedAt: currentMalAnime.my_list_status.finish_date,
|
completedAt: finishDate,
|
||||||
score: currentMalAnime.my_list_status.score,
|
score: currentMalAnime.my_list_status.score,
|
||||||
repeat: currentMalAnime.my_list_status
|
repeat: currentMalAnime.my_list_status.num_times_rewatched,
|
||||||
.num_times_rewatched,
|
|
||||||
notes: currentMalAnime.my_list_status.comments,
|
notes: currentMalAnime.my_list_status.comments,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -263,13 +288,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) {
|
if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) {
|
||||||
if (
|
if (currentSimklAnime.watched_episodes_count !== submitData.episodes) {
|
||||||
currentSimklAnime.watched_episodes_count !== submitData.episodes
|
await SimklSyncEpisodes(currentSimklAnime, submitData.episodes).then(
|
||||||
) {
|
(value: SimklAnime) => {
|
||||||
await SimklSyncEpisodes(
|
|
||||||
currentSimklAnime,
|
|
||||||
submitData.episodes,
|
|
||||||
).then((value: SimklAnime) => {
|
|
||||||
AddAnimeServiceToTable({
|
AddAnimeServiceToTable({
|
||||||
id: `s-${value.show.ids.simkl}`,
|
id: `s-${value.show.ids.simkl}`,
|
||||||
title: value.show.title,
|
title: value.show.title,
|
||||||
@@ -286,14 +307,13 @@
|
|||||||
newValue = value;
|
newValue = value;
|
||||||
return newValue;
|
return newValue;
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSimklAnime.user_rating !== submitData.rating) {
|
if (currentSimklAnime.user_rating !== submitData.rating) {
|
||||||
await SimklSyncRating(
|
await SimklSyncRating(currentSimklAnime, submitData.rating).then(
|
||||||
currentSimklAnime,
|
(value) => {
|
||||||
submitData.rating,
|
|
||||||
).then((value) => {
|
|
||||||
AddAnimeServiceToTable({
|
AddAnimeServiceToTable({
|
||||||
id: `s-${value.show.ids.simkl}`,
|
id: `s-${value.show.ids.simkl}`,
|
||||||
title: value.show.title,
|
title: value.show.title,
|
||||||
@@ -310,7 +330,8 @@
|
|||||||
newValue = value;
|
newValue = value;
|
||||||
return newValue;
|
return newValue;
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentSimklAnime.status !== submitData.status.simkl) {
|
if (currentSimklAnime.status !== submitData.status.simkl) {
|
||||||
@@ -337,14 +358,19 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error submitting changes:", error);
|
||||||
|
} finally {
|
||||||
submitting.set(false);
|
submitting.set(false);
|
||||||
submitSuccess.set(true);
|
submitSuccess.set(true);
|
||||||
|
watchlistNeedsRefresh.set(true);
|
||||||
setTimeout(() => submitSuccess.set(false), 2000);
|
setTimeout(() => submitSuccess.set(false), 2000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteEntries = async () => {
|
const deleteEntries = async () => {
|
||||||
submitting.set(true);
|
submitting.set(true);
|
||||||
|
try {
|
||||||
if (
|
if (
|
||||||
isAniListLoggedIn &&
|
isAniListLoggedIn &&
|
||||||
currentAniListAnime.data.MediaList.mediaId !== 0
|
currentAniListAnime.data.MediaList.mediaId !== 0
|
||||||
@@ -393,10 +419,28 @@
|
|||||||
notes: "",
|
notes: "",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting entries:", error);
|
||||||
|
} finally {
|
||||||
submitting.set(false);
|
submitting.set(false);
|
||||||
submitSuccess.set(true);
|
submitSuccess.set(true);
|
||||||
|
watchlistNeedsRefresh.set(true);
|
||||||
setTimeout(() => submitSuccess.set(false), 2000);
|
setTimeout(() => submitSuccess.set(false), 2000);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let max = 999;
|
||||||
|
|
||||||
|
if (currentAniListAnime.data.MediaList.media.episodes !== 0) {
|
||||||
|
max = currentAniListAnime.data.MediaList.media.episodes;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
currentAniListAnime.data.MediaList.media.episodes === 0 &&
|
||||||
|
currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode !== 0
|
||||||
|
) {
|
||||||
|
max =
|
||||||
|
currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode - 1;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<form on:submit|preventDefault={handleSubmit} class="container pt-3 pb-10">
|
<form on:submit|preventDefault={handleSubmit} class="container pt-3 pb-10">
|
||||||
@@ -423,40 +467,139 @@
|
|||||||
class="text-left block mb-2 text-sm font-medium text-white"
|
class="text-left block mb-2 text-sm font-medium text-white"
|
||||||
>Episode Progress</label
|
>Episode Progress</label
|
||||||
>
|
>
|
||||||
|
<div class="relative flex items-center max-w-[8rem]">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="decrement-button"
|
||||||
|
data-input-counter-decrement="quantity-input"
|
||||||
|
on:click={() => {
|
||||||
|
currentAniListAnime.data.MediaList.progress -= 1;
|
||||||
|
if (
|
||||||
|
currentAniListAnime.data.MediaList.progress <
|
||||||
|
currentAniListAnime.data.MediaList.media.episodes
|
||||||
|
) {
|
||||||
|
startingAnilistStatusOption = statusOptions[0];
|
||||||
|
if (currentAniListAnime.data.MediaList.repeat === 0)
|
||||||
|
completedAtDate = null;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={currentAniListAnime.data.MediaList.progress <= 0}
|
||||||
|
class={currentAniListAnime.data.MediaList.progress <= 0
|
||||||
|
? "border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
|
||||||
|
: "bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 18 2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M1 1h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
name="episodes"
|
name="episodes"
|
||||||
min="0"
|
min="0"
|
||||||
max={currentAniListAnime.data.MediaList.media.episodes}
|
{max}
|
||||||
id="episodes"
|
id="episodes"
|
||||||
class="border {currentAniListAnime.data.MediaList
|
class="border border-x-0 p-2.5 h-11 text-center text-sm block w-full placeholder-gray-400 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none
|
||||||
.progress < 0 ||
|
{currentAniListAnime.data.MediaList.progress < 0 ||
|
||||||
(currentAniListAnime.data.MediaList.media.episodes >
|
(currentAniListAnime.data.MediaList.media.episodes > 0 &&
|
||||||
0 &&
|
|
||||||
currentAniListAnime.data.MediaList.progress >
|
currentAniListAnime.data.MediaList.progress >
|
||||||
currentAniListAnime.data.MediaList.media
|
currentAniListAnime.data.MediaList.media.episodes) ||
|
||||||
.episodes)
|
(currentAniListAnime.data.MediaList.media.nextAiringEpisode
|
||||||
|
.episode > 0 &&
|
||||||
|
currentAniListAnime.data.MediaList.progress >
|
||||||
|
currentAniListAnime.data.MediaList.media.nextAiringEpisode
|
||||||
|
.episode -
|
||||||
|
1)
|
||||||
? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500'
|
? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500'
|
||||||
: 'border-gray-500 text-white focus:ring-blue-500 focus:border-blue-500'} text-sm rounded-lg block w-24 p-2.5 bg-gray-600 placeholder-gray-400"
|
: 'bg-gray-700 hover:bg-gray-600 border-gray-600 text-white focus:ring-blue-500 focus:border-blue-500'} w-24"
|
||||||
bind:value={currentAniListAnime.data.MediaList.progress}
|
bind:value={currentAniListAnime.data.MediaList.progress}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id="increment-button"
|
||||||
|
data-input-counter-increment="quantity-input"
|
||||||
|
on:click={() => {
|
||||||
|
currentAniListAnime.data.MediaList.progress += 1;
|
||||||
|
if (
|
||||||
|
currentAniListAnime.data.MediaList.media.episodes ===
|
||||||
|
currentAniListAnime.data.MediaList.progress
|
||||||
|
) {
|
||||||
|
startingAnilistStatusOption = statusOptions[2];
|
||||||
|
completedAtDate = new Date();
|
||||||
|
}
|
||||||
|
if (currentAniListAnime.data.MediaList.progress - 1 === 0) {
|
||||||
|
startingAnilistStatusOption = statusOptions[0];
|
||||||
|
if (startedAtDate === null) startedAtDate = new Date();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={(currentAniListAnime.data.MediaList.media.episodes >
|
||||||
|
0 &&
|
||||||
|
currentAniListAnime.data.MediaList.progress >=
|
||||||
|
currentAniListAnime.data.MediaList.media.episodes) ||
|
||||||
|
(currentAniListAnime.data.MediaList.media.nextAiringEpisode
|
||||||
|
.episode > 0 &&
|
||||||
|
currentAniListAnime.data.MediaList.progress >
|
||||||
|
currentAniListAnime.data.MediaList.media.nextAiringEpisode
|
||||||
|
.episode -
|
||||||
|
2)}
|
||||||
|
class={(currentAniListAnime.data.MediaList.media.episodes > 0 &&
|
||||||
|
currentAniListAnime.data.MediaList.progress >=
|
||||||
|
currentAniListAnime.data.MediaList.media.episodes) ||
|
||||||
|
(currentAniListAnime.data.MediaList.media.nextAiringEpisode
|
||||||
|
.episode > 0 &&
|
||||||
|
currentAniListAnime.data.MediaList.progress >
|
||||||
|
currentAniListAnime.data.MediaList.media.nextAiringEpisode
|
||||||
|
.episode -
|
||||||
|
2)
|
||||||
|
? "border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
|
||||||
|
: "bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 1v16M1 9h16"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
/ {currentAniListAnime.data.MediaList.media
|
/ {currentAniListAnime.data.MediaList.media.nextAiringEpisode
|
||||||
.nextAiringEpisode.episode !== 0
|
.episode !== 0
|
||||||
? currentAniListAnime.data.MediaList.media
|
? currentAniListAnime.data.MediaList.media.nextAiringEpisode
|
||||||
.nextAiringEpisode.episode - 1
|
.episode - 1
|
||||||
: currentAniListAnime.data.MediaList.media.episodes}
|
: currentAniListAnime.data.MediaList.media.episodes}
|
||||||
</div>
|
</div>
|
||||||
|
{#if currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode !== 0}
|
||||||
<div>
|
<div>
|
||||||
of {currentAniListAnime.data.MediaList.media.episodes}
|
of {currentAniListAnime.data.MediaList.media.episodes}
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
for="status"
|
for="status"
|
||||||
class="text-left block mb-2 text-sm font-medium text-gray-900 dark:text-white"
|
class="text-left block mb-2 text-sm font-medium text-white"
|
||||||
>Status</label
|
>Status</label
|
||||||
>
|
>
|
||||||
<select
|
<select
|
||||||
@@ -482,33 +625,16 @@
|
|||||||
class="text-left block mb-2 text-sm font-medium text-white"
|
class="text-left block mb-2 text-sm font-medium text-white"
|
||||||
>Date Started</label
|
>Date Started</label
|
||||||
>
|
>
|
||||||
<div class="relative max-w-sm">
|
<Datepicker
|
||||||
<div
|
bind:value={startedAtDate}
|
||||||
class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none"
|
color="slate"
|
||||||
>
|
dateFormat={{
|
||||||
<svg
|
year: "numeric",
|
||||||
class="w-4 h-4 text-gray-400"
|
month: "2-digit",
|
||||||
aria-hidden="true"
|
day: "2-digit",
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
}}
|
||||||
fill="currentColor"
|
showActionButtons
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="startedAt"
|
|
||||||
type="date"
|
|
||||||
name="startedAt"
|
|
||||||
class="border text-sm rounded-lg
|
|
||||||
focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5 bg-gray-700 border-gray-600
|
|
||||||
placeholder-gray-400 text-white"
|
|
||||||
value={startedAtDate}
|
|
||||||
placeholder="Date Started"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
@@ -516,33 +642,16 @@
|
|||||||
class="text-left block mb-2 text-sm font-medium text-white"
|
class="text-left block mb-2 text-sm font-medium text-white"
|
||||||
>Date Completed</label
|
>Date Completed</label
|
||||||
>
|
>
|
||||||
<div class="relative max-w-sm">
|
<Datepicker
|
||||||
<div
|
bind:value={completedAtDate}
|
||||||
class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none"
|
color="slate"
|
||||||
>
|
dateFormat={{
|
||||||
<svg
|
year: "numeric",
|
||||||
class="w-4 h-4 text-gray-400"
|
month: "2-digit",
|
||||||
aria-hidden="true"
|
day: "2-digit",
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
}}
|
||||||
fill="currentColor"
|
showActionButtons
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
id="completedAt"
|
|
||||||
type="date"
|
|
||||||
name="completedAt"
|
|
||||||
class="border text-sm rounded-lg
|
|
||||||
block w-full ps-10 p-2.5 bg-gray-700 border-gray-600
|
|
||||||
placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
value={completedAtDate}
|
|
||||||
placeholder="Date Completed"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
@@ -555,8 +664,7 @@
|
|||||||
name="repeat"
|
name="repeat"
|
||||||
min="0"
|
min="0"
|
||||||
id="repeat"
|
id="repeat"
|
||||||
class="border {currentAniListAnime.data.MediaList
|
class="border {currentAniListAnime.data.MediaList.repeat < 0
|
||||||
.repeat < 0
|
|
||||||
? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500'
|
? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500'
|
||||||
: 'border-gray-500 text-white focus:ring-blue-500 focus:border-blue-500'} text-sm rounded-lg block w-24 p-2.5 bg-gray-600 placeholder-gray-400 text-white"
|
: 'border-gray-500 text-white focus:ring-blue-500 focus:border-blue-500'} text-sm rounded-lg block w-24 p-2.5 bg-gray-600 placeholder-gray-400 text-white"
|
||||||
bind:value={currentAniListAnime.data.MediaList.repeat}
|
bind:value={currentAniListAnime.data.MediaList.repeat}
|
||||||
@@ -585,63 +693,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="external-data">
|
<div class="flex mb-4 rounded-lg shadow max-w-4-4 bg-gray-800">
|
||||||
<div
|
|
||||||
id="anilist-data"
|
|
||||||
class="flex flex-col md:flex-row md:pl-10 md:pr-10 pt-5 pb-5 justify-center md:gap-x-16 lg:gap-x-36 group"
|
|
||||||
>
|
|
||||||
<h2 class="text-left mb-1 text-base font-semibold text-white">
|
|
||||||
AniList
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnimeTable />
|
|
||||||
|
|
||||||
<div class="flex rounded-lg shadow max-w-4-4 bg-gray-800">
|
|
||||||
<div
|
|
||||||
class="w-full mx-auto max-w-screen-xl p-4 md:flex md:items-center md:justify-start"
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
disabled={isSubmitting}
|
|
||||||
id="delete-button"
|
|
||||||
class="text-white bg-red-700 {$submitSuccess
|
|
||||||
? 'bg-green-600 dark:bg-green-600 hover:bg-green-700 dark:hover:bg-green-700 focus:ring-4 focus:ring-green-800 dark:focus:ring-green-800'
|
|
||||||
: 'bg-red-600 dark:bg-red-600 hover:bg-red-700 dark:hover:bg-red-700 focus:ring-4 focus:ring-red-800 dark:focus:ring-red-800'} font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none"
|
|
||||||
on:click={deleteEntries}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
id="submit-loader"
|
|
||||||
aria-hidden="true"
|
|
||||||
role="status"
|
|
||||||
class="{isSubmitting
|
|
||||||
? 'inline'
|
|
||||||
: 'hidden'} w-4 h-4 me-3 text-white animate-spin"
|
|
||||||
viewBox="0 0 100 101"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
|
||||||
fill="#E5E7EB"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Delete Entries
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="w-full mx-auto max-w-screen-xl p-4 md:flex md:items-center md:justify-end"
|
class="w-full mx-auto max-w-screen-xl p-4 md:flex md:items-center md:justify-end"
|
||||||
>
|
>
|
||||||
<Button
|
<button
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
id="sync-button"
|
id="sync-button"
|
||||||
class="text-white {$submitSuccess
|
class="text-white {$submitSuccess
|
||||||
? 'bg-green-600 dark:bg-green-600 hover:bg-green-700 dark:hover:bg-green-700 focus:ring-4 focus:ring-green-800 dark:focus:ring-green-800'
|
? 'bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-800'
|
||||||
: 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus:ring-4 focus:ring-blue-800 dark:focus:ring-blue-800'} font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none"
|
: 'bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-800'} font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -665,23 +726,145 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
Sync Changes
|
Sync Changes
|
||||||
</Button>
|
</button>
|
||||||
<Button
|
<button
|
||||||
|
type="button"
|
||||||
class="text-white bg-gray-800 border border-gray-600 focus:outline-none hover:bg-gray-700 focus:ring-4
|
class="text-white bg-gray-800 border border-gray-600 focus:outline-none hover:bg-gray-700 focus:ring-4
|
||||||
focus:ring-gray-700 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white
|
focus:ring-gray-700 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2
|
||||||
dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
|
hover:border-gray-600"
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
await CheckIfAniListLoggedInAndLoadWatchList();
|
await CheckIfAniListLoggedInAndLoadWatchList();
|
||||||
return push("/");
|
return push("/");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Go Home
|
Go Home
|
||||||
</Button>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AnimeTable />
|
||||||
|
|
||||||
|
<div class="flex rounded-lg shadow max-w-4-4 bg-gray-800">
|
||||||
|
<div
|
||||||
|
class="w-full mx-auto max-w-screen-xl p-4 md:flex md:items-center md:justify-start"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
disabled={isSubmitting}
|
||||||
|
id="delete-button"
|
||||||
|
class="text-white {$submitSuccess
|
||||||
|
? 'bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-800'
|
||||||
|
: 'bg-red-600 hover:bg-red-700 focus:ring-4 focus:ring-red-800'} font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none"
|
||||||
|
on:click={deleteEntries}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
id="submit-loader"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="status"
|
||||||
|
class="{isSubmitting
|
||||||
|
? 'inline'
|
||||||
|
: 'hidden'} w-4 h-4 me-3 text-white animate-spin"
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="#E5E7EB"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Delete Entries
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="w-full mx-auto max-w-screen-xl p-4 md:flex md:items-center md:justify-end"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
disabled={isSubmitting}
|
||||||
|
id="sync-button"
|
||||||
|
class="text-white {$submitSuccess
|
||||||
|
? 'bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-800'
|
||||||
|
: 'bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-800'} font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none"
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
id="submit-loader"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="status"
|
||||||
|
class="{isSubmitting
|
||||||
|
? 'inline'
|
||||||
|
: 'hidden'} w-4 h-4 me-3 text-white animate-spin"
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="#E5E7EB"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
Sync Changes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="text-white bg-gray-800 border border-gray-600 focus:outline-none hover:bg-gray-700 focus:ring-4
|
||||||
|
focus:ring-gray-700 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2
|
||||||
|
hover:border-gray-600"
|
||||||
|
on:click={async () => {
|
||||||
|
await CheckIfAniListLoggedInAndLoadWatchList();
|
||||||
|
return push("/");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Go Home
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex m-5">
|
||||||
<div>
|
<div>
|
||||||
|
<h3 class="text-2xl">Genres</h3>
|
||||||
|
{#each currentAniListAnime.data.MediaList.media.genres || [] as genre}
|
||||||
|
<div>
|
||||||
|
<Badge large border color="blue" class="m-1 w-52">
|
||||||
|
<div>
|
||||||
|
<WebsiteLink
|
||||||
|
id={genre}
|
||||||
|
url="https://anilist.co/search/anime/{genre}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
|
<Tooltip>{genre}</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<h3 class="text-2xl">Tags</h3>
|
||||||
|
<div class="mt-2">
|
||||||
|
{#each currentAniListAnime.data.MediaList.media.tags as tag}
|
||||||
|
<div>
|
||||||
|
<Badge large border color="blue" class="m-1 w-52">
|
||||||
|
<div>
|
||||||
|
<WebsiteLink
|
||||||
|
id={tag.name}
|
||||||
|
url="https://anilist.co/search/anime/?genres={tag.name}"
|
||||||
|
/>
|
||||||
|
<span class="text-xs">({tag.rank}%)</span>
|
||||||
|
</div>
|
||||||
|
</Badge>
|
||||||
|
<Tooltip>{tag.description}</Tooltip>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="ml-5">
|
||||||
<h3 class="text-2xl">Summary</h3>
|
<h3 class="text-2xl">Summary</h3>
|
||||||
<p>{@html currentAniListAnime.data.MediaList.media.description}</p>
|
<p class="rounded border border-gray-700 p-2 mt-2">
|
||||||
|
{@html currentAniListAnime.data.MediaList.media.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
createTable,
|
createTable,
|
||||||
Render,
|
Render,
|
||||||
Subscribe,
|
Subscribe,
|
||||||
} from "svelte-headless-table"
|
} from "svelte-headless-table";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { addSortBy } from "svelte-headless-table/plugins"
|
import { addSortBy } from "svelte-headless-table/plugins";
|
||||||
import { tableItems } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"
|
import { tableItems } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
||||||
import WebsiteLink from "./WebsiteLink.svelte"
|
import WebsiteLink from "./WebsiteLink.svelte";
|
||||||
|
|
||||||
//when adding sort here is code { sort: addSortBy() }
|
//when adding sort here is code { sort: addSortBy() }
|
||||||
const table = createTable(tableItems, { sort: addSortBy() })
|
const table = createTable(tableItems, { sort: addSortBy() });
|
||||||
|
|
||||||
const columns = table.createColumns([
|
const columns = table.createColumns([
|
||||||
table.column({
|
table.column({
|
||||||
@@ -55,11 +55,11 @@
|
|||||||
header: "Notes",
|
header: "Notes",
|
||||||
accessor: "notes",
|
accessor: "notes",
|
||||||
}),
|
}),
|
||||||
])
|
]);
|
||||||
|
|
||||||
//add pluginStates when add sort back
|
//add pluginStates when add sort back
|
||||||
const { headerRows, rows, tableAttrs, tableBodyAttrs } =
|
const { headerRows, rows, tableAttrs, tableBodyAttrs } =
|
||||||
table.createViewModel(columns)
|
table.createViewModel(columns);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative overflow-x-auto rounded-lg mb-5">
|
<div class="relative overflow-x-auto rounded-lg mb-5">
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
import * as runtime from "../../wailsjs/runtime";
|
import * as runtime from "../../wailsjs/runtime";
|
||||||
import type { MyAnimeListUser } from "../mal/types/MALTypes";
|
import type { MyAnimeListUser } from "../mal/types/MALTypes";
|
||||||
import type { SimklUser } from "../simkl/types/simklTypes";
|
import type { SimklUser } from "../simkl/types/simklTypes";
|
||||||
|
import { ShowVersion } from "../../wailsjs/go/main/App";
|
||||||
|
|
||||||
let currentAniListUser: AniListUser;
|
let currentAniListUser: AniListUser;
|
||||||
let currentMALUser: MyAnimeListUser;
|
let currentMALUser: MyAnimeListUser;
|
||||||
@@ -27,8 +28,8 @@
|
|||||||
let isMALLoggedIn: boolean;
|
let isMALLoggedIn: boolean;
|
||||||
|
|
||||||
aniListUser.subscribe((value) => (currentAniListUser = value));
|
aniListUser.subscribe((value) => (currentAniListUser = value));
|
||||||
malUser.subscribe((value) => (currentMALUser = value))
|
malUser.subscribe((value) => (currentMALUser = value));
|
||||||
simklUser.subscribe(value => currentSimklUser = value)
|
simklUser.subscribe((value) => (currentSimklUser = value));
|
||||||
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
|
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
|
||||||
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
|
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
|
||||||
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
|
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
|
||||||
@@ -36,6 +37,23 @@
|
|||||||
function dropdownUser(): void {
|
function dropdownUser(): void {
|
||||||
let dropdown = document.querySelector("#userDropdown");
|
let dropdown = document.querySelector("#userDropdown");
|
||||||
dropdown.classList.toggle("hidden");
|
dropdown.classList.toggle("hidden");
|
||||||
|
|
||||||
|
if (!dropdown.classList.contains("hidden")) {
|
||||||
|
document.addEventListener("click", clickOutside);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickOutside(event: Event): void {
|
||||||
|
let dropdown = document.querySelector("#userDropdown");
|
||||||
|
let toggleBtn = document.querySelector("#userDropdownButton");
|
||||||
|
|
||||||
|
if (
|
||||||
|
!dropdown.contains(event.target as Node) &&
|
||||||
|
!toggleBtn.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
dropdown.classList.add("hidden");
|
||||||
|
document.removeEventListener("click", clickOutside);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -45,7 +63,9 @@
|
|||||||
<Avatar
|
<Avatar
|
||||||
src={currentAniListUser.data.Viewer.avatar.medium}
|
src={currentAniListUser.data.Viewer.avatar.medium}
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
dot={{ color: "green" }}
|
dot={isAniListLoggedIn && isMALLoggedIn && isSimklLoggedIn
|
||||||
|
? { color: "green" }
|
||||||
|
: { color: "yellow" }}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<Avatar class="cursor-pointer" dot={{ color: "red" }} />
|
<Avatar class="cursor-pointer" dot={{ color: "red" }} />
|
||||||
@@ -72,13 +92,19 @@
|
|||||||
on:click={logoutOfAniList}
|
on:click={logoutOfAniList}
|
||||||
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-green-800 hover:text-white"
|
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-green-800 hover:text-white"
|
||||||
>
|
>
|
||||||
<span class="maple-font text-lg text-green-200 mr-4">A</span>Logout {currentAniListUser.data.Viewer.name}
|
<span class="maple-font text-lg text-green-200 mr-4">A</span>Logout {currentAniListUser
|
||||||
|
.data.Viewer.name}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>
|
<li>
|
||||||
<button on:click={loginToAniList}
|
<button
|
||||||
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white">
|
on:click={() => {
|
||||||
|
dropdownUser();
|
||||||
|
loginToAniList();
|
||||||
|
}}
|
||||||
|
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white"
|
||||||
|
>
|
||||||
<span class="maple-font text-lg mr-4">A</span>Login to AniList
|
<span class="maple-font text-lg mr-4">A</span>Login to AniList
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -94,8 +120,13 @@
|
|||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>
|
<li>
|
||||||
<button on:click={loginToMAL}
|
<button
|
||||||
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white">
|
on:click={() => {
|
||||||
|
dropdownUser();
|
||||||
|
loginToMAL();
|
||||||
|
}}
|
||||||
|
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white"
|
||||||
|
>
|
||||||
<span class="maple-font text-lg mr-4">M</span>Login to MyAnimeList
|
<span class="maple-font text-lg mr-4">M</span>Login to MyAnimeList
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -106,19 +137,34 @@
|
|||||||
on:click={logoutOfSimkl}
|
on:click={logoutOfSimkl}
|
||||||
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-indigo-800 hover:text-white"
|
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-indigo-800 hover:text-white"
|
||||||
>
|
>
|
||||||
<span class="maple-font text-lg text-indigo-200 mr-4">S</span>Logout {currentSimklUser.user.name}
|
<span class="maple-font text-lg text-indigo-200 mr-4">S</span>Logout {currentSimklUser
|
||||||
|
.user.name}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>
|
<li>
|
||||||
<button on:click={loginToSimkl}
|
<button
|
||||||
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white">
|
on:click={() => {
|
||||||
|
dropdownUser();
|
||||||
|
loginToSimkl();
|
||||||
|
}}
|
||||||
|
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white"
|
||||||
|
>
|
||||||
<span class="maple-font text-lg mr-4">S</span>Login to Simkl
|
<span class="maple-font text-lg mr-4">S</span>Login to Simkl
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
|
<button
|
||||||
|
on:click={() => {
|
||||||
|
dropdownUser();
|
||||||
|
ShowVersion();
|
||||||
|
}}
|
||||||
|
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
|
||||||
|
>
|
||||||
|
Version
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
on:click={() => runtime.Quit()}
|
on:click={() => runtime.Quit()}
|
||||||
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
|
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
|
||||||
@@ -128,3 +174,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
481
frontend/src/helperComponents/Datepicker.svelte
Normal file
481
frontend/src/helperComponents/Datepicker.svelte
Normal file
@@ -0,0 +1,481 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
|
import { fade } from "svelte/transition";
|
||||||
|
import { Button } from "flowbite-svelte";
|
||||||
|
|
||||||
|
export let value: Date | null = null;
|
||||||
|
export let defaultDate: Date | null = null;
|
||||||
|
export let range: boolean = false;
|
||||||
|
export let rangeFrom: Date | null = null;
|
||||||
|
export let rangeTo: Date | null = null;
|
||||||
|
export let locale: string = "default";
|
||||||
|
export let firstDayOfWeek: number = 0; // 0 = Monday, 6 = Sunday
|
||||||
|
export let dateFormat: Intl.DateTimeFormatOptions = {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
};
|
||||||
|
export let placeholder: string = "Select date";
|
||||||
|
export let disabled: boolean = false;
|
||||||
|
export let required: boolean = false;
|
||||||
|
export let inputClass: string = "";
|
||||||
|
export let color: Button["color"] = "primary";
|
||||||
|
export let inline: boolean = false;
|
||||||
|
export let autohide: boolean = true;
|
||||||
|
export let showActionButtons: boolean = false;
|
||||||
|
export let title: string = "";
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
let isOpen: boolean = inline;
|
||||||
|
let inputElement: HTMLInputElement;
|
||||||
|
let datepickerContainerElement: HTMLDivElement;
|
||||||
|
let currentMonth: Date = value || defaultDate || new Date();
|
||||||
|
let focusedDate: Date | null = null;
|
||||||
|
let calendarRef: HTMLDivElement;
|
||||||
|
|
||||||
|
$: daysInMonth = getDaysInMonth(currentMonth);
|
||||||
|
$: weekdays = getWeekdays();
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (!inline) {
|
||||||
|
document.addEventListener("click", handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("click", handleClickOutside);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Color handling functions
|
||||||
|
function getFocusRingClass(color: Button["color"]): string {
|
||||||
|
switch (color) {
|
||||||
|
case "primary":
|
||||||
|
return "focus:ring-2 focus:ring-primary-400";
|
||||||
|
case "blue":
|
||||||
|
return "focus:ring-2 focus:ring-blue-400";
|
||||||
|
case "red":
|
||||||
|
return "focus:ring-2 focus:ring-red-400";
|
||||||
|
case "green":
|
||||||
|
return "focus:ring-2 focus:ring-green-400";
|
||||||
|
case "yellow":
|
||||||
|
return "focus:ring-2 focus:ring-yellow-400";
|
||||||
|
case "purple":
|
||||||
|
return "focus:ring-2 focus:ring-purple-400";
|
||||||
|
case "slate":
|
||||||
|
return "focus:ring-2 focus:ring-slate-400";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRangeBackgroundClass(color: Button["color"]): string {
|
||||||
|
switch (color) {
|
||||||
|
case "primary":
|
||||||
|
return "bg-primary-900";
|
||||||
|
case "blue":
|
||||||
|
return "bg-blue-900";
|
||||||
|
case "red":
|
||||||
|
return "bg-red-900";
|
||||||
|
case "green":
|
||||||
|
return "bg-green-900";
|
||||||
|
case "yellow":
|
||||||
|
return "bg-yellow-900";
|
||||||
|
case "purple":
|
||||||
|
return "bg-purple-900";
|
||||||
|
case "slate":
|
||||||
|
return "bg-slate-900";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDaysInMonth(date: Date): Date[] {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = date.getMonth();
|
||||||
|
const firstDay = new Date(year, month, 0);
|
||||||
|
const lastDay = new Date(year, month + 1, 0);
|
||||||
|
const daysArray: Date[] = [];
|
||||||
|
|
||||||
|
// Add days from previous month to fill the first week
|
||||||
|
let start = firstDay.getDay() - firstDayOfWeek;
|
||||||
|
if (start < 0) start += 7;
|
||||||
|
for (let i = 0; i < start; i++) {
|
||||||
|
daysArray.unshift(new Date(year, month, -i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add days of the current month
|
||||||
|
for (let i = 1; i <= lastDay.getDate(); i++) {
|
||||||
|
daysArray.push(new Date(year, month, i));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add days from next month to fill the last week
|
||||||
|
const remainingDays = 7 - (daysArray.length % 7);
|
||||||
|
if (remainingDays < 7) {
|
||||||
|
for (let i = 1; i <= remainingDays; i++) {
|
||||||
|
daysArray.push(new Date(year, month + 1, i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return daysArray;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWeekdays(): string[] {
|
||||||
|
const weekdays = [];
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const day = new Date(2021, 5, i + firstDayOfWeek);
|
||||||
|
weekdays.push(day.toLocaleString(locale, { weekday: "short" }));
|
||||||
|
}
|
||||||
|
return weekdays;
|
||||||
|
}
|
||||||
|
|
||||||
|
function changeMonth(increment: number) {
|
||||||
|
currentMonth = new Date(
|
||||||
|
currentMonth.getFullYear(),
|
||||||
|
currentMonth.getMonth() + increment,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDaySelect(day: Date) {
|
||||||
|
if (range) {
|
||||||
|
if (!rangeFrom || (rangeFrom && rangeTo)) {
|
||||||
|
rangeFrom = day;
|
||||||
|
rangeTo = null;
|
||||||
|
} else if (day < rangeFrom) {
|
||||||
|
rangeTo = rangeFrom;
|
||||||
|
rangeFrom = day;
|
||||||
|
} else {
|
||||||
|
rangeTo = day;
|
||||||
|
}
|
||||||
|
dispatch("select", { from: rangeFrom, to: rangeTo });
|
||||||
|
} else {
|
||||||
|
value = day;
|
||||||
|
dispatch("select", value);
|
||||||
|
if (autohide && !inline) isOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInputChange() {
|
||||||
|
const date = new Date(inputElement.value);
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
handleDaySelect(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (
|
||||||
|
isOpen &&
|
||||||
|
datepickerContainerElement &&
|
||||||
|
!datepickerContainerElement.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
isOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(date: Date | null): string {
|
||||||
|
if (!date) return "";
|
||||||
|
return date.toLocaleDateString(locale, dateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSameDate(date1: Date | null, date2: Date | null): boolean {
|
||||||
|
if (!date1 || !date2) return false;
|
||||||
|
return date1.toDateString() === date2.toDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
$: isSelected = (day: Date): boolean => {
|
||||||
|
if (range) {
|
||||||
|
return isSameDate(day, rangeFrom) || isSameDate(day, rangeTo);
|
||||||
|
}
|
||||||
|
return isSameDate(day, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
function isInRange(day: Date): boolean {
|
||||||
|
if (!range || !rangeFrom || !rangeTo) return false;
|
||||||
|
return day > rangeFrom && day < rangeTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isToday(day: Date): boolean {
|
||||||
|
const today = new Date();
|
||||||
|
return day.toDateString() === today.toDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCalendarKeydown(event: KeyboardEvent) {
|
||||||
|
if (!isOpen) return;
|
||||||
|
|
||||||
|
if (!focusedDate) {
|
||||||
|
focusedDate = value || new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowLeft":
|
||||||
|
focusedDate = new Date(
|
||||||
|
focusedDate.getFullYear(),
|
||||||
|
focusedDate.getMonth(),
|
||||||
|
focusedDate.getDate() - 1,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "ArrowRight":
|
||||||
|
focusedDate = new Date(
|
||||||
|
focusedDate.getFullYear(),
|
||||||
|
focusedDate.getMonth(),
|
||||||
|
focusedDate.getDate() + 1,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
focusedDate = new Date(
|
||||||
|
focusedDate.getFullYear(),
|
||||||
|
focusedDate.getMonth(),
|
||||||
|
focusedDate.getDate() - 7,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
focusedDate = new Date(
|
||||||
|
focusedDate.getFullYear(),
|
||||||
|
focusedDate.getMonth(),
|
||||||
|
focusedDate.getDate() + 7,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "Enter":
|
||||||
|
handleDaySelect(focusedDate);
|
||||||
|
break;
|
||||||
|
case "Escape":
|
||||||
|
isOpen = false;
|
||||||
|
inputElement.focus();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
if (focusedDate.getMonth() !== currentMonth.getMonth()) {
|
||||||
|
currentMonth = new Date(
|
||||||
|
focusedDate.getFullYear(),
|
||||||
|
focusedDate.getMonth(),
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus the button for the focused date
|
||||||
|
setTimeout(() => {
|
||||||
|
const focusedButton = calendarRef.querySelector(
|
||||||
|
`button[aria-label="${focusedDate!.toLocaleDateString(locale, { weekday: "long", year: "numeric", month: "long", day: "numeric" })}"]`,
|
||||||
|
) as HTMLButtonElement | null;
|
||||||
|
focusedButton?.focus();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInputKeydown(event: KeyboardEvent) {
|
||||||
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
|
event.preventDefault();
|
||||||
|
isOpen = !isOpen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleToday() {
|
||||||
|
handleDaySelect(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClear() {
|
||||||
|
value = null;
|
||||||
|
rangeFrom = null;
|
||||||
|
rangeTo = null;
|
||||||
|
dispatch("clear");
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleApply() {
|
||||||
|
dispatch("apply", range ? { from: rangeFrom, to: rangeTo } : value);
|
||||||
|
if (!inline) isOpen = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
bind:this={datepickerContainerElement}
|
||||||
|
class="relative {inline ? 'inline-block' : ''}"
|
||||||
|
>
|
||||||
|
{#if !inline}
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
bind:this={inputElement}
|
||||||
|
type="text"
|
||||||
|
class="w-full px-4 py-3 text-sm border rounded-md focus:outline-none bg-gray-700 text-white border-gray-600 {getFocusRingClass(
|
||||||
|
color,
|
||||||
|
)} {inputClass}"
|
||||||
|
{placeholder}
|
||||||
|
value={range
|
||||||
|
? `${formatDate(rangeFrom)} - ${formatDate(rangeTo)}`
|
||||||
|
: formatDate(value)}
|
||||||
|
on:focus={() => (isOpen = true)}
|
||||||
|
on:input={handleInputChange}
|
||||||
|
on:keydown={handleInputKeydown}
|
||||||
|
{disabled}
|
||||||
|
{required}
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="absolute inset-y-0 right-0 flex items-center px-3 text-gray-400 focus:outline-none"
|
||||||
|
on:click={() => (isOpen = !isOpen)}
|
||||||
|
{disabled}
|
||||||
|
aria-label={isOpen ? "Close date picker" : "Open date picker"}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-4 h-4 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if isOpen || inline}
|
||||||
|
<div
|
||||||
|
bind:this={calendarRef}
|
||||||
|
id="datepicker-dropdown"
|
||||||
|
class="
|
||||||
|
{inline ? '' : 'absolute z-10 mt-1'}
|
||||||
|
bg-gray-800 rounded-md shadow-lg"
|
||||||
|
transition:fade={{ duration: 100 }}
|
||||||
|
role="dialog"
|
||||||
|
aria-label="Calendar"
|
||||||
|
>
|
||||||
|
<div class="p-4" role="application">
|
||||||
|
{#if title}
|
||||||
|
<h2 class="text-lg font-semibold mb-4 text-white">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
{/if}
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<Button
|
||||||
|
on:click={() => changeMonth(-1)}
|
||||||
|
{color}
|
||||||
|
size="sm"
|
||||||
|
aria-label="Previous month"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 rtl:rotate-180 text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 14 10"
|
||||||
|
><path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 5H1m0 0 4 4M1 5l4-4"
|
||||||
|
></path></svg
|
||||||
|
>
|
||||||
|
</Button>
|
||||||
|
<h3
|
||||||
|
class="text-lg font-semibold text-white"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{currentMonth.toLocaleString(locale, {
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
})}
|
||||||
|
</h3>
|
||||||
|
<Button
|
||||||
|
on:click={() => changeMonth(1)}
|
||||||
|
{color}
|
||||||
|
size="sm"
|
||||||
|
aria-label="Next month"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 rtl:rotate-180 text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 14 10"
|
||||||
|
><path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M1 5h12m0 0L9 1m4 4L9 9"
|
||||||
|
></path></svg
|
||||||
|
>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-7 gap-1" role="grid">
|
||||||
|
{#each weekdays as day}
|
||||||
|
<div
|
||||||
|
class="text-center text-sm font-medium text-gray-400"
|
||||||
|
role="columnheader"
|
||||||
|
>
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{#each daysInMonth as day}
|
||||||
|
<Button
|
||||||
|
color={isSelected(day) ? color : "alternative"}
|
||||||
|
size="sm"
|
||||||
|
class="w-full h-8 {day.getMonth() !==
|
||||||
|
currentMonth.getMonth()
|
||||||
|
? 'text-gray-600'
|
||||||
|
: ''} {isToday(day)
|
||||||
|
? 'font-bold'
|
||||||
|
: ''} {isInRange(day)
|
||||||
|
? getRangeBackgroundClass(color)
|
||||||
|
: ''}"
|
||||||
|
on:click={() => handleDaySelect(day)}
|
||||||
|
on:keydown={handleCalendarKeydown}
|
||||||
|
aria-label={day.toLocaleDateString(locale, {
|
||||||
|
weekday: "long",
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
day: "numeric",
|
||||||
|
})}
|
||||||
|
aria-selected={isSelected(day)}
|
||||||
|
role="gridcell"
|
||||||
|
>
|
||||||
|
{day.getDate()}
|
||||||
|
</Button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{#if showActionButtons}
|
||||||
|
<div class="mt-4 flex justify-between">
|
||||||
|
<Button on:click={handleToday} {color} size="sm"
|
||||||
|
>Today</Button
|
||||||
|
>
|
||||||
|
<Button on:click={handleClear} color="red" size="sm"
|
||||||
|
>Clear</Button
|
||||||
|
>
|
||||||
|
<Button on:click={handleApply} {color} size="sm"
|
||||||
|
>Apply</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
@component
|
||||||
|
[Go to docs](https://flowbite-svelte.com/)
|
||||||
|
## Props
|
||||||
|
@prop export let value: Date | null = null;
|
||||||
|
@prop export let defaultDate: Date | null = null;
|
||||||
|
@prop export let range: boolean = false;
|
||||||
|
@prop export let rangeFrom: Date | null = null;
|
||||||
|
@prop export let rangeTo: Date | null = null;
|
||||||
|
@prop export let locale: string = 'default';
|
||||||
|
@prop export let firstDayOfWeek: number = 0;
|
||||||
|
@prop export let dateFormat: Intl.DateTimeFormatOptions = { year: 'numeric', month: 'long', day: 'numeric' };
|
||||||
|
@prop export let placeholder: string = 'Select date';
|
||||||
|
@prop export let disabled: boolean = false;
|
||||||
|
@prop export let required: boolean = false;
|
||||||
|
@prop export let inputClass: string = '';
|
||||||
|
@prop export let color: Button['color'] = 'primary';
|
||||||
|
@prop export let inline: boolean = false;
|
||||||
|
@prop export let autohide: boolean = true;
|
||||||
|
@prop export let showActionButtons: boolean = false;
|
||||||
|
@prop export let title: string = '';
|
||||||
|
-->
|
||||||
73
frontend/src/helperComponents/ErrorModal.svelte
Normal file
73
frontend/src/helperComponents/ErrorModal.svelte
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {
|
||||||
|
apiError,
|
||||||
|
isApiDown,
|
||||||
|
clearApiError,
|
||||||
|
setApiError,
|
||||||
|
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
||||||
|
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
|
||||||
|
import { CheckIfMALLoggedInAndSetUser } from "../helperModules/CheckIfMyAnimeListLoggedIn.svelte";
|
||||||
|
import { CheckIfSimklLoggedInAndSetUser } from "../helperModules/CheckIsSimklLoggedIn.svelte";
|
||||||
|
import { Modal, Button } from "flowbite-svelte";
|
||||||
|
let showModal = false;
|
||||||
|
$: if ($apiError) {
|
||||||
|
showModal = true;
|
||||||
|
}
|
||||||
|
async function handleRetry() {
|
||||||
|
const service = $apiError?.service;
|
||||||
|
clearApiError();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (service === "anilist") {
|
||||||
|
await CheckIfAniListLoggedInAndLoadWatchList();
|
||||||
|
} else if (service === "mal") {
|
||||||
|
await CheckIfMALLoggedInAndSetUser();
|
||||||
|
} else if (service === "simkl") {
|
||||||
|
await CheckIfSimklLoggedInAndSetUser();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
setApiError(
|
||||||
|
service || "unknown",
|
||||||
|
`Retry failed: ${errorMsg}`,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleDismiss() {
|
||||||
|
clearApiError();
|
||||||
|
showModal = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if showModal && $apiError}
|
||||||
|
<Modal
|
||||||
|
open={showModal}
|
||||||
|
title="{$apiError.service.toUpperCase()} API Error"
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<p class="text-red-800 font-medium">{$apiError.message}</p>
|
||||||
|
{#if $apiError.statusCode}
|
||||||
|
<p class="text-red-600 text-sm mt-2">
|
||||||
|
Status: {$apiError.statusCode}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-600">
|
||||||
|
The application will remain open. You can retry the connection or
|
||||||
|
dismiss this message to continue with limited functionality.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div slot="footer" class="flex gap-3 justify-end">
|
||||||
|
{#if $apiError.canRetry}
|
||||||
|
<Button on:click={handleRetry} class="bg-blue-600 hover:bg-blue-700">
|
||||||
|
Retry Connection
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button on:click={handleDismiss} color="alternative">Dismiss</Button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
{/if}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Search from "./Search.svelte"
|
import Search from "./Search.svelte";
|
||||||
import {
|
import {
|
||||||
aniListLoggedIn,
|
aniListLoggedIn,
|
||||||
loginToAniList,
|
loginToAniList,
|
||||||
@@ -7,45 +7,71 @@
|
|||||||
loginToSimkl,
|
loginToSimkl,
|
||||||
malLoggedIn,
|
malLoggedIn,
|
||||||
simklLoggedIn,
|
simklLoggedIn,
|
||||||
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"
|
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
||||||
import AvatarMenu from "./AvatarMenu.svelte";
|
import AvatarMenu from "./AvatarMenu.svelte";
|
||||||
import logo from "../assets/images/AniTrackLogo.svg"
|
import logo from "../assets/images/AniTrackLogo.svg";
|
||||||
|
import { link } from "svelte-spa-router";
|
||||||
|
|
||||||
let isAniListLoggedIn: boolean
|
let isAniListLoggedIn: boolean;
|
||||||
let isSimklLoggedIn: boolean
|
let isSimklLoggedIn: boolean;
|
||||||
let isMALLoggedIn: boolean
|
let isMALLoggedIn: boolean;
|
||||||
|
|
||||||
aniListLoggedIn.subscribe((value) => isAniListLoggedIn = value)
|
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
|
||||||
simklLoggedIn.subscribe((value) => isSimklLoggedIn = value)
|
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
|
||||||
malLoggedIn.subscribe((value) => isMALLoggedIn = value)
|
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class="border-gray-200 bg-gray-900">
|
<nav class="border-gray-200 bg-gray-900">
|
||||||
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
<div
|
||||||
|
class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4"
|
||||||
|
>
|
||||||
<div class="flex items-center space-x-3 rtl:space-x-reverse">
|
<div class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||||
<a href="/"><img src={logo} class="h-8" alt="AniTrack Logo"/></a>
|
<a href="/" use:link
|
||||||
|
><img src={logo} class="h-8" alt="AniTrack Logo" /></a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center min-[950px]:order-2 space-x-3 min-[950px]:space-x-0 rtl:space-x-reverse">
|
<div
|
||||||
|
class="flex items-center min-[950px]:order-2 space-x-3 min-[950px]:space-x-0 rtl:space-x-reverse"
|
||||||
|
>
|
||||||
<div class="min-[950px]:block min-[950px]:mr-4">
|
<div class="min-[950px]:block min-[950px]:mr-4">
|
||||||
<Search />
|
<Search />
|
||||||
</div>
|
</div>
|
||||||
<AvatarMenu />
|
<AvatarMenu />
|
||||||
<button on:click={() => {
|
<button
|
||||||
let menu = document.querySelector("#navbar-user")
|
on:click={() => {
|
||||||
menu.classList.toggle("hidden")
|
let menu = document.querySelector("#navbar-user");
|
||||||
}} type="button"
|
menu.classList.toggle("hidden");
|
||||||
|
}}
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded-lg min-[950px]:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-700 focus:ring-gray-600"
|
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded-lg min-[950px]:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-700 focus:ring-gray-600"
|
||||||
aria-controls="navbar-user" aria-expanded="false">
|
aria-controls="navbar-user"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
<span class="sr-only">Open main menu</span>
|
<span class="sr-only">Open main menu</span>
|
||||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
|
<svg
|
||||||
viewBox="0 0 17 14">
|
class="w-5 h-5"
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
aria-hidden="true"
|
||||||
d="M1 1h15M1 7h15M1 13h15"/>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 17 14"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M1 1h15M1 7h15M1 13h15"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden items-center justify-between w-full pb-4 min-[950px]:pb-0 min-[950px]:flex min-[950px]:w-auto min-[950px]:order-1 border border-gray-700 min-[950px]:border-0 bg-gray-800 min-[950px]:bg-transparent rounded-lg" id="navbar-user">
|
<div
|
||||||
<ul class="flex flex-col font-medium pb-6 min-[950px]:p-0 mt-4 min-[950px]:space-x-8 rtl:space-x-reverse min-[950px]:flex-row min-[950px]:mt-0">
|
class="hidden items-center justify-between w-full pb-4 min-[950px]:pb-0 min-[950px]:flex min-[950px]:w-auto min-[950px]:order-1 border border-gray-700 min-[950px]:border-0 bg-gray-800 min-[950px]:bg-transparent rounded-lg"
|
||||||
|
id="navbar-user"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="flex flex-col font-medium pb-6 min-[950px]:p-0 mt-4 min-[950px]:space-x-8 rtl:space-x-reverse min-[950px]:flex-row min-[950px]:mt-0"
|
||||||
|
>
|
||||||
<li>
|
<li>
|
||||||
{#if !isAniListLoggedIn}
|
{#if !isAniListLoggedIn}
|
||||||
<button on:click={loginToAniList}>
|
<button on:click={loginToAniList}>
|
||||||
|
|||||||
@@ -1,46 +1,57 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import {
|
||||||
aniListLoggedIn,
|
aniListLoggedIn,
|
||||||
|
aniListSort,
|
||||||
aniListWatchlist,
|
aniListWatchlist,
|
||||||
animePerPage,
|
animePerPage,
|
||||||
watchListPage,
|
watchListPage,
|
||||||
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
||||||
|
|
||||||
import type {AniListCurrentUserWatchList} from "../anilist/types/AniListCurrentUserWatchListType"
|
import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType";
|
||||||
import { GetAniListUserWatchingList } from "../../wailsjs/go/main/App";
|
import { GetAniListUserWatchingList } from "../../wailsjs/go/main/App";
|
||||||
import {MediaListSort} from "../anilist/types/AniListTypes";
|
|
||||||
|
|
||||||
let aniListWatchListLoaded: AniListCurrentUserWatchList
|
let aniListWatchListLoaded: AniListCurrentUserWatchList;
|
||||||
let page: number
|
let page: number;
|
||||||
let perPage: number
|
let perPage: number;
|
||||||
|
let sort: string;
|
||||||
|
|
||||||
watchListPage.subscribe(value => page = value)
|
watchListPage.subscribe((value) => (page = value));
|
||||||
animePerPage.subscribe(value => perPage = value)
|
animePerPage.subscribe((value) => (perPage = value));
|
||||||
aniListWatchlist.subscribe((value) => aniListWatchListLoaded = value)
|
aniListWatchlist.subscribe((value) => (aniListWatchListLoaded = value));
|
||||||
|
aniListSort.subscribe((value) => (sort = value));
|
||||||
|
|
||||||
const perPageOptions = [10, 20, 50]
|
const perPageOptions = [10, 20, 50];
|
||||||
|
|
||||||
function ChangeWatchListPage(newPage: number) {
|
function ChangeWatchListPage(newPage: number) {
|
||||||
GetAniListUserWatchingList(newPage, perPage, MediaListSort.UpdatedTimeDesc).then((result) => {
|
GetAniListUserWatchingList(newPage, perPage, sort).then((result) => {
|
||||||
watchListPage.set(newPage)
|
watchListPage.set(newPage);
|
||||||
aniListWatchlist.set(result)
|
aniListWatchlist.set(result);
|
||||||
aniListLoggedIn.set(true)
|
aniListLoggedIn.set(true);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePage(e): void {
|
function changePage(
|
||||||
if ((e.key === "Enter" || e.key === "Tab") && Number(e.target.value) !== page) ChangeWatchListPage(Number(e.target.value))
|
e: KeyboardEvent & { currentTarget: HTMLInputElement },
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
(e.key === "Enter" || e.key === "Tab") &&
|
||||||
|
Number(e.currentTarget.value) !== page
|
||||||
|
)
|
||||||
|
ChangeWatchListPage(Number(e.currentTarget.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeCountPerPage(e): void {
|
function changeCountPerPage(
|
||||||
GetAniListUserWatchingList(1, Number(e.target.value), MediaListSort.UpdatedTimeDesc).then((result) => {
|
e: Event & { currentTarget: HTMLSelectElement },
|
||||||
animePerPage.set(Number(e.target.value))
|
): void {
|
||||||
watchListPage.set(1)
|
GetAniListUserWatchingList(1, Number(e.currentTarget.value), sort).then(
|
||||||
aniListWatchlist.set(result)
|
(result) => {
|
||||||
aniListLoggedIn.set(true)
|
animePerPage.set(Number(e.currentTarget.value));
|
||||||
})
|
watchListPage.set(1);
|
||||||
|
aniListWatchlist.set(result);
|
||||||
|
aniListLoggedIn.set(true);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
@@ -49,15 +60,19 @@
|
|||||||
<ul class="inline-flex -space-x-px text-base h-10">
|
<ul class="inline-flex -space-x-px text-base h-10">
|
||||||
{#if page === 1}
|
{#if page === 1}
|
||||||
<li>
|
<li>
|
||||||
<button disabled
|
<button
|
||||||
class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 cursor-default">
|
disabled
|
||||||
|
class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 cursor-default"
|
||||||
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>
|
<li>
|
||||||
<button on:click={() => ChangeWatchListPage(page-1)}
|
<button
|
||||||
class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white">
|
on:click={() => ChangeWatchListPage(page - 1)}
|
||||||
|
class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white"
|
||||||
|
>
|
||||||
Previous
|
Previous
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -65,27 +80,37 @@
|
|||||||
{#each { length: aniListWatchListLoaded.data.Page.pageInfo.lastPage } as _, i}
|
{#each { length: aniListWatchListLoaded.data.Page.pageInfo.lastPage } as _, i}
|
||||||
{#if i + 1 === page}
|
{#if i + 1 === page}
|
||||||
<li>
|
<li>
|
||||||
<button on:click={() => ChangeWatchListPage(i+1)}
|
<button
|
||||||
class="flex items-center justify-center px-4 h-10 leading-tight border bg-gray-100 border-gray-700 bg-gray-700 text-white">{i + 1}</button>
|
on:click={() => ChangeWatchListPage(i + 1)}
|
||||||
|
class="flex items-center justify-center px-4 h-10 leading-tight border hover:bg-gray-100 border-gray-700 bg-gray-700 text-white"
|
||||||
|
>{i + 1}</button
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>
|
<li>
|
||||||
<button on:click={() => ChangeWatchListPage(i+1)}
|
<button
|
||||||
class="flex items-center justify-center px-4 h-10 leading-tight border dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white">{i + 1}</button>
|
on:click={() => ChangeWatchListPage(i + 1)}
|
||||||
|
class="flex items-center justify-center px-4 h-10 leading-tight border dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white"
|
||||||
|
>{i + 1}</button
|
||||||
|
>
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
{#if page === aniListWatchListLoaded.data.Page.pageInfo.lastPage}
|
{#if page === aniListWatchListLoaded.data.Page.pageInfo.lastPage}
|
||||||
<li>
|
<li>
|
||||||
<button disabled
|
<button
|
||||||
class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 cursor-default">
|
disabled
|
||||||
|
class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 cursor-default"
|
||||||
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{:else}
|
{:else}
|
||||||
<li>
|
<li>
|
||||||
<button on:click={() => ChangeWatchListPage(page+1)}
|
<button
|
||||||
class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white">
|
on:click={() => ChangeWatchListPage(page + 1)}
|
||||||
|
class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white"
|
||||||
|
>
|
||||||
Next
|
Next
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
@@ -95,8 +120,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<div class="flex mt-5">
|
<div class="flex mt-5">
|
||||||
<div class="w-20 mx-auto">
|
<div class="w-20 mx-auto">
|
||||||
<select bind:value={perPage} on:change={(e) => changeCountPerPage(e)} id="countPerPage"
|
<select
|
||||||
class="border text-sm rounded-lg block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500">
|
bind:value={perPage}
|
||||||
|
on:change={(e) => changeCountPerPage(e)}
|
||||||
|
id="countPerPage"
|
||||||
|
class="border text-sm rounded-lg block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
>
|
||||||
{#each perPageOptions as option}
|
{#each perPageOptions as option}
|
||||||
<option value={option}>
|
<option value={option}>
|
||||||
{option}
|
{option}
|
||||||
@@ -108,35 +137,81 @@
|
|||||||
<div>
|
<div>
|
||||||
<div>Total Anime: {aniListWatchListLoaded.data.Page.pageInfo.total}</div>
|
<div>Total Anime: {aniListWatchListLoaded.data.Page.pageInfo.total}</div>
|
||||||
{#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12}
|
{#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12}
|
||||||
<div class="md:hidden">Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}</div>
|
<div class="md:hidden">
|
||||||
|
Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div>Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}</div>
|
<div>
|
||||||
|
Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="max-w-xs mx-auto">
|
<div class="max-w-xs mx-auto">
|
||||||
<div class="relative flex items-center max-w-[11rem]">
|
<div class="relative flex items-center max-w-[11rem]">
|
||||||
<button type="button" id="decrement-button" on:click={() => ChangeWatchListPage(page-1)}
|
<button
|
||||||
class="bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none">
|
type="button"
|
||||||
<svg class="w-3 h-3 text-white" aria-hidden="true"
|
id="decrement-button"
|
||||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 2">
|
on:click={() => ChangeWatchListPage(page - 1)}
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
class={page <= 1
|
||||||
d="M1 1h16"/>
|
? "border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
|
||||||
|
: "bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"}
|
||||||
|
disabled={page <= 1}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 18 2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M1 1h16"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<input type="number" min="1" max="{aniListWatchListLoaded.data.Page.pageInfo.lastPage}"
|
<input
|
||||||
on:keydown={changePage} id="page-counter"
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max={aniListWatchListLoaded.data.Page.pageInfo.lastPage}
|
||||||
|
on:keydown={changePage}
|
||||||
|
id="page-counter"
|
||||||
class="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none border-x-0 h-11 font-medium text-center text-sm block w-full pb-6 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
|
class="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none border-x-0 h-11 font-medium text-center text-sm block w-full pb-6 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
|
||||||
value={page} required/>
|
value={page}
|
||||||
<div class="absolute bottom-1 start-1/2 -translate-x-1/2 rtl:translate-x-1/2 flex items-center text-xs text-gray-400 space-x-1 rtl:space-x-reverse">
|
required
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute bottom-1 start-1/2 -translate-x-1/2 rtl:translate-x-1/2 flex items-center text-xs text-gray-400 space-x-1 rtl:space-x-reverse"
|
||||||
|
>
|
||||||
<span>Page #</span>
|
<span>Page #</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="increment-button" on:click={() => ChangeWatchListPage(page+1)}
|
<button
|
||||||
class="hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none">
|
type="button"
|
||||||
<svg class="w-3 h-3 text-white" aria-hidden="true"
|
id="increment-button"
|
||||||
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 18">
|
on:click={() => ChangeWatchListPage(page + 1)}
|
||||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
class={page >= aniListWatchListLoaded.data.Page.pageInfo.lastPage
|
||||||
d="M9 1v16M1 9h16"/>
|
? "border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
|
||||||
|
: "bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"}
|
||||||
|
disabled={page >= aniListWatchListLoaded.data.Page.pageInfo.lastPage}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="w-3 h-3 text-white"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 1v16M1 9h16"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
0
frontend/src/helperComponents/Sort.svelte
Normal file
0
frontend/src/helperComponents/Sort.svelte
Normal file
@@ -6,30 +6,50 @@
|
|||||||
loading,
|
loading,
|
||||||
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
||||||
import { push } from "svelte-spa-router";
|
import { push } from "svelte-spa-router";
|
||||||
import type {AniListCurrentUserWatchList} from "../anilist/types/AniListCurrentUserWatchListType"
|
import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType";
|
||||||
import { Rating } from "flowbite-svelte";
|
import { Rating } from "flowbite-svelte";
|
||||||
import loader from '../helperFunctions/loader'
|
import loader from "../helperFunctions/loader";
|
||||||
|
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
|
||||||
|
|
||||||
|
let isAniListLoggedIn: boolean;
|
||||||
|
let aniListWatchListLoaded: AniListCurrentUserWatchList;
|
||||||
|
|
||||||
let isAniListLoggedIn: boolean
|
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
|
||||||
let aniListWatchListLoaded: AniListCurrentUserWatchList
|
aniListWatchlist.subscribe((value) => (aniListWatchListLoaded = value));
|
||||||
|
|
||||||
aniListLoggedIn.subscribe((value) => isAniListLoggedIn = value)
|
|
||||||
aniListWatchlist.subscribe((value) => aniListWatchListLoaded = value)
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#if isAniListLoggedIn}
|
{#if isAniListLoggedIn}
|
||||||
<div class="mx-auto max-w-2xl p-4 sm:p-6 lg:max-w-7xl lg:px-8 relative items-center">
|
<div
|
||||||
<h1 class="text-left text-xl font-bold mb-4">Your AniList WatchList</h1>
|
class="mx-auto max-w-2xl p-4 sm:p-6 lg:max-w-7xl lg:px-8 relative items-center"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h1 class="text-left text-xl font-bold">Your AniList WatchList</h1>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="py-2 px-4 bg-gray-700 rounded-lg"
|
||||||
|
on:click={async () => {
|
||||||
|
loading.set(true);
|
||||||
|
await CheckIfAniListLoggedInAndLoadWatchList();
|
||||||
|
loading.set(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Refresh WatchList
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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">
|
<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 aniListWatchListLoaded.data.Page.mediaList as media}
|
{#each aniListWatchListLoaded.data.Page.mediaList as media}
|
||||||
<div use:loader={loading} class="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-lg xl:aspect-h-8 xl:aspect-w-7">
|
<div
|
||||||
|
use:loader={loading}
|
||||||
|
class="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-lg xl:aspect-h-8 xl:aspect-w-7"
|
||||||
|
>
|
||||||
<div class="flex flex-col items-center group">
|
<div class="flex flex-col items-center group">
|
||||||
<button on:click={() => {
|
<button
|
||||||
push(`#/anime/${media.media.id}`)
|
on:click={() => {
|
||||||
|
push(`#/anime/${media.media.id}`);
|
||||||
// loading.set(true)
|
// loading.set(true)
|
||||||
// GetAniListSingleItem(media.media.id, true).then(() => {
|
// GetAniListSingleItem(media.media.id, true).then(() => {
|
||||||
// loading.set(false)
|
// loading.set(false)
|
||||||
@@ -37,27 +57,38 @@
|
|||||||
// })
|
// })
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img class="rounded-lg" src={media.media.coverImage.large} alt={
|
<img
|
||||||
media.media.title.english === "" ?
|
class="rounded-lg w-[230px] h-[330px] object-cover"
|
||||||
media.media.title.romaji :
|
src={media.media.coverImage.large}
|
||||||
media.media.title.english
|
alt={media.media.title.english === ""
|
||||||
}/>
|
? media.media.title.romaji
|
||||||
|
: media.media.title.english}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
<Rating id="anime-rating" total={5} size={35} rating={media.score/2.0}/>
|
<Rating
|
||||||
<button class="mt-4 text-md font-semibold text-white-700"
|
id="anime-rating"
|
||||||
on:click={() => GetAnimeSingleItem(media.media.id, true)}>
|
total={5}
|
||||||
{
|
size={35}
|
||||||
media.media.title.english === "" ?
|
rating={media.score / 2.0}
|
||||||
media.media.title.romaji :
|
/>
|
||||||
media.media.title.english
|
<button
|
||||||
}
|
class="mt-4 text-md font-semibold text-white-700"
|
||||||
|
on:click={() => GetAnimeSingleItem(media.media.id, true)}
|
||||||
|
>
|
||||||
|
{media.media.title.english === ""
|
||||||
|
? media.media.title.romaji
|
||||||
|
: media.media.title.english}
|
||||||
</button>
|
</button>
|
||||||
<p class="mt-1 text-lg font-medium text-white-900">{media.progress}
|
<p class="mt-1 text-lg font-medium text-white-900">
|
||||||
/ {media.media.nextAiringEpisode.episode !== 0 ?
|
{media.progress}
|
||||||
media.media.nextAiringEpisode.episode - 1 : media.media.episodes}</p>
|
/ {media.media.nextAiringEpisode.episode !== 0
|
||||||
|
? media.media.nextAiringEpisode.episode - 1
|
||||||
|
: media.media.episodes}
|
||||||
|
</p>
|
||||||
{#if media.media.episodes > 0}
|
{#if media.media.episodes > 0}
|
||||||
<p class="mt-1 text-lg font-medium text-white-900">Total
|
<p class="mt-1 text-lg font-medium text-white-900">
|
||||||
Episodes: {media.media.episodes}</p>
|
Total Episodes: {media.media.episodes}
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {BrowserOpenURL} from "../../wailsjs/runtime"
|
import { BrowserOpenURL } from "../../wailsjs/runtime";
|
||||||
|
|
||||||
export let id: string
|
export let id: string;
|
||||||
let url = ""
|
export let url = "";
|
||||||
let isAniList = false
|
let isAniList = false;
|
||||||
let isMAL = false
|
let isMAL = false;
|
||||||
let isSimkl = false
|
let isSimkl = false;
|
||||||
let newId = id
|
let newId = id;
|
||||||
let re = /[ams]?-?(.*)/
|
let re = /[ams]?-?(.*)/;
|
||||||
if (id !== undefined && id.length > 0) {
|
if (id !== undefined && id.length > 0) {
|
||||||
isAniList = id.includes("a-")
|
isAniList = id.includes("a-");
|
||||||
isMAL = id.includes("m-")
|
isMAL = id.includes("m-");
|
||||||
isSimkl = id.includes("s-")
|
isSimkl = id.includes("s-");
|
||||||
newId = id.match(re)[1]
|
if (isAniList || isMAL || isSimkl) newId = id.match(re)[1];
|
||||||
|
else newId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isAniList) url = `https://anilist.co/anime/${newId}`;
|
||||||
if (isAniList) url = `https://anilist.co/anime/${newId}`
|
if (isMAL) url = `https://myanimelist.net/anime/${newId}`;
|
||||||
if (isMAL) url = `https://myanimelist.net/anime/${newId}`
|
if (isSimkl) url = `https://simkl.com/anime/${newId}`;
|
||||||
if (isSimkl) url = `https://simkl.com/anime/${newId}`
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if url.length > 0}
|
{#if url.length > 0}
|
||||||
<button class="underline underline-offset-2 px-4 py-1" on:click={() => BrowserOpenURL(url)}>{newId}</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="underline underline-offset-2 px-4 py-1"
|
||||||
|
on:click={() => BrowserOpenURL(url)}>{newId}</button
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
{id}
|
{id}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
37
frontend/src/helperFunctions/convertAniListDateIn.ts
Normal file
37
frontend/src/helperFunctions/convertAniListDateIn.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import moment from "moment";
|
||||||
|
|
||||||
|
const convertAniListDateToString = (date: {
|
||||||
|
year?: number;
|
||||||
|
month?: number;
|
||||||
|
day?: number;
|
||||||
|
}): string => {
|
||||||
|
if (
|
||||||
|
date.year === undefined ||
|
||||||
|
(date.year === 0 && date.month === undefined) ||
|
||||||
|
(date.month === 0 && date.day === undefined) ||
|
||||||
|
date.day === 0
|
||||||
|
) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const newISODate = new Date(date.year, date.month - 1, date.day);
|
||||||
|
const newMoment = moment(newISODate);
|
||||||
|
return newMoment.format("MM-DD-YYYY");
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertAniListDateToDate = (date: {
|
||||||
|
year?: number;
|
||||||
|
month?: number;
|
||||||
|
day?: number;
|
||||||
|
}): Date | null => {
|
||||||
|
if (
|
||||||
|
date.year === undefined ||
|
||||||
|
(date.year === 0 && date.month === undefined) ||
|
||||||
|
(date.month === 0 && date.day === undefined) ||
|
||||||
|
date.day === 0
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return new Date(date.year, date.month - 1, date.day);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { convertAniListDateToString, convertAniListDateToDate };
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import moment from "moment";
|
|
||||||
|
|
||||||
export default (date: {
|
|
||||||
year?: number,
|
|
||||||
month?: number,
|
|
||||||
day?: number,
|
|
||||||
}): string => {
|
|
||||||
if (date.year === undefined || date.year === 0
|
|
||||||
&& date.month === undefined || date.month === 0
|
|
||||||
&& date.day === undefined || date.day === 0
|
|
||||||
) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
const newISODate = new Date(date.year, date.month - 1, date.day)
|
|
||||||
const newMoment = moment(newISODate)
|
|
||||||
return newMoment.format('YYYY-MM-DD')
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
type Date = {
|
|
||||||
year: number,
|
|
||||||
month: number,
|
|
||||||
day: number,
|
|
||||||
}
|
|
||||||
|
|
||||||
export default (date: string): Date => {
|
|
||||||
if (date === "") {
|
|
||||||
return {
|
|
||||||
year: 0,
|
|
||||||
month: 0,
|
|
||||||
day: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const re = /^([0-9]{4})-([0-9]{2})-([0-9]{2})/
|
|
||||||
const newDate = re.exec(date)
|
|
||||||
return {
|
|
||||||
year: Number(newDate[1]),
|
|
||||||
month: Number(newDate[2]),
|
|
||||||
day: Number(newDate[3])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
39
frontend/src/helperFunctions/convertDateToAniList.ts
Normal file
39
frontend/src/helperFunctions/convertDateToAniList.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
type AnilistDate = {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
day: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDateStringToAniList = (date: string): AnilistDate => {
|
||||||
|
if (date === "") {
|
||||||
|
return {
|
||||||
|
year: 0,
|
||||||
|
month: 0,
|
||||||
|
day: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const re = /^([0-9]{4})-([0-9]{2})-([0-9]{2})/;
|
||||||
|
const newDate = re.exec(date);
|
||||||
|
return {
|
||||||
|
year: Number(newDate[1]),
|
||||||
|
month: Number(newDate[2]),
|
||||||
|
day: Number(newDate[3]),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertDateToAniList = (date: Date | null): AnilistDate => {
|
||||||
|
if (date === null) {
|
||||||
|
return {
|
||||||
|
year: 0,
|
||||||
|
month: 0,
|
||||||
|
day: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
year: Number(date.getFullYear()),
|
||||||
|
month: Number(date.getMonth()) + 1,
|
||||||
|
day: Number(date.getDate()),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { convertDateStringToAniList, convertDateToAniList };
|
||||||
@@ -1,34 +1,81 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import {CheckIfAniListLoggedIn, GetAniListLoggedInUser, GetAniListUserWatchingList} from "../../wailsjs/go/main/App";
|
import {
|
||||||
import {MediaListSort} from "../anilist/types/AniListTypes";
|
CheckIfAniListLoggedIn,
|
||||||
import { aniListUser, watchListPage, animePerPage, aniListPrimary, aniListLoggedIn, aniListWatchlist } from "./GlobalVariablesAndHelperFunctions.svelte"
|
GetAniListLoggedInUser,
|
||||||
|
GetAniListUserWatchingList,
|
||||||
|
} from "../../wailsjs/go/main/App";
|
||||||
|
import {
|
||||||
|
aniListUser,
|
||||||
|
watchListPage,
|
||||||
|
animePerPage,
|
||||||
|
aniListPrimary,
|
||||||
|
aniListLoggedIn,
|
||||||
|
aniListWatchlist,
|
||||||
|
aniListSort,
|
||||||
|
clearApiError,
|
||||||
|
setApiError,
|
||||||
|
} from "./GlobalVariablesAndHelperFunctions.svelte";
|
||||||
|
|
||||||
let isAniListPrimary: boolean
|
let isAniListPrimary: boolean;
|
||||||
let page: number
|
let page: number;
|
||||||
let perPage: number
|
let perPage: number;
|
||||||
|
let sort: string;
|
||||||
|
|
||||||
aniListPrimary.subscribe(value => isAniListPrimary = value)
|
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
|
||||||
watchListPage.subscribe(value => page = value)
|
watchListPage.subscribe((value) => (page = value));
|
||||||
animePerPage.subscribe(value => perPage = value)
|
animePerPage.subscribe((value) => (perPage = value));
|
||||||
|
aniListSort.subscribe((value) => (sort = value));
|
||||||
|
|
||||||
export const LoadAniListUser = async () => {
|
export const LoadAniListUser = async () => {
|
||||||
await GetAniListLoggedInUser().then(user => {
|
try {
|
||||||
aniListUser.set(user)
|
await GetAniListLoggedInUser().then((user) => {
|
||||||
})
|
aniListUser.set(user);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
setApiError(
|
||||||
|
"anilist",
|
||||||
|
`Failed to load user: ${errorMsg}`,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const LoadAniListWatchList = async () => {
|
export const LoadAniListWatchList = async () => {
|
||||||
await GetAniListUserWatchingList(page, perPage, MediaListSort.UpdatedTimeDesc).then((watchList) => {
|
try {
|
||||||
aniListWatchlist.set(watchList)
|
const watchList = await GetAniListUserWatchingList(page, perPage, sort);
|
||||||
})
|
aniListWatchlist.set(watchList);
|
||||||
|
clearApiError();
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
setApiError(
|
||||||
|
"anilist",
|
||||||
|
`Failed to load watch list: ${errorMsg}`,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
export const CheckIfAniListLoggedInAndLoadWatchList = async () => {
|
export const CheckIfAniListLoggedInAndLoadWatchList = async () => {
|
||||||
const loggedIn = await CheckIfAniListLoggedIn()
|
try {
|
||||||
|
const loggedIn = await CheckIfAniListLoggedIn();
|
||||||
if (loggedIn) {
|
if (loggedIn) {
|
||||||
await LoadAniListUser()
|
await LoadAniListUser();
|
||||||
if (isAniListPrimary) await LoadAniListWatchList()
|
if (isAniListPrimary) await LoadAniListWatchList();
|
||||||
}
|
}
|
||||||
aniListLoggedIn.set(loggedIn)
|
aniListLoggedIn.set(loggedIn);
|
||||||
|
} catch (err) {
|
||||||
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
||||||
|
setApiError(
|
||||||
|
"anilist",
|
||||||
|
`Authentication failed: ${errorMsg}`,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
aniListLoggedIn.set(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -10,153 +10,203 @@
|
|||||||
LogoutMyAnimeList,
|
LogoutMyAnimeList,
|
||||||
LogoutSimkl,
|
LogoutSimkl,
|
||||||
SimklGetUserWatchlist,
|
SimklGetUserWatchlist,
|
||||||
SimklSearch
|
SimklSearch,
|
||||||
} from "../../wailsjs/go/main/App";
|
} from "../../wailsjs/go/main/App";
|
||||||
import type {
|
import type {
|
||||||
AniListCurrentUserWatchList,
|
AniListCurrentUserWatchList,
|
||||||
AniListGetSingleAnime
|
AniListGetSingleAnime,
|
||||||
} from "../anilist/types/AniListCurrentUserWatchListType.js";
|
} from "../anilist/types/AniListCurrentUserWatchListType.js";
|
||||||
import {writable} from 'svelte/store'
|
import { writable } from "svelte/store";
|
||||||
import type {SimklAnime, SimklUser, SimklWatchList} from "../simkl/types/simklTypes";
|
import type {
|
||||||
import {type AniListUser, MediaListSort} from "../anilist/types/AniListTypes";
|
SimklAnime,
|
||||||
import type {MALAnime, MALWatchlist, MyAnimeListUser} from "../mal/types/MALTypes";
|
SimklUser,
|
||||||
|
SimklWatchList,
|
||||||
|
} from "../simkl/types/simklTypes";
|
||||||
|
import {
|
||||||
|
type AniListUser,
|
||||||
|
MediaListSort,
|
||||||
|
} from "../anilist/types/AniListTypes";
|
||||||
|
import type {
|
||||||
|
MALAnime,
|
||||||
|
MALWatchlist,
|
||||||
|
MyAnimeListUser,
|
||||||
|
} from "../mal/types/MALTypes";
|
||||||
import type { TableItems } from "../helperTypes/TableTypes";
|
import type { TableItems } from "../helperTypes/TableTypes";
|
||||||
import { AniListGetSingleAnimeDefaultData } from "../helperDefaults/AniListGetSingleAnime";
|
import { AniListGetSingleAnimeDefaultData } from "../helperDefaults/AniListGetSingleAnime";
|
||||||
|
|
||||||
export let aniListAnime = writable(AniListGetSingleAnimeDefaultData)
|
export let aniListAnime = writable(AniListGetSingleAnimeDefaultData);
|
||||||
export let title = writable("")
|
export let title = writable("");
|
||||||
export let aniListLoggedIn = writable(false)
|
export let aniListLoggedIn = writable(false);
|
||||||
export let simklLoggedIn = writable(false)
|
export let simklLoggedIn = writable(false);
|
||||||
export let malLoggedIn = writable(false)
|
export let malLoggedIn = writable(false);
|
||||||
export let simklWatchList = writable({} as SimklWatchList)
|
export let simklWatchList = writable({} as SimklWatchList);
|
||||||
export let aniListPrimary = writable(true)
|
export let aniListPrimary = writable(true);
|
||||||
export let simklPrimary = writable(false)
|
export let simklPrimary = writable(false);
|
||||||
export let malPrimary = writable(false)
|
export let malPrimary = writable(false);
|
||||||
export let simklUser = writable({} as SimklUser)
|
export let simklUser = writable({} as SimklUser);
|
||||||
export let aniListUser = writable({} as AniListUser)
|
export let aniListUser = writable({} as AniListUser);
|
||||||
export let malUser = writable({} as MyAnimeListUser)
|
export let malUser = writable({} as MyAnimeListUser);
|
||||||
export let aniListWatchlist = writable({} as AniListCurrentUserWatchList)
|
export let aniListWatchlist = writable({} as AniListCurrentUserWatchList);
|
||||||
export let malWatchList = writable({} as MALWatchlist)
|
export let malWatchList = writable({} as MALWatchlist);
|
||||||
export let malAnime = writable({} as MALAnime)
|
export let malAnime = writable({} as MALAnime);
|
||||||
export let simklAnime = writable({} as SimklAnime)
|
export let simklAnime = writable({} as SimklAnime);
|
||||||
export let loading = writable(false)
|
export let loading = writable(false);
|
||||||
export let tableItems = writable([] as TableItems)
|
export let tableItems = writable([] as TableItems);
|
||||||
|
export let watchlistNeedsRefresh = writable(false);
|
||||||
|
export let aniListSort = writable(MediaListSort.UpdatedTimeDesc);
|
||||||
|
|
||||||
export let watchListPage = writable(1)
|
export let watchListPage = writable(1);
|
||||||
export let animePerPage = writable(20)
|
export let animePerPage = writable(20);
|
||||||
|
|
||||||
let isAniListPrimary: boolean
|
let isAniListPrimary: boolean;
|
||||||
let page: number
|
let page: number;
|
||||||
let perPage: number
|
let perPage: number;
|
||||||
let aniWatchlist: AniListCurrentUserWatchList
|
let sort: string;
|
||||||
let currentAniListAnime: AniListGetSingleAnime
|
let aniWatchlist: AniListCurrentUserWatchList;
|
||||||
|
let currentAniListAnime: AniListGetSingleAnime;
|
||||||
|
|
||||||
let isMalLoggedIn: boolean
|
let isMalLoggedIn: boolean;
|
||||||
let isSimklLoggedIn: boolean
|
let isSimklLoggedIn: boolean;
|
||||||
|
|
||||||
aniListPrimary.subscribe(value => isAniListPrimary = value)
|
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
|
||||||
watchListPage.subscribe(value => page = value)
|
watchListPage.subscribe((value) => (page = value));
|
||||||
animePerPage.subscribe(value => perPage = value)
|
animePerPage.subscribe((value) => (perPage = value));
|
||||||
aniListWatchlist.subscribe(value => aniWatchlist = value)
|
aniListWatchlist.subscribe((value) => (aniWatchlist = value));
|
||||||
malLoggedIn.subscribe(value => isMalLoggedIn = value)
|
malLoggedIn.subscribe((value) => (isMalLoggedIn = value));
|
||||||
simklLoggedIn.subscribe(value => isSimklLoggedIn = value)
|
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
|
||||||
aniListAnime.subscribe(value => currentAniListAnime = value)
|
aniListAnime.subscribe((value) => (currentAniListAnime = value));
|
||||||
|
aniListSort.subscribe((value) => (sort = value));
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
export async function GetAnimeSingleItem(aniId: number, login: boolean): Promise<""> {
|
service: string;
|
||||||
await GetAniListItem(aniId, login).then(aniListResult => {
|
message: string;
|
||||||
let finalResult: AniListGetSingleAnime
|
statusCode?: string;
|
||||||
finalResult = aniListResult
|
canRetry: boolean;
|
||||||
if (login === false) {
|
|
||||||
finalResult.data.MediaList.status = ""
|
|
||||||
finalResult.data.MediaList.score = 0
|
|
||||||
finalResult.data.MediaList.progress = 0
|
|
||||||
finalResult.data.MediaList.notes = ""
|
|
||||||
finalResult.data.MediaList.repeat = 0
|
|
||||||
finalResult.data.MediaList.startedAt.day = 0
|
|
||||||
finalResult.data.MediaList.startedAt.month = 0
|
|
||||||
finalResult.data.MediaList.startedAt.year = 0
|
|
||||||
finalResult.data.MediaList.completedAt.day = 0
|
|
||||||
finalResult.data.MediaList.completedAt.month = 0
|
|
||||||
finalResult.data.MediaList.completedAt.year = 0
|
|
||||||
}
|
}
|
||||||
aniListAnime.set(finalResult)
|
export const apiError = writable<ApiError | null>(null);
|
||||||
title.set(currentAniListAnime.data.MediaList.media.title.english === "" ?
|
export const isApiDown = writable(false);
|
||||||
currentAniListAnime.data.MediaList.media.title.romaji :
|
export function setApiError(
|
||||||
currentAniListAnime.data.MediaList.media.title.english)
|
service: string,
|
||||||
})
|
message: string,
|
||||||
|
statusCode?: string,
|
||||||
|
canRetry: boolean = true,
|
||||||
|
) {
|
||||||
|
apiError.set({
|
||||||
|
service,
|
||||||
|
message,
|
||||||
|
statusCode,
|
||||||
|
canRetry,
|
||||||
|
});
|
||||||
|
isApiDown.set(true);
|
||||||
|
}
|
||||||
|
export function clearApiError() {
|
||||||
|
apiError.set(null);
|
||||||
|
isApiDown.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GetAnimeSingleItem(
|
||||||
|
aniId: number,
|
||||||
|
login: boolean,
|
||||||
|
): Promise<""> {
|
||||||
|
await GetAniListItem(aniId, login).then((aniListResult) => {
|
||||||
|
let finalResult: AniListGetSingleAnime;
|
||||||
|
finalResult = aniListResult;
|
||||||
|
if (login === false) {
|
||||||
|
finalResult.data.MediaList.status = "";
|
||||||
|
finalResult.data.MediaList.score = 0;
|
||||||
|
finalResult.data.MediaList.progress = 0;
|
||||||
|
finalResult.data.MediaList.notes = "";
|
||||||
|
finalResult.data.MediaList.repeat = 0;
|
||||||
|
finalResult.data.MediaList.startedAt.day = 0;
|
||||||
|
finalResult.data.MediaList.startedAt.month = 0;
|
||||||
|
finalResult.data.MediaList.startedAt.year = 0;
|
||||||
|
finalResult.data.MediaList.completedAt.day = 0;
|
||||||
|
finalResult.data.MediaList.completedAt.month = 0;
|
||||||
|
finalResult.data.MediaList.completedAt.year = 0;
|
||||||
|
}
|
||||||
|
aniListAnime.set(finalResult);
|
||||||
|
title.set(
|
||||||
|
currentAniListAnime.data.MediaList.media.title.english === ""
|
||||||
|
? currentAniListAnime.data.MediaList.media.title.romaji
|
||||||
|
: currentAniListAnime.data.MediaList.media.title.english,
|
||||||
|
);
|
||||||
|
});
|
||||||
if (isMalLoggedIn) {
|
if (isMalLoggedIn) {
|
||||||
await GetMyAnimeListAnime(currentAniListAnime.data.MediaList.media.idMal).then(malResult => {
|
await GetMyAnimeListAnime(
|
||||||
malAnime.set(malResult)
|
currentAniListAnime.data.MediaList.media.idMal,
|
||||||
})
|
).then((malResult) => {
|
||||||
|
malAnime.set(malResult);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (isSimklLoggedIn) {
|
if (isSimklLoggedIn) {
|
||||||
await SimklSearch(currentAniListAnime.data.MediaList).then((value: SimklAnime) => {
|
await SimklSearch(currentAniListAnime.data.MediaList).then(
|
||||||
simklAnime.set(value)
|
(value: SimklAnime) => {
|
||||||
})
|
simklAnime.set(value);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return ""
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loginToSimkl(): void {
|
export function loginToSimkl(): void {
|
||||||
GetSimklLoggedInUser().then(user => {
|
GetSimklLoggedInUser().then((user) => {
|
||||||
if (Object.keys(user).length === 0) {
|
if (Object.keys(user).length === 0) {
|
||||||
simklLoggedIn.set(false)
|
simklLoggedIn.set(false);
|
||||||
} else {
|
} else {
|
||||||
simklUser.set(user)
|
simklUser.set(user);
|
||||||
SimklGetUserWatchlist().then(result => {
|
SimklGetUserWatchlist().then((result) => {
|
||||||
simklWatchList.set(result)
|
simklWatchList.set(result);
|
||||||
simklLoggedIn.set(true)
|
simklLoggedIn.set(true);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loginToAniList(): void {
|
export function loginToAniList(): void {
|
||||||
GetAniListLoggedInUser().then(result => {
|
GetAniListLoggedInUser().then((result) => {
|
||||||
aniListUser.set(result)
|
aniListUser.set(result);
|
||||||
if (isAniListPrimary) {
|
if (isAniListPrimary) {
|
||||||
GetAniListUserWatchingList(page, perPage, MediaListSort.UpdatedTimeDesc).then((result) => {
|
GetAniListUserWatchingList(page, perPage, sort).then((result) => {
|
||||||
aniListWatchlist.set(result)
|
aniListWatchlist.set(result);
|
||||||
aniListLoggedIn.set(true)
|
aniListLoggedIn.set(true);
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
aniListLoggedIn.set(true)
|
aniListLoggedIn.set(true);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loginToMAL(): void {
|
export function loginToMAL(): void {
|
||||||
GetMyAnimeListLoggedInUser().then(result => {
|
GetMyAnimeListLoggedInUser().then((result) => {
|
||||||
malUser.set(result)
|
malUser.set(result);
|
||||||
malLoggedIn.set(true)
|
malLoggedIn.set(true);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logoutOfAniList(): void {
|
export function logoutOfAniList(): void {
|
||||||
LogoutAniList().then(result => {
|
LogoutAniList().then((result) => {
|
||||||
console.log(result)
|
console.log(result);
|
||||||
if (Object.keys(aniWatchlist).length !== 0) {
|
if (Object.keys(aniWatchlist).length !== 0) {
|
||||||
aniListWatchlist.set({} as AniListCurrentUserWatchList)
|
aniListWatchlist.set({} as AniListCurrentUserWatchList);
|
||||||
}
|
}
|
||||||
aniListUser.set({} as AniListUser)
|
aniListUser.set({} as AniListUser);
|
||||||
aniListLoggedIn.set(false)
|
aniListLoggedIn.set(false);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logoutOfMAL(): void {
|
export function logoutOfMAL(): void {
|
||||||
LogoutMyAnimeList().then(result => {
|
LogoutMyAnimeList().then((result) => {
|
||||||
console.log(result)
|
console.log(result);
|
||||||
malUser.set({} as MyAnimeListUser)
|
malUser.set({} as MyAnimeListUser);
|
||||||
malLoggedIn.set(false)
|
malLoggedIn.set(false);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logoutOfSimkl(): void {
|
export function logoutOfSimkl(): void {
|
||||||
LogoutSimkl().then(result => {
|
LogoutSimkl().then((result) => {
|
||||||
console.log(result)
|
console.log(result);
|
||||||
simklUser.set({} as SimklUser)
|
simklUser.set({} as SimklUser);
|
||||||
simklLoggedIn.set(false)
|
simklLoggedIn.set(false);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,9 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { aniListAnime, GetAnimeSingleItem } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
||||||
import Anime from "../helperComponents/Anime.svelte"
|
import Anime from "../helperComponents/Anime.svelte"
|
||||||
|
import { AniListGetSingleAnimeDefaultData } from "../helperDefaults/AniListGetSingleAnime";
|
||||||
|
import Spinner from "../helperComponents/Spinner.svelte";
|
||||||
|
|
||||||
export let params: Record<string, string>
|
export let params: Record<string, string>
|
||||||
|
let loadPromise = load(params.id)
|
||||||
|
$: loadPromise = load(params.id)
|
||||||
|
|
||||||
|
async function load(id: string) {
|
||||||
|
aniListAnime.update(() => AniListGetSingleAnimeDefaultData)
|
||||||
|
await GetAnimeSingleItem(Number(id), true)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#key params.id}
|
{#key params.id}
|
||||||
|
{#await loadPromise}
|
||||||
|
<Spinner />
|
||||||
|
{:then _}
|
||||||
<Anime />
|
<Anime />
|
||||||
|
{/await}
|
||||||
{/key}
|
{/key}
|
||||||
@@ -5,16 +5,46 @@ import {
|
|||||||
aniListLoggedIn,
|
aniListLoggedIn,
|
||||||
aniListPrimary,
|
aniListPrimary,
|
||||||
loading,
|
loading,
|
||||||
|
isApiDown,
|
||||||
|
apiError,
|
||||||
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
|
||||||
import loader from '../helperFunctions/loader'
|
import loader from "../helperFunctions/loader";
|
||||||
|
|
||||||
let isAniListPrimary: boolean
|
let isAniListPrimary: boolean;
|
||||||
let isAniListLoggedIn: boolean
|
let isAniListLoggedIn: boolean;
|
||||||
|
|
||||||
aniListPrimary.subscribe((value) => isAniListPrimary = value)
|
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
|
||||||
aniListLoggedIn.subscribe((value) => isAniListLoggedIn = value)
|
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
|
||||||
</script>
|
</script>
|
||||||
{#if isAniListLoggedIn && isAniListPrimary}
|
|
||||||
|
{#if $isApiDown}
|
||||||
|
<div class="container py-10">
|
||||||
|
<div
|
||||||
|
class="bg-yellow-50 border border-yellow-200 rounded-lg p-6 text-center"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="mx-auto h-12 w-12 text-yellow-600 mb-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<h2 class="text-xl font-semibold text-yellow-900 mb-2">
|
||||||
|
API Unavailable
|
||||||
|
</h2>
|
||||||
|
<p class="text-yellow-700 mb-4">
|
||||||
|
The {$apiError?.service || "service"} is currently unavailable. The app will
|
||||||
|
remain open, and you can retry when the service is back online.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if isAniListLoggedIn && isAniListPrimary}
|
||||||
<div class="container py-10">
|
<div class="container py-10">
|
||||||
<Pagination />
|
<Pagination />
|
||||||
<WatchList />
|
<WatchList />
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
// vite.config.ts
|
|
||||||
import { defineConfig } from "file:///home/nymusicman/Code/AniTrack/frontend/node_modules/vite/dist/node/index.js";
|
|
||||||
import { svelte } from "file:///home/nymusicman/Code/AniTrack/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js";
|
|
||||||
var vite_config_default = defineConfig({
|
|
||||||
plugins: [svelte()]
|
|
||||||
});
|
|
||||||
export {
|
|
||||||
vite_config_default as default
|
|
||||||
};
|
|
||||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9ueW11c2ljbWFuL0NvZGUvQW5pVHJhY2svZnJvbnRlbmRcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9ob21lL255bXVzaWNtYW4vQ29kZS9BbmlUcmFjay9mcm9udGVuZC92aXRlLmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vaG9tZS9ueW11c2ljbWFuL0NvZGUvQW5pVHJhY2svZnJvbnRlbmQvdml0ZS5jb25maWcudHNcIjtpbXBvcnQge2RlZmluZUNvbmZpZ30gZnJvbSAndml0ZSdcbmltcG9ydCB7c3ZlbHRlfSBmcm9tICdAc3ZlbHRlanMvdml0ZS1wbHVnaW4tc3ZlbHRlJ1xuXG4vLyBodHRwczovL3ZpdGVqcy5kZXYvY29uZmlnL1xuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcbiAgcGx1Z2luczogW3N2ZWx0ZSgpXVxufSlcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBdVMsU0FBUSxvQkFBbUI7QUFDbFUsU0FBUSxjQUFhO0FBR3JCLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K
|
|
||||||
2
frontend/wailsjs/go/main/App.d.ts
vendored
2
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -42,6 +42,8 @@ export function MyAnimeListLogin():Promise<void>;
|
|||||||
|
|
||||||
export function MyAnimeListUpdate(arg1:main.MALAnime,arg2:main.MALUploadStatus):Promise<main.MalListStatus>;
|
export function MyAnimeListUpdate(arg1:main.MALAnime,arg2:main.MALUploadStatus):Promise<main.MalListStatus>;
|
||||||
|
|
||||||
|
export function ShowVersion():Promise<void>;
|
||||||
|
|
||||||
export function SimklGetUserWatchlist():Promise<main.SimklWatchListType>;
|
export function SimklGetUserWatchlist():Promise<main.SimklWatchListType>;
|
||||||
|
|
||||||
export function SimklLogin():Promise<void>;
|
export function SimklLogin():Promise<void>;
|
||||||
|
|||||||
@@ -82,6 +82,10 @@ export function MyAnimeListUpdate(arg1, arg2) {
|
|||||||
return window['go']['main']['App']['MyAnimeListUpdate'](arg1, arg2);
|
return window['go']['main']['App']['MyAnimeListUpdate'](arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ShowVersion() {
|
||||||
|
return window['go']['main']['App']['ShowVersion']();
|
||||||
|
}
|
||||||
|
|
||||||
export function SimklGetUserWatchlist() {
|
export function SimklGetUserWatchlist() {
|
||||||
return window['go']['main']['App']['SimklGetUserWatchlist']();
|
return window['go']['main']['App']['SimklGetUserWatchlist']();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,8 +202,7 @@ export namespace main {
|
|||||||
export class MALAnime {
|
export class MALAnime {
|
||||||
id: id;
|
id: id;
|
||||||
title: title;
|
title: title;
|
||||||
// Go type: struct { Large string "json:\"large\" json:\"large\""; Medium string "json:\"medium\" json:\"medium\"" }
|
main_picture: mainPicture;
|
||||||
main_picture: any;
|
|
||||||
alternative_titles: alternativeTitles;
|
alternative_titles: alternativeTitles;
|
||||||
start_date: startDate;
|
start_date: startDate;
|
||||||
end_date: endDate;
|
end_date: endDate;
|
||||||
@@ -242,7 +241,7 @@ export namespace main {
|
|||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.id = source["id"];
|
this.id = source["id"];
|
||||||
this.title = source["title"];
|
this.title = source["title"];
|
||||||
this.main_picture = this.convertValues(source["main_picture"], Object);
|
this.main_picture = source["main_picture"];
|
||||||
this.alternative_titles = source["alternative_titles"];
|
this.alternative_titles = source["alternative_titles"];
|
||||||
this.start_date = source["start_date"];
|
this.start_date = source["start_date"];
|
||||||
this.end_date = source["end_date"];
|
this.end_date = source["end_date"];
|
||||||
@@ -314,7 +313,7 @@ export namespace main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
export class MALWatchlist {
|
export class MALWatchlist {
|
||||||
data: struct { Node struct { Id int "json:\"id\" ts_type:\"id\""; Title string "json:\"title\" ts_type:\"title\""; MainPicture struct { Medium string "json:\"medium\" json:\"medium\""; Large string "json:\"large\" json:\"large\"" } "json:\"main_picture\" json:\"mainPicture\"" } "json:\"node\" json:\"node\""; ListStatus struct { Status string "json:\"status\" ts_type:\"status\""; Score int "json:\"score\" ts_type:\"score\""; NumEpisodesWatched int "json:\"num_episodes_watched\" ts_type:\"numEpisodesWatched\""; IsRewatching bool "json:\"is_rewatching\" ts_type:\"isRewatching\""; UpdatedAt time.Time "json:\"updated_at\" ts_type:\"updatedAt\""; StartDate string "json:\"start_date\" ts_type:\"startDate\""; FinishDate string "json:\"finish_date\" ts_type:\"finishDate\"" } "json:\"list_status\" ts_type:\"listStatus\"" }[];
|
data: data;
|
||||||
paging: paging;
|
paging: paging;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
@@ -323,27 +322,9 @@ export namespace main {
|
|||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.data = this.convertValues(source["data"], struct { Node struct { Id int "json:\"id\" ts_type:\"id\""; Title string "json:\"title\" ts_type:\"title\""; MainPicture struct { Medium string "json:\"medium\" json:\"medium\""; Large string "json:\"large\" json:\"large\"" } "json:\"main_picture\" json:\"mainPicture\"" } "json:\"node\" json:\"node\""; ListStatus struct { Status string "json:\"status\" ts_type:\"status\""; Score int "json:\"score\" ts_type:\"score\""; NumEpisodesWatched int "json:\"num_episodes_watched\" ts_type:\"numEpisodesWatched\""; IsRewatching bool "json:\"is_rewatching\" ts_type:\"isRewatching\""; UpdatedAt time.Time "json:\"updated_at\" ts_type:\"updatedAt\""; StartDate string "json:\"start_date\" ts_type:\"startDate\""; FinishDate string "json:\"finish_date\" ts_type:\"finishDate\"" } "json:\"list_status\" ts_type:\"listStatus\"" });
|
this.data = source["data"];
|
||||||
this.paging = source["paging"];
|
this.paging = source["paging"];
|
||||||
}
|
}
|
||||||
|
|
||||||
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 MalListStatus {
|
export class MalListStatus {
|
||||||
status: status;
|
status: status;
|
||||||
@@ -383,7 +364,7 @@ export namespace main {
|
|||||||
id: number;
|
id: number;
|
||||||
mediaId: number;
|
mediaId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
// Go type: 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\""; Tags []struct { Id int "json:\"id\""; Name string "json:\"name\""; Description string "json:\"description\""; Rank int "json:\"rank\""; IsMediaSpoiler bool "json:\"isMediaSpoiler\""; IsAdult bool "json:\"isAdult\"" } "json:\"tags\""; IsAdult bool "json:\"isAdult\"" }
|
// Go type: 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\""; Genres []string "json:\"genres\""; Tags []struct { Id int "json:\"id\""; Name string "json:\"name\""; Description string "json:\"description\""; Rank int "json:\"rank\""; IsMediaSpoiler bool "json:\"isMediaSpoiler\""; IsAdult bool "json:\"isAdult\"" } "json:\"tags\""; IsAdult bool "json:\"isAdult\"" }
|
||||||
media: any;
|
media: any;
|
||||||
status: string;
|
status: string;
|
||||||
// Go type: struct { Year int "json:\"year\""; Month int "json:\"month\""; Day int "json:\"day\"" }
|
// Go type: struct { Year int "json:\"year\""; Month int "json:\"month\""; Day int "json:\"day\"" }
|
||||||
@@ -624,11 +605,10 @@ export namespace struct { Node main {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace struct { Node struct { Id int "json:\"id\" ts_type:\"id\""; Title string "json:\"title\" ts_type:\"title\""; MainPicture struct { Medium string "json:\"medium\" json:\"medium\""; Large string "json:\"large\" json:\"large\"" } "json:\"main_picture\" json:\"mainPicture\"" } "json:\"node\" json:\"node\""; ListStatus struct { Status string "json:\"status\" ts_type:\"status\""; Score int "json:\"score\" ts_type:\"score\""; NumEpisodesWatched int "json:\"num_episodes_watched\" ts_type:\"numEpisodesWatched\""; IsRewatching bool "json:\"is_rewatching\" ts_type:\"isRewatching\""; UpdatedAt time {
|
export namespace struct { Node struct { Id int "json:\"id\" ts_type:\"id\""; Title string "json:\"title\" ts_type:\"title\""; MainPicture struct { Medium string "json:\"medium\" ts_type:\"medium\""; Large string "json:\"large\" ts_type:\"large\"" } "json:\"main_picture\" ts_type:\"mainPicture\"" } "json:\"node\" ts_type:\"node\""; ListStatus struct { Status string "json:\"status\" ts_type:\"status\""; Score int "json:\"score\" ts_type:\"score\""; NumEpisodesWatched int "json:\"num_episodes_watched\" ts_type:\"numEpisodesWatched\""; IsRewatching bool "json:\"is_rewatching\" ts_type:\"isRewatching\""; UpdatedAt time {
|
||||||
|
|
||||||
export class {
|
export class {
|
||||||
// Go type: struct { Id int "json:\"id\" ts_type:\"id\""; Title string "json:\"title\" ts_type:\"title\""; MainPicture struct { Medium string "json:\"medium\" json:\"medium\""; Large string "json:\"large\" json:\"large\"" } "json:\"main_picture\" json:\"mainPicture\"" }
|
node: node;
|
||||||
node: any;
|
|
||||||
list_status: listStatus;
|
list_status: listStatus;
|
||||||
|
|
||||||
static createFrom(source: any = {}) {
|
static createFrom(source: any = {}) {
|
||||||
@@ -637,27 +617,9 @@ export namespace struct { Node struct { Id int "json:\"id\" ts_type:\"id\""; Tit
|
|||||||
|
|
||||||
constructor(source: any = {}) {
|
constructor(source: any = {}) {
|
||||||
if ('string' === typeof source) source = JSON.parse(source);
|
if ('string' === typeof source) source = JSON.parse(source);
|
||||||
this.node = this.convertValues(source["node"], Object);
|
this.node = source["node"];
|
||||||
this.list_status = source["list_status"];
|
this.list_status = source["list_status"];
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
frontend/wailsjs/runtime/runtime.d.ts
vendored
2
frontend/wailsjs/runtime/runtime.d.ts
vendored
@@ -134,7 +134,7 @@ export function WindowIsFullscreen(): Promise<boolean>;
|
|||||||
|
|
||||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||||
// Sets the width and height of the window.
|
// Sets the width and height of the window.
|
||||||
export function WindowSetSize(width: number, height: number): Promise<Size>;
|
export function WindowSetSize(width: number, height: number): void;
|
||||||
|
|
||||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||||
// Gets the width and height of the window.
|
// Gets the width and height of the window.
|
||||||
|
|||||||
24
go.mod
24
go.mod
@@ -1,49 +1,49 @@
|
|||||||
module AniTrack
|
module AniTrack
|
||||||
|
|
||||||
go 1.23
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/keyring v1.2.2
|
github.com/99designs/keyring v1.2.2
|
||||||
github.com/tidwall/gjson v1.18.0
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/wailsapp/wails/v2 v2.9.2
|
github.com/wailsapp/wails/v2 v2.10.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
|
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
|
||||||
github.com/bep/debounce v1.2.1 // indirect
|
github.com/bep/debounce v1.2.1 // indirect
|
||||||
github.com/danieljoos/wincred v1.2.2 // indirect
|
github.com/danieljoos/wincred v1.2.2 // indirect
|
||||||
github.com/dvsekhvalnov/jose2go v1.7.0 // indirect
|
github.com/dvsekhvalnov/jose2go v1.8.0 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.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 v0.0.0-20190726142602-4481cbc300e2 // indirect
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // 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/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||||
github.com/labstack/echo/v4 v4.12.0 // indirect
|
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||||
github.com/labstack/gommon v0.4.2 // indirect
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||||
github.com/leaanthony/u v1.1.1 // indirect
|
github.com/leaanthony/u v1.1.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mtibben/percent v0.2.1 // indirect
|
github.com/mtibben/percent v0.2.1 // indirect
|
||||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/samber/lo v1.47.0 // indirect
|
github.com/samber/lo v1.49.1 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.1 // indirect
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/wailsapp/go-webview2 v1.0.16 // indirect
|
github.com/wailsapp/go-webview2 v1.0.19 // indirect
|
||||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||||
golang.org/x/crypto v0.28.0 // indirect
|
golang.org/x/crypto v0.45.0 // indirect
|
||||||
golang.org/x/net v0.30.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
golang.org/x/sys v0.26.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
golang.org/x/term v0.25.0 // indirect
|
golang.org/x/term v0.37.0 // indirect
|
||||||
golang.org/x/text v0.19.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
// replace github.com/wailsapp/wails/v2 v2.9.1 => /home/nymusicman/go/pkg/mod
|
// replace github.com/wailsapp/wails/v2 v2.9.1 => /home/nymusicman/go/pkg/mod
|
||||||
|
|||||||
53
go.sum
53
go.sum
@@ -8,8 +8,8 @@ github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ
|
|||||||
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
|
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dvsekhvalnov/jose2go v1.7.0 h1:bnQc8+GMnidJZA8zc6lLEAb4xNrIqHwO+9TzqvtQZPo=
|
github.com/dvsekhvalnov/jose2go v1.8.0 h1:LqkkVKAlHFfH9LOEl5fe4p/zL02OhWE7pCufMBG2jLA=
|
||||||
github.com/dvsekhvalnov/jose2go v1.7.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
|
github.com/dvsekhvalnov/jose2go v1.8.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 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
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 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0=
|
||||||
@@ -23,10 +23,11 @@ github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NM
|
|||||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
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/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
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/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/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||||
|
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||||
@@ -42,9 +43,8 @@ github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQc
|
|||||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
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 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
|
||||||
@@ -60,12 +60,12 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
|||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
@@ -79,31 +79,30 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
|||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
github.com/wailsapp/go-webview2 v1.0.16 h1:wffnvnkkLvhRex/aOrA3R7FP7rkvOqL/bir1br7BekU=
|
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
|
||||||
github.com/wailsapp/go-webview2 v1.0.16/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
|
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||||
github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
|
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
|
||||||
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
|
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
|
||||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
|
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/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
4
main.go
4
main.go
@@ -34,9 +34,9 @@ func main() {
|
|||||||
app,
|
app,
|
||||||
},
|
},
|
||||||
Linux: &linux.Options{
|
Linux: &linux.Options{
|
||||||
Icon: []byte("./build/appicon.png"),
|
Icon: []byte("./build/AniTrack.png"),
|
||||||
WindowIsTranslucent: false,
|
WindowIsTranslucent: false,
|
||||||
WebviewGpuPolicy: linux.WebviewGpuPolicyAlways,
|
WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
|
||||||
ProgramName: "AniTrack",
|
ProgramName: "AniTrack",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,6 +12,6 @@
|
|||||||
},
|
},
|
||||||
"info": {
|
"info": {
|
||||||
"productName": "AniTrack",
|
"productName": "AniTrack",
|
||||||
"productVersion": "0.1.1"
|
"productVersion": "0.7.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user