54 Commits
0.1.2 ... 0.2.0

Author SHA1 Message Date
c10e853564 updated minor version for tag fix 2025-06-06 23:25:24 -04:00
cd043d093f fixed issue where tags would not reload after submit 2025-06-06 23:25:08 -04:00
6db01f7f9f added tags to frontend anime item 2025-05-14 14:31:43 -04:00
8460d56d55 created simple linux install script for AniTrack 2025-05-07 10:03:58 -04:00
8fedbe4607 created xdg icon set instead of one big icon 2025-05-07 10:03:37 -04:00
e069c47242 automated adding anime by moving progress from 0 to 1 2025-03-30 10:03:05 -04:00
aba0f2d1d5 removed console.log 2025-03-30 10:02:38 -04:00
af6cb7f08a removed unecessary button dependency and neovim reformatted 2025-03-30 09:52:46 -04:00
487e5ee5a8 upped version for automation 2025-03-26 19:36:20 -04:00
631bd8b885 made increment and decrement buttons automate status and datecompleted 2025-03-26 19:35:08 -04:00
b35be6926a rename code to ANILIST_CODE in environment 2025-03-16 13:51:00 -04:00
5a9f4391dc changed flowbite button to standard button 2025-03-03 19:53:18 -05:00
72004c98b4 fixed button colors 2025-03-03 17:27:39 -05:00
3db25bc33a restored status block 2025-03-03 16:13:01 -05:00
f3a1536953 updated version for visual corrections and library updtae 2025-03-03 15:44:05 -05:00
dc01aa314c made dark mode default for every element 2025-03-03 15:29:33 -05:00
baed9a4a67 upgraded wails and crypts 2025-03-03 15:28:30 -05:00
0e00120778 fixed watchlist url 2025-02-18 19:43:20 -05:00
697692c277 reordered environment variables 2025-02-18 12:57:30 -05:00
3e82677c2c added simkl rest files 2025-02-18 12:50:23 -05:00
b81549e5a6 added MAL requests 2025-02-18 12:31:16 -05:00
fd806df0a9 added rest anilist set items 2025-02-18 12:06:58 -05:00
26f85dd412 I want to keep all changes
Merge branch 'main' of ssh://git.linuxhg.com:2222/john-okeefe/anitrack
2025-02-18 11:43:17 -05:00
4d9c54a116 added AniList Auth Pages 2025-02-17 21:02:30 -05:00
068e568ec6 added AniList GET REST items 2025-02-17 20:50:10 -05:00
4e6f910e74 format change 2025-02-17 20:03:53 -05:00
b39f19f03a added REST private env to gitignore 2025-02-17 20:03:16 -05:00
1a083deb54 reverted back to svelte-headless-table 2025-02-15 21:12:36 -05:00
61f8f5dd84 removed println from simkl body 2025-02-15 18:48:17 -05:00
4509e479bc upgraded go packages 2025-02-15 18:48:03 -05:00
ee8dd2e866 upgraded go packages 2025-02-15 18:47:52 -05:00
0c6a8a40c3 removed unused import 2025-02-07 22:58:39 -05:00
45845c2a69 finished switch from svelte table to tanstack table 2025-02-07 22:57:05 -05:00
3ec5eb1a03 began transition to tanstack table 2025-02-05 21:36:23 -05:00
a2aa90edec added buttons to top of table 2025-01-26 11:00:49 -05:00
d08283bd2d upped minor version 2025-01-25 22:51:59 -05:00
73d349ee1a cleaned up some print lines 2025-01-25 22:50:54 -05:00
c9c6650829 significantly improved datepicker 2025-01-25 22:50:35 -05:00
896c6640e2 added datepicker manually 2025-01-25 19:18:08 -05:00
18c744c1cf fixed formatting 2025-01-25 19:17:42 -05:00
0475d39c6c added genres to AniList Item.bru 2025-01-19 13:02:55 -05:00
dde5d20537 turned off webgl for this app due to breakage 2025-01-17 22:05:11 -05:00
314646e7f5 upgraded go packages 2025-01-17 22:04:38 -05:00
72dfbf4a03 spelling correct 2025-01-17 20:39:18 -05:00
d4ad4bc430 updated go packages 2025-01-17 20:39:06 -05:00
49681f3ffb point appicon to anitrack logo 2024-12-15 00:31:34 -05:00
d98d0e77c1 wails autofix based on changes 2024-12-15 00:31:10 -05:00
8e57b4b259 added a simple desktop file to repo 2024-12-15 00:30:53 -05:00
1e1c891173 renamed appicon to AniTrack 2024-12-15 00:30:38 -05:00
5ee9c42352 cleaned up errors in go code 2024-12-15 00:19:48 -05:00
23ec111c60 upped minor version for episode button 2024-12-14 13:50:09 -05:00
f24ee9edfd improved episode input in Anime Page 2024-12-14 13:48:55 -05:00
1fd453f399 upped version number for bug fix 2024-12-06 16:32:06 -05:00
3ab77ea8d3 fixed bug in episode input when 0 2024-12-06 16:30:53 -05:00
57 changed files with 1895 additions and 483 deletions

5
.gitignore vendored
View File

@ -29,4 +29,7 @@ package.json.md5
package-lock.json
.idea
.env
environment.go
environment.go
# REST (http files)
http-client.private.env.json

View File

@ -17,26 +17,29 @@ func AniListQuery(body interface{}, login bool) (json.RawMessage, string) {
if login && (AniListJWT{}) != aniListJwt {
response.Header.Add("Authorization", "Bearer "+aniListJwt.AccessToken)
} 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("Accept", "application/json")
client := &http.Client{}
res, reserr := client.Do(response)
if reserr != nil {
res, resErr := client.Do(response)
if resErr != nil {
log.Printf("Failed at res, %s\n", err)
}
defer res.Body.Close()
returnedBody, err := io.ReadAll(res.Body)
if err != nil {
return nil, "Could not read the returned body."
}
return returnedBody, res.Status
}
func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
var user = a.GetAniListLoggedInUser()
user := a.GetAniListLoggedInUser()
var neededVariables interface{}
@ -141,11 +144,11 @@ func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
returnedBody, status := AniListQuery(body, login)
var post AniListGetSingleAnime
if status == "404 Not Found" && login == false {
if status == "404 Not Found" && !login {
return post
}
if status == "404 Not Found" && login {
if status == "404 Not Found" {
post = a.GetAniListItem(aniId, false)
}
@ -154,7 +157,7 @@ func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
log.Printf("Failed at unmarshal, %s\n", err)
}
if login == false {
if !login {
post.Data.MediaList.UserID = user.Data.Viewer.ID
post.Data.MediaList.Status = ""
post.Data.MediaList.StartedAt.Year = 0
@ -199,7 +202,7 @@ func (a *App) AniListSearch(query string) any {
perPage
}
media (search: $search, type: $listType) {
id
id
idMal
title {
romaji
@ -249,7 +252,7 @@ func (a *App) AniListSearch(query string) any {
}
func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) AniListCurrentUserWatchList {
var user = a.GetAniListLoggedInUser()
user := a.GetAniListLoggedInUser()
type Variables struct {
Page int `json:"page"`
PerPage int `json:"perPage"`
@ -412,8 +415,8 @@ func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSi
}{
Query: `
mutation(
$mediaId:Int,
$progress:Int,
$mediaId:Int,
$progress:Int,
$status:MediaListStatus,
$score:Float,
$repeat:Int,
@ -422,15 +425,15 @@ func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSi
$completedAt:FuzzyDateInput,
){
SaveMediaListEntry(
mediaId:$mediaId,
progress:$progress,
mediaId:$mediaId,
progress:$progress,
status:$status,
score:$score,
repeat:$repeat,
notes:$notes,
startedAt:$startedAt
completedAt:$completedAt
){
){
id
mediaId
userId
@ -525,7 +528,7 @@ func (a *App) AniListDeleteEntry(mediaListId int) DeleteAniListReturn {
){
DeleteMediaListEntry(
id:$id,
){
){
deleted
}
}

View File

@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
@ -30,11 +31,11 @@ var aniCtxShutdown, aniCancel = context.WithCancel(context.Background())
func (a *App) CheckIfAniListLoggedIn() bool {
if (AniListJWT{} == aniListJwt) {
tokenType, err := aniRing.Get("anilistTokenType")
expiresIn, err := aniRing.Get("anilistTokenExpiresIn")
accessToken, err := aniRing.Get("anilistAccessToken")
refreshToken, err := aniRing.Get("anilistRefreshToken")
if err != nil || len(accessToken.Data) == 0 {
tokenType, tokenErr := aniRing.Get("anilistTokenType")
expiresIn, expiresInErr := aniRing.Get("anilistTokenExpiresIn")
refreshToken, refreshTokenErr := aniRing.Get("anilistRefreshToken")
accessToken, accessTokenErr := aniRing.Get("anilistAccessToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
return false
} else {
aniListJwt.TokenType = string(tokenType.Data)
@ -50,11 +51,11 @@ func (a *App) CheckIfAniListLoggedIn() bool {
func (a *App) AniListLogin() {
if (AniListJWT{} == aniListJwt) {
tokenType, err := aniRing.Get("anilistTokenType")
expiresIn, err := aniRing.Get("anilistTokenExpiresIn")
accessToken, err := aniRing.Get("anilistAccessToken")
refreshToken, err := aniRing.Get("anilistRefreshToken")
if err != nil || len(accessToken.Data) == 0 {
tokenType, tokenErr := aniRing.Get("anilistTokenType")
expiresIn, expiresInErr := aniRing.Get("anilistTokenExpiresIn")
refreshToken, refreshTokenErr := aniRing.Get("anilistRefreshToken")
accessToken, accessTokenErr := aniRing.Get("anilistAccessToken")
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"
runtime.BrowserOpenURL(*wailsContext, getAniListCodeUrl)
@ -123,7 +124,7 @@ func (a *App) handleAniListCallback(wg *sync.WaitGroup) {
go func() {
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)
}
fmt.Println("Shutting down...")
@ -152,14 +153,17 @@ func getAniListAuthorizationToken(content string) AniListJWT {
response.Header.Add("Accept", "application/json")
client := &http.Client{}
res, reserr := client.Do(response)
if reserr != nil {
res, resErr := client.Do(response)
if resErr != nil {
log.Printf("Failed at res, %s\n", err)
}
defer res.Body.Close()
returnedBody, err := io.ReadAll(res.Body)
if err != nil {
log.Printf("Could not read returned body, %s\n.", err)
}
var post AniListJWT
err = json.Unmarshal(returnedBody, &post)
@ -204,13 +208,12 @@ func (a *App) GetAniListLoggedInUser() AniListUser {
func (a *App) LogoutAniList() string {
if (AniListJWT{} != aniListJwt) {
err := aniRing.Remove("anilistTokenType")
err = aniRing.Remove("anilistTokenExpiresIn")
err = aniRing.Remove("anilistAccessToken")
err = aniRing.Remove("anilistRefreshToken")
if err != nil {
fmt.Println("AniList Logout Failed", err)
typeErr := aniRing.Remove("anilistTokenType")
expiresInErr := aniRing.Remove("anilistTokenExpiresIn")
accessTokenErr := aniRing.Remove("anilistAccessToken")
refreshTokenErr := aniRing.Remove("anilistRefreshToken")
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil {
fmt.Println("AniList Logout Failed")
}
aniListJwt = AniListJWT{}
}

View File

@ -46,10 +46,10 @@ type MALWatchlist 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"`
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"`
@ -59,7 +59,7 @@ type MALWatchlist struct {
StartDate string `json:"start_date" ts_type:"startDate"`
FinishDate string `json:"finish_date" ts_type:"finishDate"`
} `json:"list_status" ts_type:"listStatus"`
} `json:"data" json:"data"`
} `json:"data" ts_type:"data"`
Paging struct {
Previous string `json:"previous" ts_type:"previous"`
Next string `json:"next" ts_type:"next"`
@ -70,9 +70,9 @@ type MALAnime struct {
Id int `json:"id" ts_type:"id"`
Title string `json:"title" ts_type:"title"`
MainPicture struct {
Large string `json:"large" json:"large"`
Medium string `json:"medium" json:"medium"`
} `json:"main_picture" json:"mainPicture"`
Large string `json:"large" ts_type:"large"`
Medium string `json:"medium" ts_type:"medium"`
} `json:"main_picture" ts_type:"mainPicture"`
AlternativeTitles struct {
Synonyms []string `json:"synonyms" ts_type:"synonyms"`
En string `json:"en" ts_type:"en"`

View File

@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
@ -50,7 +51,7 @@ func base64URLEncode(str []byte) string {
func verifier() (*CodeVerifier, error) {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
b := make([]byte, length, length)
b := make([]byte, length)
for i := 0; i < length; i++ {
b[i] = byte(r.Intn(255))
}
@ -71,16 +72,17 @@ func (v *CodeVerifier) CodeChallengeS256() string {
func (a *App) CheckIfMyAnimeListLoggedIn() bool {
if (MyAnimeListJWT{} == myAnimeListJwt) {
tokenType, err := myAnimeListRing.Get("MyAnimeListTokenType")
expiresIn, err := myAnimeListRing.Get("MyAnimeListExpiresIn")
accessToken, err := myAnimeListRing.Get("MyAnimeListAccessToken")
refreshToken, err := myAnimeListRing.Get("MyAnimeListRefreshToken")
if err != nil || len(accessToken.Data) == 0 {
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType")
expiresIn, expiresInErr := myAnimeListRing.Get("MyAnimeListExpiresIn")
refreshToken, refreshTokenErr := myAnimeListRing.Get("MyAnimeListAccessToken")
accessToken, accessTokenErr := myAnimeListRing.Get("MyAnimeListRefreshToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
return false
} else {
var expiresInConvertErr error
myAnimeListJwt.TokenType = string(tokenType.Data)
myAnimeListJwt.ExpiresIn, err = strconv.Atoi(string(expiresIn.Data))
if err != nil {
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data))
if expiresInConvertErr != nil {
fmt.Println("unable to convert string to int")
}
myAnimeListJwt.AccessToken = string(accessToken.Data)
@ -93,12 +95,13 @@ func (a *App) CheckIfMyAnimeListLoggedIn() bool {
}
func (a *App) MyAnimeListLogin() {
if a.CheckIfMyAnimeListLoggedIn() == false {
tokenType, err := myAnimeListRing.Get("MyAnimeListTokenType")
expiresIn, err := myAnimeListRing.Get("MyAnimeListExpiresIn")
accessToken, err := myAnimeListRing.Get("MyAnimeListAccessToken")
refreshToken, err := myAnimeListRing.Get("MyAnimeListRefreshToken")
if err != nil || len(accessToken.Data) == 0 {
if !a.CheckIfMyAnimeListLoggedIn() {
fmt.Println("check logged in function failed")
tokenType, tokenErr := myAnimeListRing.Get("MyAnimeListTokenType")
expiresIn, expiresInErr := myAnimeListRing.Get("MyAnimeListExpiresIn")
refreshToken, refreshTokenErr := myAnimeListRing.Get("MyAnimeListAccessToken")
accessToken, accessTokenErr := myAnimeListRing.Get("MyAnimeListRefreshToken")
if (tokenErr != nil || expiresInErr != nil || refreshTokenErr != nil || accessTokenErr != nil) || len(accessToken.Data) == 0 {
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"
runtime.BrowserOpenURL(*wailsContext, getMyAnimeListCodeUrl)
@ -107,9 +110,10 @@ func (a *App) MyAnimeListLogin() {
a.handleMyAnimeListCallback(serverDone, verifier)
serverDone.Wait()
} else {
var expiresInConvertErr error
myAnimeListJwt.TokenType = string(tokenType.Data)
myAnimeListJwt.ExpiresIn, err = strconv.Atoi(string(expiresIn.Data))
if err != nil {
myAnimeListJwt.ExpiresIn, expiresInConvertErr = strconv.Atoi(string(expiresIn.Data))
if expiresInConvertErr != nil {
fmt.Println("unable to convert string to int in Login function")
}
myAnimeListJwt.AccessToken = string(accessToken.Data)
@ -171,7 +175,7 @@ func (a *App) handleMyAnimeListCallback(wg *sync.WaitGroup, verifier *CodeVerifi
go func() {
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)
}
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")
client := &http.Client{}
res, reserr := client.Do(response)
if reserr != nil {
res, resErr := client.Do(response)
if resErr != nil {
log.Printf("Failed at res, %s\n", err)
}
defer res.Body.Close()
returnedBody, err := io.ReadAll(res.Body)
if err != nil {
log.Printf("Could not read returned body, %s\n", err)
}
var post MyAnimeListJWT
err = json.Unmarshal(returnedBody, &post)
@ -257,14 +264,17 @@ func refreshMyAnimeListAuthorizationToken() {
response.Header.Add("Content-Type", "application/x-www-form-urlencoded")
client := &http.Client{}
res, reserr := client.Do(response)
if reserr != nil {
res, resErr := client.Do(response)
if resErr != nil {
log.Printf("Failed at res, %s\n", err)
}
defer res.Body.Close()
returnedBody, err := io.ReadAll(res.Body)
if err != nil {
log.Printf("Could not read returned body, %s\n", err)
}
err = json.Unmarshal(returnedBody, &myAnimeListJwt)
if err != nil {
@ -291,12 +301,9 @@ func refreshMyAnimeListAuthorizationToken() {
Title: "MyAnimeList Authorization",
Message: "It is now safe to close your browser tab",
})
if err != nil {
fmt.Println(err)
}
return
}
func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser {
@ -311,7 +318,6 @@ func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser {
req.Header.Add("myAnimeList-api-key", Environment.MAL_CLIENT_ID)
response, err := client.Do(req)
if err != nil {
log.Printf("Failed at request, %s\n", err)
return MyAnimeListUser{}
@ -333,13 +339,12 @@ func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser {
func (a *App) LogoutMyAnimeList() string {
if (MyAnimeListJWT{} != myAnimeListJwt) {
err := myAnimeListRing.Remove("MyAnimeListTokenType")
err = myAnimeListRing.Remove("MyAnimeListExpiresIn")
err = myAnimeListRing.Remove("MyAnimeListAccessToken")
err = myAnimeListRing.Remove("MyAnimeListRefreshToken")
if err != nil {
fmt.Println("MAL Logout Failed", err)
typeErr := myAnimeListRing.Remove("MyAnimeListTokenType")
expiresInErr := myAnimeListRing.Remove("MyAnimeListExpiresIn")
accessTokenErr := myAnimeListRing.Remove("MyAnimeListAccessToken")
refreshTokenErr := myAnimeListRing.Remove("MyAnimeListRefreshToken")
if typeErr != nil || expiresInErr != nil || accessTokenErr != nil || refreshTokenErr != nil {
fmt.Println("MAL Logout Failed")
}
myAnimeListJwt = MyAnimeListJWT{}
}

View File

@ -31,7 +31,6 @@ func SimklHelper(method string, url string, body interface{}) json.RawMessage {
req.Header.Add("simkl-api-key", Environment.SIMKL_CLIENT_ID)
resp, err := client.Do(req)
if err != nil {
fmt.Println("Errored when sending request to the server")
message, _ := json.Marshal(struct {
@ -47,7 +46,6 @@ func SimklHelper(method string, url string, body interface{}) json.RawMessage {
respBody, _ := io.ReadAll(resp.Body)
return respBody
}
func (a *App) SimklGetUserWatchlist() SimklWatchListType {
@ -62,7 +60,6 @@ func (a *App) SimklGetUserWatchlist() SimklWatchListType {
}
err := json.Unmarshal(respBody, &errCheck)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
@ -85,7 +82,6 @@ func (a *App) SimklGetUserWatchlist() SimklWatchListType {
}
func (a *App) SimklSyncEpisodes(anime SimklAnime, progress int) SimklAnime {
var episodes []Episode
var url string
var shows []SimklPostShow
@ -140,7 +136,7 @@ func (a *App) SimklSyncEpisodes(anime SimklAnime, progress int) SimklAnime {
func (a *App) SimklSyncRating(anime SimklAnime, rating int) SimklAnime {
var url string
var showWithRating = ShowWithRating{
showWithRating := ShowWithRating{
Title: anime.Show.Title,
Ids: Ids{
Simkl: anime.Show.Ids.Simkl,
@ -150,7 +146,7 @@ func (a *App) SimklSyncRating(anime SimklAnime, rating int) SimklAnime {
Rating: rating,
}
var showWithoutRating = ShowWithoutRating{
showWithoutRating := ShowWithoutRating{
Title: anime.Show.Title,
Ids: Ids{
Simkl: anime.Show.Ids.Simkl,
@ -197,7 +193,7 @@ func (a *App) SimklSyncRating(anime SimklAnime, rating int) SimklAnime {
func (a *App) SimklSyncStatus(anime SimklAnime, status string) SimklAnime {
url := "https://api.simkl.com/sync/add-to-list"
var show = SimklShowStatus{
show := SimklShowStatus{
Title: anime.Show.Title,
Ids: Ids{
Simkl: anime.Show.Ids.Simkl,
@ -266,7 +262,6 @@ func (a *App) SimklSearch(aniListAnime MediaList) SimklAnime {
if len(anime) == 0 {
url = "https://api.simkl.com/search/id?mal=" + strconv.Itoa(aniListAnime.Media.IDMal)
respBody = SimklHelper("GET", url, nil)
fmt.Println(string(respBody))
err = json.Unmarshal(respBody, &anime)
}
@ -300,7 +295,7 @@ func (a *App) SimklSyncRemove(anime SimklAnime) bool {
url := "https://api.simkl.com/sync/history/remove"
var showArray []SimklShowStatus
var singleShow = SimklShowStatus{
singleShow := SimklShowStatus{
Title: anime.Show.Title,
Ids: Ids{
Simkl: anime.Show.Ids.Simkl,
@ -349,5 +344,4 @@ func WatchListUpdate(anime SimklAnime) {
if !updated {
SimklWatchList.Anime = append(SimklWatchList.Anime, anime)
}
return
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
@ -28,10 +29,10 @@ var simklCtxShutdown, simklCancel = context.WithCancel(context.Background())
func (a *App) CheckIfSimklLoggedIn() bool {
if (SimklJWT{} == simklJwt) {
tokenType, err := simklRing.Get("SimklTokenType")
accessToken, err := simklRing.Get("SimklAccessToken")
scope, err := simklRing.Get("SimklScope")
if err != nil || len(accessToken.Data) == 0 {
tokenType, tokenTypeErr := simklRing.Get("SimklTokenType")
accessToken, accessTokenErr := simklRing.Get("SimklAccessToken")
scope, scopeErr := simklRing.Get("SimklScope")
if (tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil) || len(accessToken.Data) == 0 {
return false
} else {
simklJwt.TokenType = string(tokenType.Data)
@ -45,11 +46,11 @@ func (a *App) CheckIfSimklLoggedIn() bool {
}
func (a *App) SimklLogin() {
if a.CheckIfSimklLoggedIn() == false {
tokenType, err := simklRing.Get("SimklTokenType")
accessToken, err := simklRing.Get("SimklAccessToken")
scope, err := simklRing.Get("SimklScope")
if err != nil || len(accessToken.Data) == 0 {
if !a.CheckIfSimklLoggedIn() {
tokenType, tokenTypeErr := simklRing.Get("SimklTokenType")
accessToken, accessTokenErr := simklRing.Get("SimklAccessToken")
scope, scopeErr := simklRing.Get("SimklScope")
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
runtime.BrowserOpenURL(*wailsContext, getSimklCodeUrl)
@ -114,7 +115,7 @@ func (a *App) handleSimklCallback(wg *sync.WaitGroup) {
go func() {
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)
}
fmt.Println("Shutting down...")
@ -147,14 +148,17 @@ func getSimklAuthorizationToken(content string) SimklJWT {
response.Header.Add("Content-Type", "application/json")
client := &http.Client{}
res, reserr := client.Do(response)
if reserr != nil {
res, resErr := client.Do(response)
if resErr != nil {
log.Printf("Failed at res, %s\n", err)
}
defer res.Body.Close()
returnedBody, err := io.ReadAll(res.Body)
if err != nil {
log.Printf("Could not read returned body, %s\n.", err)
}
var post SimklJWT
err = json.Unmarshal(returnedBody, &post)
@ -177,7 +181,6 @@ func (a *App) GetSimklLoggedInUser() SimklUser {
req.Header.Add("simkl-api-key", Environment.SIMKL_CLIENT_ID)
response, err := client.Do(req)
if err != nil {
log.Printf("Failed at request, %s\n", err)
return SimklUser{}
@ -193,7 +196,6 @@ func (a *App) GetSimklLoggedInUser() SimklUser {
}
err = json.Unmarshal(respBody, &errCheck)
if err != nil {
log.Printf("Failed at unmarshal, %s\n", err)
}
@ -215,12 +217,12 @@ func (a *App) GetSimklLoggedInUser() SimklUser {
func (a *App) LogoutSimkl() string {
if (SimklJWT{} != simklJwt) {
err := simklRing.Remove("SimklTokenType")
err = simklRing.Remove("SimklAccessToken")
err = simklRing.Remove("SimklScope")
tokenTypeErr := simklRing.Remove("SimklTokenType")
accessTokenErr := simklRing.Remove("SimklAccessToken")
scopeErr := simklRing.Remove("SimklScope")
if err != nil {
fmt.Println("Simkl Logout Failed", err)
if tokenTypeErr != nil || accessTokenErr != nil || scopeErr != nil {
fmt.Println("Simkl Logout Failed")
}
simklJwt = SimklJWT{}
}

View File

@ -25,6 +25,7 @@ body:graphql {
media {
id
idMal
genres
tags {
id
name

View File

@ -12,7 +12,7 @@ post {
headers {
Accept: application/json
Content-Type: application/json
Content-Type: application/x-www-form-urlencoded
}
body:form-urlencoded {
@ -20,7 +20,7 @@ body:form-urlencoded {
client_id: {{ANILIST_APP_ID}}
client_secret: {{ANILIST_SECRET_TOKEN}}
redirect_uri: http://localhost:6734/callback
code: {{code}}
code: {{ANILIST_CODE}}
}
body:multipart-form {

View File

@ -9,7 +9,7 @@ vars {
}
vars:secret [
ANILIST_ACCESS_TOKEN,
code,
ANILIST_CODE,
SIMKL_AUTH_TOKEN,
MAL_CODE,
MAL_VERIFIER,

11
build/AniTrack.desktop Executable file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

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
View 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."

View File

@ -1,13 +1,16 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>AniTrack</title>
</head>
<body>
<div id="app"></div>
<script src="./src/main.ts" type="module"></script>
<script src="./node_modules/flowbite/dist/flowbite.js"></script>
</body>
<head>
<meta charset="UTF-8" />
<meta content="width=device-width, initial-scale=1.0" name="viewport" />
<title>AniTrack</title>
</head>
<body>
<div id="app"></div>
<script src="./src/main.ts" type="module"></script>
<script
src="./node_modules/flowbite/dist/flowbite.js"
type="module"
></script>
</body>
</html>

View File

@ -16,7 +16,7 @@
"postcss": "^8.4.45",
"svelte": "^4.0.0",
"svelte-check": "^3.4.3",
"svelte-headless-table": "^0.18.2",
"svelte-headless-table": "^0.18.3",
"svelte-preprocess": "^5.0.3",
"svelte-spa-router": "^4.0.1",
"tailwind-merge": "^2.5.2",

View File

@ -1,80 +1,93 @@
export interface AniListCurrentUserWatchList {
data: {
Page: {
pageInfo: {
total: number
perPage: number
currentPage: number
lastPage: number
hasNextPage: boolean
},
mediaList: MediaList[]
}
}
data: {
Page: {
pageInfo: {
total: number;
perPage: number;
currentPage: number;
lastPage: number;
hasNextPage: boolean;
};
mediaList: MediaList[];
};
};
}
export interface AniListGetSingleAnime {
data: {
MediaList: MediaList
}
data: {
MediaList: MediaList;
};
}
export interface MediaList {
id: number
mediaId: number
userId: number
media: {
id: number
idMal: number
title: {
romaji: string
english?: string
native: string
}
description: string
coverImage: {
large: string
}
season: string
seasonYear: number
status: string
episodes?: number
nextAiringEpisode?: {
airingAt: number
timeUntilAiring: number
episode: number
}
}
status: string
startedAt: {
year: number
month: number
day: number
}
completedAt: {
year?: number
month?: number
day?: number
}
notes?: string
progress: number
score: number
repeat: number
user: {
id: number
name: string
avatar: {
large: string
medium: string
}
statistics: {
anime: {
count: number
statuses: [{
status: string
count: number
}]
}
}
}
}
id: number;
mediaId: number;
userId: number;
media: {
id: number;
idMal: number;
title: {
romaji: string;
english?: string;
native: string;
};
description: string;
coverImage: {
large: string;
};
season: string;
seasonYear: number;
status: string;
episodes?: number;
nextAiringEpisode?: {
airingAt: number;
timeUntilAiring: number;
episode: number;
};
tags: [
{
id: number;
name: string;
description: string;
rank: number;
isMediaSpoiler: boolean;
isAdult: boolean;
},
];
isAdult: boolean;
};
status: string;
startedAt: {
year: number;
month: number;
day: number;
};
completedAt: {
year?: number;
month?: number;
day?: number;
};
notes?: string;
progress: number;
score: number;
repeat: number;
user: {
id: number;
name: string;
avatar: {
large: string;
medium: string;
};
statistics: {
anime: {
count: number;
statuses: [
{
status: string;
count: number;
},
];
};
};
};
}

View File

@ -8,10 +8,12 @@
simklLoggedIn,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { push } from "svelte-spa-router";
import { Button } from "flowbite-svelte";
import type { AniListGetSingleAnime } from "../anilist/types/AniListCurrentUserWatchListType";
import Rating from "./Rating.svelte";
import convertAniListDateToString from "../helperFunctions/convertAniListDateToString";
import {
convertAniListDateToString,
convertAniListDateToDate,
} from "../helperFunctions/convertAniListDateIn";
import AnimeTable from "./AnimeTable.svelte";
import type {
MALAnime,
@ -25,7 +27,7 @@
StatusOptions,
} from "../helperTypes/StatusTypes";
import type { AniListUpdateVariables } from "../anilist/types/AniListTypes";
import convertDateStringToAniList from "../helperFunctions/convertDateStringToAniList";
import { convertDateToAniList } from "../helperFunctions/convertDateToAniList";
import {
AniListDeleteEntry,
AniListUpdateEntry,
@ -38,6 +40,9 @@
} from "../../wailsjs/go/main/App";
import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import Datepicker from "./Datepicker.svelte";
import { Badge } from "flowbite-svelte";
const re = /^([0-9]{4})-([0-9]{2})-([0-9]{2})/;
let isAniListLoggedIn: boolean;
let isMalLoggedIn: boolean;
@ -78,10 +83,10 @@
(option) =>
currentAniListAnime.data.MediaList.status === option.aniList,
)[0];
const startedAtDate = convertAniListDateToString(
let startedAtDate: Date | null = convertAniListDateToDate(
currentAniListAnime.data.MediaList.startedAt,
);
const completedAtDate = convertAniListDateToString(
let completedAtDate: Date | null = convertAniListDateToDate(
currentAniListAnime.data.MediaList.completedAt,
);
@ -103,19 +108,34 @@
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({
id: `m-${currentMalAnime.id}`,
title: currentMalAnime.title,
service: "MyAnimeList",
progress: currentMalAnime.my_list_status.num_episodes_watched,
status: currentMalAnime.my_list_status.status,
startedAt: currentMalAnime.my_list_status.start_date,
completedAt: currentMalAnime.my_list_status.finish_date,
startedAt: startDate,
completedAt: finishDate,
score: currentMalAnime.my_list_status.score,
repeat: currentMalAnime.my_list_status.num_times_rewatched,
notes: currentMalAnime.my_list_status.comments,
});
}
if (isSimklLoggedIn && Object.keys(currentSimklAnime).length > 0)
AddAnimeServiceToTable({
@ -137,8 +157,8 @@
rating: number;
episodes: number;
status: StatusOption;
startedAt: string;
completedAt: string;
startedAt: Date | null;
completedAt: Date | null;
repeat: number;
notes: string;
} = {
@ -150,8 +170,8 @@
mal: "",
simkl: "",
},
startedAt: "",
completedAt: "",
startedAt: null,
completedAt: null,
repeat: 0,
notes: "",
};
@ -188,14 +208,16 @@
score: submitData.rating,
repeat: submitData.repeat,
notes: submitData.notes,
startedAt: convertDateStringToAniList(submitData.startedAt),
completedAt: convertDateStringToAniList(submitData.completedAt),
startedAt: convertDateToAniList(startedAtDate),
completedAt: convertDateToAniList(completedAtDate),
};
await AniListUpdateEntry(body).then(
(value: AniListGetSingleAnime) => {
// in future when you inevitably add tags to typescript, until Anilist fixes the api bug
// where tags break the SaveMediaListEntry return, you'll want to use this delete line
// delete value.data.MediaList.media.tags
/* TODO in future when you inevitably add tags to typescript, until Anilist fixes the api bug
where tags break the SaveMediaListEntry return, you'll want to use this delete line
delete value.data.MediaList.media.tags */
value.data.MediaList.media.tags =
currentAniListAnime.data.MediaList.media.tags;
aniListAnime.update((newValue) => {
newValue = value;
return newValue;
@ -244,6 +266,20 @@
value.my_list_status.comments = malAnimeReturn.comments;
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({
id: `m-${currentMalAnime.id}`,
title: currentMalAnime.title,
@ -251,8 +287,8 @@
progress:
currentMalAnime.my_list_status.num_episodes_watched,
status: currentMalAnime.my_list_status.status,
startedAt: currentMalAnime.my_list_status.start_date,
completedAt: currentMalAnime.my_list_status.finish_date,
startedAt: startDate,
completedAt: finishDate,
score: currentMalAnime.my_list_status.score,
repeat: currentMalAnime.my_list_status
.num_times_rewatched,
@ -397,6 +433,20 @@
submitSuccess.set(true);
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>
<form on:submit|preventDefault={handleSubmit} class="container pt-3 pb-10">
@ -423,24 +473,118 @@
class="text-left block mb-2 text-sm font-medium text-white"
>Episode Progress</label
>
<input
type="number"
name="episodes"
min="0"
max={currentAniListAnime.data.MediaList.media.episodes}
id="episodes"
class="border {currentAniListAnime.data.MediaList
.progress < 0 ||
(currentAniListAnime.data.MediaList.media.episodes >
0 &&
currentAniListAnime.data.MediaList.progress >
currentAniListAnime.data.MediaList.media
.episodes)
? '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"
bind:value={currentAniListAnime.data.MediaList.progress}
required
/>
<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;
}
}}
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"
>
<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
type="number"
name="episodes"
min="0"
{max}
id="episodes"
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
{currentAniListAnime.data.MediaList.progress < 0 ||
(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 -
1)
? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500'
: '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
}
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();
}
}}
class="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>
/ {currentAniListAnime.data.MediaList.media
.nextAiringEpisode.episode !== 0
@ -448,15 +592,17 @@
.nextAiringEpisode.episode - 1
: currentAniListAnime.data.MediaList.media.episodes}
</div>
<div>
of {currentAniListAnime.data.MediaList.media.episodes}
</div>
{#if currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode !== 0}
<div>
of {currentAniListAnime.data.MediaList.media
.episodes}
</div>
{/if}
</div>
<div>
<label
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
>
<select
@ -482,33 +628,16 @@
class="text-left block mb-2 text-sm font-medium text-white"
>Date Started</label
>
<div class="relative max-w-sm">
<div
class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none"
>
<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"
/>
</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>
<Datepicker
bind:value={startedAtDate}
color="slate"
dateFormat={{
year: "numeric",
month: "2-digit",
day: "2-digit",
}}
showActionButtons
/>
</div>
<div>
<label
@ -516,33 +645,16 @@
class="text-left block mb-2 text-sm font-medium text-white"
>Date Completed</label
>
<div class="relative max-w-sm">
<div
class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none"
>
<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"
/>
</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>
<Datepicker
bind:value={completedAtDate}
color="slate"
dateFormat={{
year: "numeric",
month: "2-digit",
day: "2-digit",
}}
showActionButtons
/>
</div>
<div>
<label
@ -585,63 +697,16 @@
</div>
</div>
</div>
<div id="external-data">
<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 class="flex mb-4 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-end"
>
<Button
<button
disabled={isSubmitting}
id="sync-button"
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-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-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
@ -665,23 +730,123 @@
/>
</svg>
Sync Changes
</Button>
<Button
</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 dark:bg-gray-800 dark:text-white
dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700"
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>
</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>
<h3 class="text-2xl">Summary</h3>
<p>{@html currentAniListAnime.data.MediaList.media.description}</p>
<div class="flex m-5">
<div>
<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-40"
>{tag.name}</Badge
>
</div>
{/each}
</div>
</div>
<div class="ml-5">
<h3 class="text-2xl">Summary</h3>
<p class="rounded border border-gray-700 p-2 mt-2">
{@html currentAniListAnime.data.MediaList.media.description}
</p>
</div>
</div>
</form>

View File

@ -4,19 +4,19 @@
createTable,
Render,
Subscribe,
} from "svelte-headless-table"
} from "svelte-headless-table";
// @ts-ignore
import { addSortBy } from "svelte-headless-table/plugins"
import { tableItems } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"
import WebsiteLink from "./WebsiteLink.svelte"
import { addSortBy } from "svelte-headless-table/plugins";
import { tableItems } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import WebsiteLink from "./WebsiteLink.svelte";
//when adding sort here is code { sort: addSortBy() }
const table = createTable(tableItems, { sort: addSortBy() })
const table = createTable(tableItems, { sort: addSortBy() });
const columns = table.createColumns([
table.column({
header: "Service Id",
cell: ({ value }) => createRender(WebsiteLink, {id: value}),
cell: ({ value }) => createRender(WebsiteLink, { id: value }),
accessor: "id",
}),
table.column({
@ -55,11 +55,11 @@
header: "Notes",
accessor: "notes",
}),
])
]);
//add pluginStates when add sort back
const { headerRows, rows, tableAttrs, tableBodyAttrs } =
table.createViewModel(columns)
table.createViewModel(columns);
</script>
<div class="relative overflow-x-auto rounded-lg mb-5">

View 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 = '';
-->

View 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 };

View File

@ -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')
}

View File

@ -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])
}
}

View 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 };

View File

@ -202,8 +202,7 @@ export namespace main {
export class MALAnime {
id: id;
title: title;
// Go type: struct { Large string "json:\"large\" json:\"large\""; Medium string "json:\"medium\" json:\"medium\"" }
main_picture: any;
main_picture: mainPicture;
alternative_titles: alternativeTitles;
start_date: startDate;
end_date: endDate;
@ -242,7 +241,7 @@ export namespace main {
if ('string' === typeof source) source = JSON.parse(source);
this.id = source["id"];
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.start_date = source["start_date"];
this.end_date = source["end_date"];
@ -314,7 +313,7 @@ export namespace main {
}
}
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;
static createFrom(source: any = {}) {
@ -323,27 +322,9 @@ export namespace main {
constructor(source: any = {}) {
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"];
}
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 {
status: status;
@ -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 {
// 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: any;
node: node;
list_status: listStatus;
static createFrom(source: any = {}) {
@ -637,27 +617,9 @@ export namespace struct { Node struct { Id int "json:\"id\" ts_type:\"id\""; Tit
constructor(source: any = {}) {
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"];
}
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;
}
}
}

View File

@ -134,7 +134,7 @@ export function WindowIsFullscreen(): Promise<boolean>;
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
// 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)
// Gets the width and height of the window.

24
go.mod
View File

@ -1,49 +1,49 @@
module AniTrack
go 1.23
go 1.24.0
require (
github.com/99designs/keyring v1.2.2
github.com/tidwall/gjson v1.18.0
github.com/wailsapp/wails/v2 v2.9.2
github.com/wailsapp/wails/v2 v2.10.1
)
require (
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
github.com/bep/debounce v1.2.1 // indirect
github.com/danieljoos/wincred v1.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/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
github.com/labstack/echo/v4 v4.12.0 // indirect
github.com/labstack/echo/v4 v4.13.3 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
github.com/leaanthony/gosod v1.0.4 // indirect
github.com/leaanthony/slicer v1.6.0 // 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/mtibben/percent v0.2.1 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // 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/pretty v1.2.1 // indirect
github.com/tkrajina/go-reflector v0.5.8 // indirect
github.com/valyala/bytebufferpool v1.0.0 // 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
golang.org/x/crypto v0.28.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.0 // indirect
golang.org/x/term v0.25.0 // indirect
golang.org/x/text v0.19.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
)
// replace github.com/wailsapp/wails/v2 v2.9.1 => /home/nymusicman/go/pkg/mod

53
go.sum
View File

@ -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/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dvsekhvalnov/jose2go v1.7.0 h1:bnQc8+GMnidJZA8zc6lLEAb4xNrIqHwO+9TzqvtQZPo=
github.com/dvsekhvalnov/jose2go v1.7.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU=
github.com/dvsekhvalnov/jose2go v1.8.0 h1:LqkkVKAlHFfH9LOEl5fe4p/zL02OhWE7pCufMBG2jLA=
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/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
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/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
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/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
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.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
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.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs=
@ -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.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
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.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
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/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
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/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
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.16/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
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/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.9.2 h1:Xb5YRTos1w5N7DTMyYegWaGukCP2fIaX9WF21kPPF2k=
github.com/wailsapp/wails/v2 v2.9.2/go.mod h1:uehvlCwJSFcBq7rMCGfk4rxca67QQGsbg5Nm4m9UnBs=
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
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.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
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-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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
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.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -34,9 +34,9 @@ func main() {
app,
},
Linux: &linux.Options{
Icon: []byte("./build/appicon.png"),
Icon: []byte("./build/AniTrack.png"),
WindowIsTranslucent: false,
WebviewGpuPolicy: linux.WebviewGpuPolicyAlways,
WebviewGpuPolicy: linux.WebviewGpuPolicyNever,
ProgramName: "AniTrack",
},
})

View File

@ -0,0 +1,79 @@
# @name AniChart
POST https://graphql.anilist.co
Accept: applicaton/json
X-REQUEST-TYPE: Graphql
query ($page: Int, $perPage: Int, $airingAt_greater:Int) {
Page(page: $page, perPage: $perPage) {
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
airingSchedules(airingAt_greater:$airingAt_greater){
id
airingAt
timeUntilAiring
episode
mediaId
media{
id
title{
english
romaji
native
}
type
format
status
startDate{
year
month
day
}
endDate{
year
month
day
}
season
seasonYear
episodes
duration
coverImage{
medium
large
color
extraLarge
}
bannerImage
genres
averageScore
meanScore
popularity
trending
favourites
tags{
id
name
description
category
rank
isGeneralSpoiler
isMediaSpoiler
isAdult
}
isAdult
}
}
}
}
{
"page": 50,
"perPage": 20,
"airingAt_greater": 1730260800
}

View File

@ -0,0 +1,83 @@
# @name AniList Item
POST https://graphql.anilist.co
Accept: applicaton/json
X-REQUEST-TYPE: Graphql
Authorization: Bearer {{ANILIST_ACCESS_TOKEN}}
query ($userId: Int, $mediaId: Int, $listType: MediaType) {
MediaList(mediaId: $mediaId, userId: $userId, type: $listType) {
id
mediaId
userId
media {
id
idMal
tags {
id
name
description
rank
isMediaSpoiler
isAdult
}
title {
romaji
english
native
}
description
coverImage {
large
}
season
seasonYear
status
episodes
nextAiringEpisode {
airingAt
timeUntilAiring
episode
}
isAdult
}
status
startedAt {
year
month
day
}
completedAt {
year
month
day
}
notes
progress
score
repeat
user {
id
name
avatar {
large
medium
}
statistics {
anime {
count
statuses {
status
count
}
}
}
}
}
}
{
"userId": 413504,
"mediaId": 170998,
"listType": "ANIME"
}

View File

@ -0,0 +1,70 @@
# @name AniList MediaList User Query
POST https://graphql.anilist.co
Accept: applicaton/json
X-REQUEST-TYPE: Graphql
query(
$page: Int
$perPage: Int
$userId: Int
$listType: MediaType
$status: MediaListStatus
) {
Page(page: $page, perPage: $perPage) {
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
mediaList(userId: $userId, type: $listType, status: $status) {
id
mediaId
userId
media {
id
idMal
title {
romaji
english
native
}
description
coverImage {
large
}
season
seasonYear
episodes
}
status
notes
progress
score
repeat
user {
id
statistics {
anime {
count
statuses {
status
count
}
}
}
}
}
}
}
{
"page": 1,
"perPage": 20,
"userId": 413504,
"listType": "ANIME",
"status": "CURRENT"
}

View File

@ -0,0 +1,44 @@
# @name AniList Search
POST https://graphql.anilist.co
Accept: applicaton/json
X-REQUEST-TYPE: Graphql
query ($search: String!, $listType: MediaType) {
Page (page: 1, perPage: 100) {
pageInfo {
total
currentPage
lastPage
hasNextPage
perPage
}
media (search: $search, type: $listType) {
id
idMal
title {
romaji
english
native
}
description
coverImage {
large
}
season
seasonYear
status
episodes
nextAiringEpisode{
airingAt
timeUntilAiring
episode
}
}
}
}
{
"search": "dan-da-dan",
"listType": "ANIME"
}

View File

@ -0,0 +1,93 @@
# @name GetAniListUserWatchList
POST https://graphql.anilist.co
Accept: applicaton/json
X-REQUEST-TYPE: Graphql
query (
$page: Int
$perPage: Int
$userId: Int
$listType: MediaType
$status: MediaListStatus
$sort: [MediaListSort]
) {
Page(page: $page, perPage: $perPage) {
pageInfo {
total
perPage
currentPage
lastPage
hasNextPage
}
mediaList(userId: $userId, type: $listType, status: $status, sort: $sort) {
id
mediaId
userId
media {
id
idMal
title {
romaji
english
native
}
description
coverImage {
large
}
season
seasonYear
status
episodes
nextAiringEpisode {
airingAt
timeUntilAiring
episode
}
}
status
startedAt {
year
month
day
}
completedAt {
year
month
day
}
notes
progress
score
repeat
user {
id
name
avatar {
large
medium
}
statistics {
anime {
count
statuses {
status
count
}
}
}
}
}
}
}
{
"page": 1,
"perPage": 20,
"userId": 413504,
"listType": "ANIME",
"status": "CURRENT",
"sort": "UPDATED_TIME_DESC"
}

View File

@ -0,0 +1,3 @@
# @name GetAuthorizationToken
GET https://anilist.co/api/v2/oauth/authorize?client_id={{ANILIST_APP_ID}}&redirect_uri=http://localhost:6734/callback&response_type=code

View File

@ -0,0 +1,11 @@
# @name Load AniList Oauth Token
POST https://anilist.co/api/v2/oauth/token
Content-Type: application/x-www-form-urlencoded
Accept: application/json
grant_type=authorization_code
client_id={{ANILIST_APP_ID}}
client_secret={{ANILIST_SECRET_ID}}
redirect_uri=http://localhost:6734/callback
code={{ANILIST_CODE}}

View File

@ -0,0 +1,76 @@
# @name AniList Change Episode Watched
POST https://graphql.anilist.co
Content-Type: applicaton/json
Accept: applicaton/json
X-REQUEST-TYPE: Graphql
Authorization: Bearer {{ANILIST_ACCESS_TOKEN}}
mutation($mediaId:Int, $progress:Int, $status:MediaListStatus){
SaveMediaListEntry(mediaId:$mediaId, progress:$progress, status:$status){
id
mediaId
userId
media {
id
idMal
title {
romaji
english
native
}
description
coverImage {
large
}
season
seasonYear
status
episodes
nextAiringEpisode {
airingAt
timeUntilAiring
episode
}
isAdult
}
status
startedAt{
year
month
day
}
completedAt{
year
month
day
}
notes
progress
score
repeat
user {
id
name
avatar{
large
medium
}
statistics{
anime{
count
statuses{
status
count
}
}
}
}
}
}
{
"mediaId": 169417,
"progress": 12,
"status":"COMPLETED"
}

View File

@ -0,0 +1,19 @@
# @name AniList Change Status
POST https://graphql.anilist.co
Content-Type: applicaton/json
Accept: applicaton/json
X-REQUEST-TYPE: Graphql
Authorization: Bearer {{ANILIST_ACCESS_TOKEN}}
mutation($mediaId:Int, $status:MediaListStatus){
SaveMediaListEntry(mediaId:$mediaId, status:$status){
id
status
}
}
{
"mediaId": 1,
"status": "CURRENT"
}

View File

@ -0,0 +1,65 @@
# @name AniList Change Count
POST https://graphql.anilist.co
Content-Type: applicaton/json
Accept: applicaton/json
X-REQUEST-TYPE: Graphql
Authorization: Bearer {{ANILIST_ACCESS_TOKEN}}
mutation (
$mediaId: Int
$progress: Int
$status: MediaListStatus
$score: Float
$repeat: Int
$notes: String
$startedAt: FuzzyDateInput
$completedAt: FuzzyDateInput
) {
SaveMediaListEntry(
mediaId: $mediaId
progress: $progress
status: $status
score: $score
repeat: $repeat
notes: $notes
startedAt: $startedAt
completedAt: $completedAt
) {
mediaId
progress
status
score
repeat
notes
startedAt {
year
month
day
}
completedAt {
year
month
day
}
}
}
{
"mediaId": 170998,
"progress": 5,
"status": "CURRENT",
"score": 9.0,
"repeat": 0,
"notes": ",malSync::eyJ1IjoiaHR0cHM6Ly93d3cuY3J1bmNoeXJvbGwuY29tL3Nlcmllcy9HVkRIWDg1Wk4vI3NlYXNvbj1HNjNWQzJHUUsiLCJwIjoiIn0=::",
"startedAt": {
"year": 2024,
"month": 7,
"day": 10
},
"completedAt": {
"year": 0,
"month": 0,
"day": 0
}
}

View File

@ -0,0 +1,17 @@
# @name AniList Delete Media
POST https://graphql.anilist.co
Content-Type: applicaton/json
Accept: applicaton/json
X-REQUEST-TYPE: Graphql
Authorization: Bearer {{ANILIST_ACCESS_TOKEN}}
mutation ($id: Int) {
DeleteMediaListEntry(id: $id) {
deleted
}
}
{
"id": 430978266
}

View File

@ -0,0 +1,6 @@
# @name Get AnimeList
GET https://api.myanimelist.net/v2/users/{{MAL_USER}}/animelist?fields=list_status&status=watching&limit=1000
Content-Type application/x-www-form-urlencoded
Accept: application/json
Authorization: Bearer {{MAL_ACCESS_TOKEN}}

View File

@ -0,0 +1,12 @@
# @name Get Authorization
POST https://myanimelist.net/v1/oauth2/token
Content-Type: application/x-www-form-urlencoded
Accept: application/json
grant_type=authorization_code&
client_id={{MAL_CLIENT_ID}}&
client_secret={{MAL_CLIENT_SECRET}}&
redirect_uri=http://localhost:6734/callback&
code={{MAL_CODE}}&
code_verifier={{MAL_VERIFIER}}

View File

@ -0,0 +1,5 @@
# @name Get Single Anime
GET https://api.myanimelist.net/v2/anime/57380?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
Accept: application/json
Authorization: Bearer {{MAL_ACCESS_TOKEN}}

View File

@ -0,0 +1,5 @@
# @name MAL Oauth Page
GET https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id={{MAL_CLIENT_ID}}&redirect_uri={{MAL_CALLBACK_URI}}
cookie MALSESSIONID=5ad688aafb78239bfd84752752ce193f; MALHLOGSESSID=632f67c3955267b4e57fc3d74b373ebb
Accept: application/json

View File

@ -0,0 +1,8 @@
# @name Update Anime Status
PATCH https://api.myanimelist.net/v2/anime/50205/my_list_status
Content-Type: application/x-www-form-urlencoded
Accept: application/json
Authorization: Bearer {{MAL_ACCESS_TOKEN}}
num_watched_episodes=3

3
rest/Simkl/Get Code.http Normal file
View File

@ -0,0 +1,3 @@
# @name Get Code
GET https://simkl.com/oauth/authorize?response_type=code&client_id={{SIMKL_CLIENT_ID}}&redirect_uri=http://localhost:6734/callback

View File

@ -0,0 +1,5 @@
# @name Get Anime Full Info
GET https://api.simkl.com/anime/40084?extended=full
Accept application/json
simkl-api-key {{SIMKL_CLIENT_ID}}

View File

@ -0,0 +1,7 @@
# @name GetUser WatchList
GET https://api.simkl.com/sync/all-items/anime/
Content-Type application/json
Accept application/json
simkl-api-key {{SIMKL_CLIENT_ID}}
Authorization Bearer {{SIMKL_AUTH_TOKEN}}

View File

@ -0,0 +1,5 @@
# @name Search By MALID to Get Simkl ID
GET https://api.simkl.com/search/id?anilist=174576
Accept application/json
simkl-api-key {{SIMKL_CLIENT_ID}}

View File

@ -0,0 +1,17 @@
# @name Delete Entry
GET https://api.simkl.com/sync/history/remove
Content-Type application/json
Accept application/json
simkl-api-key {{SIMKL_CLIENT_ID}}
Authorization Bearer {{SIMKL_AUTH_TOKEN}}
{
"shows": [
{
"ids": {
"simkl": 909121
}
}
]
}

View File

@ -0,0 +1,40 @@
# @name Update Episode
GET https://api.simkl.com/sync/history
Content-Type application/json
Accept application/json
simkl-api-key {{SIMKL_CLIENT_ID}}
Authorization Bearer {{SIMKL_AUTH_TOKEN}}
{
"shows": [
{
"title": "Ramen Aka Neko",
"ids": {
"simkl": 2307708,
"mal": "57325",
"anilist": "170998"
},
"episodes": [
{
"number": 1
},
{
"number": 2
},
{
"number": 3
},
{
"number": 4
},
{
"number": 5
},
{
"number": 6
}
]
}
]
}

View File

@ -0,0 +1,12 @@
# @name SimklGetAuthorizationToken
POST https://api.simkl.com/oauth/token
Content-Type application/json
{
"grant_type": "authorization_code",
"client_id": "{{SIMKL_CLIENT_ID}}",
"client_secret": "{{SIMKL_CLIENT_SECRET}}",
"redirect_uri": "http://localhost:6734/callback",
"code": {{SIMKL_CODE}}
}

19
rest/http-client.env.json Normal file
View File

@ -0,0 +1,19 @@
{
"$schema": "https://raw.githubusercontent.com/mistweaverco/kulala.nvim/main/schemas/http-client.env.schema.json",
"dev": {
"ANILIST_ACCESS_TOKEN": "",
"ANILIST_APP_ID": "",
"ANILIST_SECRET": "",
"ANILSIT_CODE": "",
"MAL_ACCESS_TOKEN": "",
"MAL_CLIENT_ID": "",
"MAL_CLIENT_SECRET": "",
"MAL_CODE": "",
"MAL_USER": "",
"MAL_VERIFIER": "",
"SIMKL_AUTH_TOKEN": "",
"SIMKL_CODE": "",
"SIMKL_CLIENT_ID": "",
"SIMKL_CLIENT_SECRET": ""
}
}

View File

@ -12,6 +12,6 @@
},
"info": {
"productName": "AniTrack",
"productVersion": "0.1.2"
"productVersion": "0.2.0"
}
}