7 Commits

Author SHA1 Message Date
3bfb31f8bf docs: update AniList search query in Bruno collection
Add the genre field to the AniList Search GraphQL query in the Bruno API
collection. This keeps the API documentation collection in sync with the
application's query structure, allowing for testing and verification of
genre data retrieval from the AniList API.
2026-03-19 20:55:48 -04:00
4739fb4344 build: update Wails generated models for genres support
Regenerate the Wails TypeScript models to include the new Genres field
in the MediaList type definition. This is an auto-generated file that
reflects the updated Go backend type structure with the genres []string
field added to the media object.
2026-03-19 20:55:48 -04:00
f4382304df feat(frontend): add genre display UI and enhance link component
- Anime.svelte: Add genre display section with clickable badges that link
  to AniList search results for each genre. Genres are now displayed above
  the existing tags section with consistent styling.

- WebsiteLink.svelte: Enhance component to support custom URLs via the `url`
  export parameter. Previously, the component only generated URLs based on
  service prefixes (a-, m-, s-). Now it accepts a direct URL parameter for
  flexible linking to AniList searches and other external resources.

These changes provide users with an improved browsing experience by making
genres interactive and easily searchable.
2026-03-19 20:55:21 -04:00
4400dfd637 feat(frontend): update TypeScript types for AniList genres support
Update the AniListCurrentUserWatchListType TypeScript interface to include
the genres field as a string array, matching the updated backend Go type
definition. This ensures type safety and proper IDE autocomplete when
working with genre data in the frontend.
2026-03-19 20:54:39 -04:00
b90d8eb2d3 feat(backend): add genres support to AniList integration
Add the `genres` field to AniList GraphQL queries and type definitions:
- Add genres field to GetAniListItem query for fetching single anime details
- Add genres field to AniListSearch query for search results
- Add genres field to GetAniListUserWatchingList query for user's watch list
- Update MediaList type definition to include Genres []string field

This enhancement allows the application to retrieve and display anime genre
information from the AniList API, providing users with better categorization
and discovery capabilities.
2026-03-19 20:54:39 -04:00
1f796189b4 moved user information into a userstore and updated code that touches it 2025-12-24 11:49:09 -05:00
18daf41bf9 moved user information into a userstore and updated code that touches it 2025-12-24 11:45:59 -05:00
60 changed files with 2354 additions and 2047 deletions

3
.gitignore vendored
View File

@@ -33,6 +33,3 @@ environment.go
# REST (http files) # REST (http files)
http-client.private.env.json http-client.private.env.json
# Build artifacts
build/*.tar.gz

View File

@@ -3,7 +3,6 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
@@ -184,7 +183,7 @@ func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
return post return post
} }
func (a *App) AniListSearch(query string) (interface{}, error) { func (a *App) AniListSearch(query string) any {
type Variables struct { type Variables struct {
Search string `json:"search"` Search string `json:"search"`
ListType string `json:"listType"` ListType string `json:"listType"`
@@ -243,20 +242,18 @@ func (a *App) AniListSearch(query string) (interface{}, error) {
ListType: "ANIME", ListType: "ANIME",
}, },
} }
returnedBody, status := AniListQuery(body, false) returnedBody, _ := AniListQuery(body, false)
if status != "200 OK" {
return nil, fmt.Errorf("API search failed with status: %s", status)
}
var post interface{} var post interface{}
err := json.Unmarshal(returnedBody, &post) err := json.Unmarshal(returnedBody, &post)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return nil, fmt.Errorf("Failed to parse search results")
} }
return post, nil
return post
} }
func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) (AniListCurrentUserWatchList, error) { func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) AniListCurrentUserWatchList {
user := a.GetAniListLoggedInUser() user := a.GetAniListLoggedInUser()
type Variables struct { type Variables struct {
Page int `json:"page"` Page int `json:"page"`
@@ -407,15 +404,11 @@ func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) (An
err := json.Unmarshal(returnedBody, &badPost) err := json.Unmarshal(returnedBody, &badPost)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return post, fmt.Errorf("API authentication error")
} }
return post, fmt.Errorf("AniList API error: %s", badPost.Errors[0].Message) log.Fatal(badPost.Errors[0].Message)
}
if status != "200 OK" {
return post, fmt.Errorf("API request failed with status: %s", status)
} }
return post, nil return post
} }
func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSingleAnime { func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSingleAnime {

View File

@@ -11,19 +11,10 @@ import (
"strings" "strings"
) )
func MALHelper(method string, malUrl string, body url.Values) (json.RawMessage, string, error) { func MALHelper(method string, malUrl string, body url.Values) (json.RawMessage, string) {
client := &http.Client{} client := &http.Client{}
req, err := http.NewRequest(method, malUrl, strings.NewReader(body.Encode())) req, _ := http.NewRequest(method, malUrl, strings.NewReader(body.Encode()))
if err != nil {
message, _ := json.Marshal(struct {
Message string `json:"message"`
}{
Message: "Failed to create request: " + err.Error(),
})
return message, "", fmt.Errorf("failed to create request: %w", err)
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded") req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Authorization", "Bearer "+myAnimeListJwt.AccessToken) req.Header.Add("Authorization", "Bearer "+myAnimeListJwt.AccessToken)
@@ -31,57 +22,47 @@ func MALHelper(method string, malUrl string, body url.Values) (json.RawMessage,
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
fmt.Println("Errored when sending request to the server")
message, _ := json.Marshal(struct { message, _ := json.Marshal(struct {
Message string `json:"message"` Message string `json:"message" ts_type:"message"`
}{ }{
Message: "Network error: " + err.Error(), Message: "Errored when sending request to the server" + err.Error(),
}) })
return message, "", fmt.Errorf("network error: %w", err)
return message, resp.Status
} }
defer resp.Body.Close() defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body) respBody, _ := io.ReadAll(resp.Body)
if err != nil {
return nil, resp.Status, fmt.Errorf("failed to read response: %w", err)
}
if resp.Status == "401 Unauthorized" { if resp.Status == "401 Unauthorized" {
refreshMyAnimeListAuthorizationToken() refreshMyAnimeListAuthorizationToken()
return MALHelper(method, malUrl, body) MALHelper(method, malUrl, body)
} }
if resp.Status != "200 OK" && resp.Status != "201 Created" && resp.Status != "202 Accepted" { return respBody, resp.Status
return respBody, resp.Status, fmt.Errorf("API returned status: %s", resp.Status)
}
return respBody, resp.Status, nil
} }
func (a *App) GetMyAnimeList(count int) (MALWatchlist, error) { func (a *App) GetMyAnimeList(count int) MALWatchlist {
limit := strconv.Itoa(count) limit := strconv.Itoa(count)
user := a.GetMyAnimeListLoggedInUser() user := a.GetMyAnimeListLoggedInUser()
malUrl := "https://api.myanimelist.net/v2/users/" + user.Name + "/animelist?fields=list_status&status=watching&limit=" + limit malUrl := "https://api.myanimelist.net/v2/users/" + user.Name + "/animelist?fields=list_status&status=watching&limit=" + limit
var malList MALWatchlist var malList MALWatchlist
respBody, resStatus, err := MALHelper("GET", malUrl, nil) respBody, resStatus := MALHelper("GET", malUrl, nil)
if err != nil {
return malList, fmt.Errorf("failed to get MAL watchlist: %w", err)
}
if resStatus == "200 OK" { if resStatus == "200 OK" {
err := json.Unmarshal(respBody, &malList) err := json.Unmarshal(respBody, &malList)
if err != nil { if err != nil {
log.Printf("Failed to unmarshal json response, %s\n", err) log.Printf("Failed to unmarshal json response, %s\n", err)
return malList, fmt.Errorf("failed to parse response: %w", err)
} }
} }
return malList, nil return malList
} }
func (a *App) MyAnimeListUpdate(anime MALAnime, update MALUploadStatus) (MalListStatus, error) { func (a *App) MyAnimeListUpdate(anime MALAnime, update MALUploadStatus) MalListStatus {
if update.NumTimesRewatched >= 1 { if update.NumTimesRewatched >= 1 {
update.IsRewatching = true update.IsRewatching = true
} else { } else {
@@ -97,52 +78,39 @@ func (a *App) MyAnimeListUpdate(anime MALAnime, update MALUploadStatus) (MalList
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(anime.Id) + "/my_list_status" malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(anime.Id) + "/my_list_status"
var status MalListStatus var status MalListStatus
respBody, respStatus, err := MALHelper("PATCH", malUrl, body) respBody, respStatus := MALHelper("PATCH", malUrl, body)
if err != nil {
return status, fmt.Errorf("failed to update MAL entry: %w", err)
}
if respStatus == "200 OK" { if respStatus == "200 OK" {
err := json.Unmarshal(respBody, &status) err := json.Unmarshal(respBody, &status)
if err != nil { if err != nil {
log.Printf("Failed to unmarshal json response, %s\n", err) log.Printf("Failed to unmarshal json response, %s\n", err)
return status, fmt.Errorf("failed to parse response: %w", err)
} }
} }
return status, nil
return status
} }
func (a *App) GetMyAnimeListAnime(id int) (MALAnime, error) { func (a *App) GetMyAnimeListAnime(id int) MALAnime {
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(id) + "?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,genres,created_at,updated_at,media_type,status,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,recommendations,studios,statistics" malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(id) + "?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,genres,created_at,updated_at,media_type,status,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,recommendations,studios,statistics"
respBody, respStatus := MALHelper("GET", malUrl, nil)
var malAnime MALAnime var malAnime MALAnime
respBody, respStatus, err := MALHelper("GET", malUrl, nil)
if err != nil {
return malAnime, fmt.Errorf("failed to get MAL anime: %w", err)
}
if respStatus == "200 OK" { if respStatus == "200 OK" {
err := json.Unmarshal(respBody, &malAnime) err := json.Unmarshal(respBody, &malAnime)
if err != nil { if err != nil {
log.Printf("Failed to unmarshal json response, %s\n", err) log.Printf("Failed to unmarshal json response, %s\n", err)
return malAnime, fmt.Errorf("failed to parse response: %w", err)
} }
} }
return malAnime, nil
return malAnime
} }
func (a *App) DeleteMyAnimeListEntry(id int) (bool, error) { func (a *App) DeleteMyAnimeListEntry(id int) bool {
malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(id) + "/my_list_status" malUrl := "https://api.myanimelist.net/v2/anime/" + strconv.Itoa(id) + "/my_list_status"
_, respStatus, err := MALHelper("DELETE", malUrl, nil) _, respStatus := MALHelper("DELETE", malUrl, nil)
if err != nil {
return false, fmt.Errorf("failed to delete MAL entry: %w", err)
}
if respStatus == "200 OK" { if respStatus == "200 OK" {
return true, nil return true
} else { } else {
return false, fmt.Errorf("delete failed with status: %s", respStatus) return false
} }
} }

View File

@@ -176,7 +176,7 @@ func (a *App) handleMyAnimeListCallback(wg *sync.WaitGroup, verifier *CodeVerifi
go func() { go func() {
defer wg.Done() defer wg.Done()
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("Server error: %s\n", err) log.Fatalf("listen: %s\n", err)
} }
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
}() }()

View File

@@ -14,24 +14,16 @@ import (
var SimklWatchList SimklWatchListType var SimklWatchList SimklWatchListType
func SimklHelper(method string, url string, body interface{}) (json.RawMessage, error) { func SimklHelper(method string, url string, body interface{}) json.RawMessage {
reader, err := json.Marshal(body) reader, _ := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal body: %w", err)
}
var req *http.Request var req *http.Request
client := &http.Client{} client := &http.Client{}
if body != nil { if body != nil {
req, err = http.NewRequest(method, url, bytes.NewBuffer(reader)) req, _ = http.NewRequest(method, url, bytes.NewBuffer(reader))
} else { } else {
req, err = http.NewRequest(method, url, nil) req, _ = http.NewRequest(method, url, nil)
}
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
} }
req.Header.Add("Content-Type", "application/json") req.Header.Add("Content-Type", "application/json")
@@ -40,45 +32,41 @@ func SimklHelper(method string, url string, body interface{}) (json.RawMessage,
resp, err := client.Do(req) resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("network error: %w", err) fmt.Println("Errored when sending request to the server")
message, _ := json.Marshal(struct {
Message string `json:"message"`
}{
Message: "Errored when sending request to the server" + err.Error(),
})
return message
} }
defer resp.Body.Close() defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body) respBody, _ := io.ReadAll(resp.Body)
if err != nil { return respBody
return nil, fmt.Errorf("failed to read response: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return respBody, fmt.Errorf("API returned status: %d", resp.StatusCode)
}
return respBody, nil
} }
func (a *App) SimklGetUserWatchlist() (SimklWatchListType, error) { func (a *App) SimklGetUserWatchlist() SimklWatchListType {
method := "GET" method := "GET"
url := "https://api.simkl.com/sync/all-items/anime" url := "https://api.simkl.com/sync/all-items/anime"
respBody, err := SimklHelper(method, url, nil) respBody := SimklHelper(method, url, nil)
if err != nil {
return SimklWatchListType{}, fmt.Errorf("failed to get Simkl watchlist: %w", err)
}
var errCheck struct { var errCheck struct {
Error string `json:"error"` Error string `json:"error"`
Message string `json:"message"` Message string `json:"message"`
} }
err = json.Unmarshal(respBody, &errCheck) err := json.Unmarshal(respBody, &errCheck)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return SimklWatchListType{}, fmt.Errorf("failed to parse error response: %w", err)
} }
if errCheck.Error != "" { if errCheck.Error != "" {
a.LogoutSimkl() a.LogoutSimkl()
return SimklWatchListType{}, fmt.Errorf("Simkl API error: %s", errCheck.Message) return SimklWatchListType{}
} }
var watchlist SimklWatchListType var watchlist SimklWatchListType
@@ -86,15 +74,14 @@ func (a *App) SimklGetUserWatchlist() (SimklWatchListType, error) {
err = json.Unmarshal(respBody, &watchlist) err = json.Unmarshal(respBody, &watchlist)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return SimklWatchListType{}, fmt.Errorf("failed to parse watchlist: %w", err)
} }
SimklWatchList = watchlist SimklWatchList = watchlist
return watchlist, nil return watchlist
} }
func (a *App) SimklSyncEpisodes(anime SimklAnime, progress int) (SimklAnime, error) { func (a *App) SimklSyncEpisodes(anime SimklAnime, progress int) SimklAnime {
var episodes []Episode var episodes []Episode
var url string var url string
var shows []SimklPostShow var shows []SimklPostShow
@@ -125,19 +112,13 @@ func (a *App) SimklSyncEpisodes(anime SimklAnime, progress int) (SimklAnime, err
simklSync := SimklSyncHistoryType{shows} simklSync := SimklSyncHistoryType{shows}
respBody, err := SimklHelper("POST", url, simklSync) respBody := SimklHelper("POST", url, simklSync)
if err != nil {
log.Printf("Failed to sync episodes: %s\n", err)
return anime, fmt.Errorf("failed to sync episodes: %w", err)
}
var success interface{} var success interface{}
err = json.Unmarshal(respBody, &success) err := json.Unmarshal(respBody, &success)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return anime, fmt.Errorf("failed to parse response: %w", err)
} }
for i, simklAnime := range SimklWatchList.Anime { for i, simklAnime := range SimklWatchList.Anime {
@@ -150,10 +131,10 @@ func (a *App) SimklSyncEpisodes(anime SimklAnime, progress int) (SimklAnime, err
WatchListUpdate(anime) WatchListUpdate(anime)
return anime, nil return anime
} }
func (a *App) SimklSyncRating(anime SimklAnime, rating int) (SimklAnime, error) { func (a *App) SimklSyncRating(anime SimklAnime, rating int) SimklAnime {
var url string var url string
showWithRating := ShowWithRating{ showWithRating := ShowWithRating{
Title: anime.Show.Title, Title: anime.Show.Title,
@@ -188,17 +169,13 @@ func (a *App) SimklSyncRating(anime SimklAnime, rating int) (SimklAnime, error)
Shows []interface{} `json:"shows" ts_type:"shows"` Shows []interface{} `json:"shows" ts_type:"shows"`
}{shows} }{shows}
respBody, err := SimklHelper("POST", url, simklSync) respBody := SimklHelper("POST", url, simklSync)
if err != nil {
log.Printf("Failed to sync rating: %s\n", err)
return anime, fmt.Errorf("failed to sync rating: %w", err)
}
var success interface{} var success interface{}
err = json.Unmarshal(respBody, &success) err := json.Unmarshal(respBody, &success)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return anime, fmt.Errorf("failed to parse response: %w", err)
} }
for i, simklAnime := range SimklWatchList.Anime { for i, simklAnime := range SimklWatchList.Anime {
@@ -211,10 +188,10 @@ func (a *App) SimklSyncRating(anime SimklAnime, rating int) (SimklAnime, error)
WatchListUpdate(anime) WatchListUpdate(anime)
return anime, nil return anime
} }
func (a *App) SimklSyncStatus(anime SimklAnime, status string) (SimklAnime, error) { func (a *App) SimklSyncStatus(anime SimklAnime, status string) SimklAnime {
url := "https://api.simkl.com/sync/add-to-list" url := "https://api.simkl.com/sync/add-to-list"
show := SimklShowStatus{ show := SimklShowStatus{
Title: anime.Show.Title, Title: anime.Show.Title,
@@ -234,17 +211,13 @@ func (a *App) SimklSyncStatus(anime SimklAnime, status string) (SimklAnime, erro
Shows []SimklShowStatus `json:"shows" ts_type:"shows"` Shows []SimklShowStatus `json:"shows" ts_type:"shows"`
}{shows} }{shows}
respBody, err := SimklHelper("POST", url, simklSync) respBody := SimklHelper("POST", url, simklSync)
if err != nil {
log.Printf("Failed to sync status: %s\n", err)
return anime, fmt.Errorf("failed to sync status: %w", err)
}
var success interface{} var success interface{}
err = json.Unmarshal(respBody, &success) err := json.Unmarshal(respBody, &success)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return anime, fmt.Errorf("failed to parse response: %w", err)
} }
for i, simklAnime := range SimklWatchList.Anime { for i, simklAnime := range SimklWatchList.Anime {
@@ -257,20 +230,15 @@ func (a *App) SimklSyncStatus(anime SimklAnime, status string) (SimklAnime, erro
WatchListUpdate(anime) WatchListUpdate(anime)
return anime, nil return anime
} }
func (a *App) SimklSearch(aniListAnime MediaList) (SimklAnime, error) { func (a *App) SimklSearch(aniListAnime MediaList) SimklAnime {
var result SimklAnime var result SimklAnime
if reflect.DeepEqual(SimklWatchList, SimklWatchListType{}) { if reflect.DeepEqual(SimklWatchList, SimklWatchListType{}) {
fmt.Println("Watchlist empty. Calling...") fmt.Println("Watchlist empty. Calling...")
watchlist, err := a.SimklGetUserWatchlist() SimklWatchList = a.SimklGetUserWatchlist()
if err != nil {
log.Printf("Failed to get watchlist: %s\n", err)
return result, fmt.Errorf("failed to load watchlist for search: %w", err)
}
SimklWatchList = watchlist
} }
for _, anime := range SimklWatchList.Anime { for _, anime := range SimklWatchList.Anime {
@@ -287,30 +255,22 @@ func (a *App) SimklSearch(aniListAnime MediaList) (SimklAnime, error) {
var anime SimklSearchType var anime SimklSearchType
url := "https://api.simkl.com/search/id?anilist=" + strconv.Itoa(aniListAnime.Media.ID) url := "https://api.simkl.com/search/id?anilist=" + strconv.Itoa(aniListAnime.Media.ID)
respBody, err := SimklHelper("GET", url, nil) respBody := SimklHelper("GET", url, nil)
if err != nil {
log.Printf("Failed to search Simkl: %s\n", err) err := json.Unmarshal(respBody, &anime)
return result, fmt.Errorf("failed to search Simkl by AniList ID: %w", err)
}
err = json.Unmarshal(respBody, &anime)
if len(anime) == 0 { if len(anime) == 0 {
url = "https://api.simkl.com/search/id?mal=" + strconv.Itoa(aniListAnime.Media.IDMal) url = "https://api.simkl.com/search/id?mal=" + strconv.Itoa(aniListAnime.Media.IDMal)
respBody, err = SimklHelper("GET", url, nil) respBody = SimklHelper("GET", url, nil)
if err != nil {
log.Printf("Failed to search Simkl by MAL ID: %s\n", err)
return result, fmt.Errorf("failed to search by MAL ID: %w", err)
}
err = json.Unmarshal(respBody, &anime) err = json.Unmarshal(respBody, &anime)
} }
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return result, fmt.Errorf("failed to parse search results: %w", err)
} }
if len(anime) == 0 { if len(anime) == 0 {
return result, nil return result
} }
for _, watchListAnime := range SimklWatchList.Anime { for _, watchListAnime := range SimklWatchList.Anime {
@@ -328,10 +288,10 @@ func (a *App) SimklSearch(aniListAnime MediaList) (SimklAnime, error) {
} }
} }
return result, nil return result
} }
func (a *App) SimklSyncRemove(anime SimklAnime) (bool, error) { func (a *App) SimklSyncRemove(anime SimklAnime) bool {
url := "https://api.simkl.com/sync/history/remove" url := "https://api.simkl.com/sync/history/remove"
var showArray []SimklShowStatus var showArray []SimklShowStatus
@@ -352,28 +312,25 @@ func (a *App) SimklSyncRemove(anime SimklAnime) (bool, error) {
Shows: showArray, Shows: showArray,
} }
respBody, err := SimklHelper("POST", url, show) respBody := SimklHelper("POST", url, show)
if err != nil {
log.Printf("Failed to sync remove: %s\n", err)
return false, fmt.Errorf("failed to sync remove: %w", err)
}
var success SimklDeleteType var success SimklDeleteType
err = json.Unmarshal(respBody, &success)
err := json.Unmarshal(respBody, &success)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return false, fmt.Errorf("failed to parse response: %w", err)
} }
if success.Deleted.Shows >= 1 { if success.Deleted.Shows >= 1 {
for i, simklAnime := range SimklWatchList.Anime { for i, simklAnime := range SimklWatchList.Anime {
if simklAnime.Show.Ids.Simkl == anime.Show.Ids.Simkl { if simklAnime.Show.Ids.Simkl == anime.Show.Ids.Simkl {
SimklWatchList.Anime = slices.Delete(SimklWatchList.Anime, i, i+1) SimklWatchList.Anime = slices.Delete(SimklWatchList.Anime, i, i+1)
} }
} }
return true, nil return true
} else {
return false
} }
return false, fmt.Errorf("no shows were deleted")
} }
func WatchListUpdate(anime SimklAnime) { func WatchListUpdate(anime SimklAnime) {

View File

@@ -116,7 +116,7 @@ func (a *App) handleSimklCallback(wg *sync.WaitGroup) {
go func() { go func() {
defer wg.Done() defer wg.Done()
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("Server error: %s\n", err) log.Fatalf("listen: %s\n", err)
} }
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
}() }()
@@ -138,8 +138,7 @@ func getSimklAuthorizationToken(content string) SimklJWT {
} }
jsonData, err := json.Marshal(data) jsonData, err := json.Marshal(data)
if err != nil { if err != nil {
log.Printf("Failed to marshal data: %s\n", err) log.Fatal(err)
return SimklJWT{}
} }
response, err := http.NewRequest("POST", "https://api.simkl.com/oauth/token", bytes.NewBuffer(jsonData)) response, err := http.NewRequest("POST", "https://api.simkl.com/oauth/token", bytes.NewBuffer(jsonData))

5
frontend/.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"svelte.svelte-vscode"
]
}

View File

@@ -1,72 +1,27 @@
<script lang="ts"> <script lang="ts">
import { import {onMount} from "svelte";
aniListLoggedIn, import {userStore} from "./helperFunctions/userStore"
malLoggedIn, import Router from "svelte-spa-router"
simklLoggedIn, import Home from "./routes/Home.svelte";
watchlistNeedsRefresh, import {wrap} from "svelte-spa-router/wrap";
aniListPrimary, import Spinner from "./helperComponents/Spinner.svelte";
malPrimary, import Header from "./helperComponents/Header.svelte";
simklPrimary,
malWatchList,
simklWatchList,
} from "./helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { onMount } from "svelte";
import Router from "svelte-spa-router";
import Home from "./routes/Home.svelte";
import { wrap } from "svelte-spa-router/wrap";
import Spinner from "./helperComponents/Spinner.svelte";
import Header from "./helperComponents/Header.svelte";
import { CheckIfAniListLoggedInAndLoadWatchList } from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte";
import { CheckIfSimklLoggedInAndSetUser } from "./helperModules/CheckIsSimklLoggedIn.svelte";
import {
CheckIfAniListLoggedIn,
GetMyAnimeList,
SimklGetUserWatchlist,
} from "../wailsjs/go/main/App";
import { loc } from "svelte-spa-router";
import ErrorModal from "./helperComponents/ErrorModal.svelte";
onMount(async () => { onMount(async () => {
let isAniListLoggedIn: boolean; await userStore.checkProvider('anilist')
let isMALLoggedIn: boolean; await userStore.checkProvider('mal')
let isSimklLoggedIn: boolean; await userStore.checkProvider('simkl')
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value)); })
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
!isAniListLoggedIn && (await CheckIfAniListLoggedInAndLoadWatchList());
!isMALLoggedIn && (await CheckIfMALLoggedInAndSetUser());
!isSimklLoggedIn && (await CheckIfSimklLoggedInAndSetUser());
});
$: if ($loc?.location === "/" && $watchlistNeedsRefresh) {
(async () => {
if ($aniListLoggedIn && $aniListPrimary) {
await CheckIfAniListLoggedInAndLoadWatchList();
}
if ($malLoggedIn && $malPrimary) {
await GetMyAnimeList(1000).then((w) => malWatchList.set(w));
}
if ($simklLoggedIn && $simklPrimary) {
await SimklGetUserWatchlist().then((w) => simklWatchList.set(w));
}
watchlistNeedsRefresh.set(false);
})();
}
</script> </script>
<Header /> {#if $userStore.anilist.isLoggedIn}
<ErrorModal /> <Header />
<Router <Router routes={{
routes={{ '/': Home,
"/": Home, '/anime/:id': wrap({
"/anime/:id": wrap({ asyncComponent: () => import('./routes/AnimeRoutePage.svelte'),
asyncComponent: () => import("./routes/AnimeRoutePage.svelte"), loadingComponent: Spinner
conditions: [async () => await CheckIfAniListLoggedIn()], }),
loadingComponent: Spinner, // '*': "Not Found"
}), }} />
// '*': "Not Found" {/if}
}}
/>

View File

@@ -1,6 +1,6 @@
import type {AniListGetSingleAnime} from "../anilist/types/AniListCurrentUserWatchListType"; import type {AniListGetSingleAnime} from "../types/AniListCurrentUserWatchListType";
export const AniListGetSingleAnimeDefaultData: AniListGetSingleAnime = { export const AniListGetSingleAnimeDefaultData: AniListGetSingleAnime = {
data: { data: {
MediaList: { MediaList: {
id: 0, id: 0,
@@ -26,7 +26,18 @@ export const AniListGetSingleAnimeDefaultData: AniListGetSingleAnime = {
airingAt: 0, airingAt: 0,
timeUntilAiring: 0, timeUntilAiring: 0,
episode: 0, episode: 0,
} },
tags: [
{
id: 0,
name: "",
description: "",
rank: 0,
isMediaSpoiler: false,
isAdult: false
}
],
isAdult: false
}, },
status: "", status: "",
startedAt: { startedAt: {

View File

@@ -0,0 +1,89 @@
import type {AniListCurrentUserWatchList} from "../types/AniListCurrentUserWatchListType"
export const AniListWatchListDefaultData: AniListCurrentUserWatchList = {
data: {
Page: {
pageInfo: {
total: 0,
perPage: 0,
currentPage: 0,
lastPage: 0,
hasNextPage: false
},
mediaList: [
{
id: 0,
mediaId: 0,
userId: 0,
media: {
id: 0,
idMal: 0,
title: {
romaji: "",
english: "",
native: "",
},
description: "",
coverImage: {
large: "",
},
season: "",
seasonYear: 0,
status: "",
episodes: 0,
nextAiringEpisode: {
airingAt: 0,
timeUntilAiring: 0,
episode: 0,
},
tags: [
{
id: 0,
name: "",
description: "",
rank: 0,
isMediaSpoiler: false,
isAdult: false,
},
],
isAdult: false,
},
status: "",
startedAt: {
year: 0,
month: 0,
day: 0,
},
completedAt: {
year: 0,
month: 0,
day: 0,
},
notes: "",
progress: 0,
score: 0,
repeat: 0,
user: {
id: 0,
name: "",
avatar: {
large: "",
medium: "",
},
statistics: {
anime: {
count: 0,
statuses: [
{
status: "",
count: 0,
}
]
}
}
}
}
]
}
}
}

View File

@@ -0,0 +1,67 @@
import type {AniListUser} from "../types/AniListTypes";
import type {MyAnimeListUser} from "../types/MALTypes";
import type {SimklUser} from "../types/simklTypes";
export const AniListUserDefaultData: AniListUser = {
"data": {
"Viewer": {
id: 0,
name: "",
avatar: {
large: "",
medium: "",
},
bannerImage: "",
siteUrl: ""
}
}
}
export const MALUserDefaultData: MyAnimeListUser = {
id: 0,
name: "",
picture: "",
gender: "",
birthday: "",
location: "",
joinedAt: "",
AnimeStatistics: {
numItemsWatching: 0,
numItemsCompleted: 0,
numItemsOnHold: 0,
numItemsDropped: 0,
numItemsPlanToWatch: 0,
numItems: 0,
numDaysWatched: 0,
numDaysWatching: 0,
numDaysCompleted: 0,
numDaysOnHold: 0,
numDaysDropped: 0,
numDays: 0,
numEpisodes: 0,
numTimesRewatched: 0,
meanScore: 0
},
timeZone: "",
isSupporter: false
}
export const SimklUserDefaultData: SimklUser = {
user: {
name: "",
joined_at: "",
gender: "",
avatar: "",
bio: "",
loc: "",
age: "",
},
account: {
id: 0,
timezone: "",
type: "",
},
connections: {
facebook: false
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,177 +1,133 @@
<script lang="ts"> <script lang="ts">
import { Avatar } from "flowbite-svelte"; import { Avatar } from "flowbite-svelte";
import type { AniListUser } from "../anilist/types/AniListTypes"; import {userStore} from "../helperFunctions/userStore"
import { import * as runtime from "../../wailsjs/runtime";
aniListLoggedIn, import { ShowVersion } from "../../wailsjs/go/main/App";
aniListUser,
malUser,
simklUser,
malLoggedIn,
simklLoggedIn,
loginToAniList,
loginToMAL,
loginToSimkl,
logoutOfAniList,
logoutOfMAL,
logoutOfSimkl,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import * as runtime from "../../wailsjs/runtime";
import type { MyAnimeListUser } from "../mal/types/MALTypes";
import type { SimklUser } from "../simkl/types/simklTypes";
import { ShowVersion } from "../../wailsjs/go/main/App";
let currentAniListUser: AniListUser; function dropdownUser(): void {
let currentMALUser: MyAnimeListUser; let dropdown = document.querySelector("#userDropdown");
let currentSimklUser: SimklUser; dropdown.classList.toggle("hidden");
let isAniListLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isMALLoggedIn: boolean;
aniListUser.subscribe((value) => (currentAniListUser = value)); if (!dropdown.classList.contains("hidden")) {
malUser.subscribe((value) => (currentMALUser = value)); document.addEventListener("click", clickOutside)
simklUser.subscribe((value) => (currentSimklUser = value)); }
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
function dropdownUser(): void {
let dropdown = document.querySelector("#userDropdown");
dropdown.classList.toggle("hidden");
if (!dropdown.classList.contains("hidden")) {
document.addEventListener("click", clickOutside);
} }
}
function clickOutside(event: Event): void { function clickOutside(event: Event): void {
let dropdown = document.querySelector("#userDropdown"); let dropdown = document.querySelector("#userDropdown")
let toggleBtn = document.querySelector("#userDropdownButton"); let toggleBtn = document.querySelector("#userDropdownButton")
if ( if (!dropdown.contains(event.target as Node) && !toggleBtn.contains(event.target as Node)) {
!dropdown.contains(event.target as Node) && dropdown.classList.add("hidden")
!toggleBtn.contains(event.target as Node) document.removeEventListener("click", clickOutside)
) { }
dropdown.classList.add("hidden");
document.removeEventListener("click", clickOutside);
} }
}
</script> </script>
<div class="relative"> <div class="relative">
<button id="userDropdownButton" on:click={dropdownUser}> <button id="userDropdownButton" on:click={dropdownUser}>
{#if isAniListLoggedIn} {#if $userStore.anilist.isLoggedIn}
<Avatar <Avatar
src={currentAniListUser.data.Viewer.avatar.medium} src={$userStore.anilist.user.data.Viewer.avatar.medium}
class="cursor-pointer" class="cursor-pointer"
dot={isAniListLoggedIn && isMALLoggedIn && isSimklLoggedIn dot={{ color: "green" }}
? { color: "green" } />
: { color: "yellow" }} {:else}
/> <Avatar class="cursor-pointer" dot={{ color: "red" }} />
{:else} {/if}
<Avatar class="cursor-pointer" dot={{ color: "red" }} /> </button>
{/if} <div
</button> id="userDropdown"
<div class="absolute hidden right-0 2xl:left-1/2 2xl:-translate-x-1/2 z-10 divide-y rounded-lg shadow w-44 bg-gray-700 divide-gray-600"
id="userDropdown"
class="absolute hidden right-0 2xl:left-1/2 2xl:-translate-x-1/2 z-10 divide-y rounded-lg shadow w-44 bg-gray-700 divide-gray-600"
>
<div class="px-4 py-3 text-sm text-white">
{#if isAniListLoggedIn}
<div>{currentAniListUser.data.Viewer.name}</div>
{:else}
<div>You are not logged into AniList</div>
{/if}
</div>
<ul
class="py-2 text-sm text-gray-200"
aria-labelledby="dropdownUserAvatarButton"
> >
{#if isAniListLoggedIn} <div class="px-4 py-3 text-sm text-white">
<li> {#if $userStore.anilist.isLoggedIn}
<button <div>{$userStore.anilist.user.data.Viewer.name}</div>
on:click={logoutOfAniList} {:else}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-green-800 hover:text-white" <div>You are not logged into AniList</div>
> {/if}
<span class="maple-font text-lg text-green-200 mr-4">A</span>Logout {currentAniListUser </div>
.data.Viewer.name} <ul
</button> class="py-2 text-sm text-gray-200"
</li> aria-labelledby="dropdownUserAvatarButton"
{:else} >
<li> {#if $userStore.anilist.isLoggedIn}
<button <li>
on:click={() => { <button
dropdownUser(); on:click={() => userStore.logout("anilist")}
loginToAniList(); class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-green-800 hover:text-white"
}} >
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white" <span class="maple-font text-lg text-green-200 mr-4">A</span>Logout {$userStore.anilist.user.data.Viewer.name}
> </button>
<span class="maple-font text-lg mr-4">A</span>Login to AniList </li>
</button> {:else}
</li> <li>
{/if} <button on:click={() => {
{#if isMALLoggedIn} dropdownUser()
<li> userStore.checkProvider("anilist")
<button }}
on:click={logoutOfMAL} class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white">
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-blue-800 hover:text-white" <span class="maple-font text-lg mr-4">A</span>Login to AniList
> </button>
<span class="maple-font text-lg text-blue-200 mr-4">M</span>Logout {currentMALUser.name} </li>
</button> {/if}
</li> {#if $userStore.mal.isLoggedIn}
{:else} <li>
<li> <button
<button on:click={() => userStore.logout("mal")}
on:click={() => { class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-blue-800 hover:text-white"
dropdownUser(); >
loginToMAL(); <span class="maple-font text-lg text-blue-200 mr-4">M</span>Logout {$userStore.mal.user.name}
}} </button>
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white" </li>
> {:else}
<span class="maple-font text-lg mr-4">M</span>Login to MyAnimeList <li>
</button> <button on:click={() => {
</li> dropdownUser()
{/if} userStore.checkProvider("mal")
{#if isSimklLoggedIn} }}
<li> class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white">
<button <span class="maple-font text-lg mr-4">M</span>Login to MyAnimeList
on:click={logoutOfSimkl} </button>
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-indigo-800 hover:text-white" </li>
> {/if}
<span class="maple-font text-lg text-indigo-200 mr-4">S</span>Logout {currentSimklUser {#if $userStore.simkl.isLoggedIn}
.user.name} <li>
</button> <button
</li> on:click={() => userStore.logout("simkl")}
{:else} class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-indigo-800 hover:text-white"
<li> >
<button <span class="maple-font text-lg text-indigo-200 mr-4">S</span>Logout {$userStore.simkl.user.user.name}
on:click={() => { </button>
dropdownUser(); </li>
loginToSimkl(); {:else}
}} <li>
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white" <button on:click={() => {
> dropdownUser()
<span class="maple-font text-lg mr-4">S</span>Login to Simkl userStore.checkProvider("simkl")
</button> }}
</li> class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white">
{/if} <span class="maple-font text-lg mr-4">S</span>Login to Simkl
</ul> </button>
<div class="py-2"> </li>
<button {/if}
on:click={() => { </ul>
dropdownUser(); <div class="py-2">
ShowVersion(); <button
}} on:click={() => {
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white" dropdownUser()
> ShowVersion()
Version }}
</button> class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
<button >
on:click={() => runtime.Quit()} Version
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white" </button>
> <button
Exit Application on:click={() => runtime.Quit()}
</button> class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
>
Exit Application
</button>
</div>
</div> </div>
</div> </div>
</div>

View File

@@ -1,73 +0,0 @@
<script lang="ts">
import {
apiError,
isApiDown,
clearApiError,
setApiError,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import { CheckIfMALLoggedInAndSetUser } from "../helperModules/CheckIfMyAnimeListLoggedIn.svelte";
import { CheckIfSimklLoggedInAndSetUser } from "../helperModules/CheckIsSimklLoggedIn.svelte";
import { Modal, Button } from "flowbite-svelte";
let showModal = false;
$: if ($apiError) {
showModal = true;
}
async function handleRetry() {
const service = $apiError?.service;
clearApiError();
try {
if (service === "anilist") {
await CheckIfAniListLoggedInAndLoadWatchList();
} else if (service === "mal") {
await CheckIfMALLoggedInAndSetUser();
} else if (service === "simkl") {
await CheckIfSimklLoggedInAndSetUser();
}
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
setApiError(
service || "unknown",
`Retry failed: ${errorMsg}`,
undefined,
true,
);
}
}
function handleDismiss() {
clearApiError();
showModal = false;
}
</script>
{#if showModal && $apiError}
<Modal
open={showModal}
title="{$apiError.service.toUpperCase()} API Error"
size="md"
>
<div class="space-y-4">
<div class="p-4 bg-red-50 border border-red-200 rounded-lg">
<p class="text-red-800 font-medium">{$apiError.message}</p>
{#if $apiError.statusCode}
<p class="text-red-600 text-sm mt-2">
Status: {$apiError.statusCode}
</p>
{/if}
</div>
<p class="text-gray-600">
The application will remain open. You can retry the connection or
dismiss this message to continue with limited functionality.
</p>
</div>
<div slot="footer" class="flex gap-3 justify-end">
{#if $apiError.canRetry}
<Button on:click={handleRetry} class="bg-blue-600 hover:bg-blue-700">
Retry Connection
</Button>
{/if}
<Button on:click={handleDismiss} color="alternative">Dismiss</Button>
</div>
</Modal>
{/if}

View File

@@ -1,102 +1,63 @@
<script lang="ts"> <script lang="ts">
import Search from "./Search.svelte"; import Search from "./Search.svelte"
import {
aniListLoggedIn,
loginToAniList,
loginToMAL,
loginToSimkl,
malLoggedIn,
simklLoggedIn,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import AvatarMenu from "./AvatarMenu.svelte"; import AvatarMenu from "./AvatarMenu.svelte";
import logo from "../assets/images/AniTrackLogo.svg"; import logo from "../assets/images/AniTrackLogo.svg"
import { link } from "svelte-spa-router"; import {userStore} from "../helperFunctions/userStore"
let isAniListLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isMALLoggedIn: boolean;
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
</script> </script>
<nav class="border-gray-200 bg-gray-900"> <nav class="border-gray-200 bg-gray-900">
<div <div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4"
>
<div class="flex items-center space-x-3 rtl:space-x-reverse"> <div class="flex items-center space-x-3 rtl:space-x-reverse">
<a href="/" use:link <a href="/"><img src={logo} class="h-8" alt="AniTrack Logo"/></a>
><img src={logo} class="h-8" alt="AniTrack Logo" /></a
>
</div> </div>
<div <div class="flex items-center min-[950px]:order-2 space-x-3 min-[950px]:space-x-0 rtl:space-x-reverse">
class="flex items-center min-[950px]:order-2 space-x-3 min-[950px]:space-x-0 rtl:space-x-reverse"
>
<div class="min-[950px]:block min-[950px]:mr-4"> <div class="min-[950px]:block min-[950px]:mr-4">
<Search /> <Search />
</div> </div>
<AvatarMenu /> <AvatarMenu/>
<button <button on:click={() => {
on:click={() => { let menu = document.querySelector("#navbar-user")
let menu = document.querySelector("#navbar-user"); menu.classList.toggle("hidden")
menu.classList.toggle("hidden"); }} type="button"
}} class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded-lg min-[950px]:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-700 focus:ring-gray-600"
type="button" aria-controls="navbar-user" aria-expanded="false">
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded-lg min-[950px]:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-700 focus:ring-gray-600"
aria-controls="navbar-user"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span> <span class="sr-only">Open main menu</span>
<svg <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none"
class="w-5 h-5" viewBox="0 0 17 14">
aria-hidden="true" <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
xmlns="http://www.w3.org/2000/svg" d="M1 1h15M1 7h15M1 13h15"/>
fill="none"
viewBox="0 0 17 14"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M1 1h15M1 7h15M1 13h15"
/>
</svg> </svg>
</button> </button>
</div> </div>
<div <div class="hidden items-center justify-between w-full pb-4 min-[950px]:pb-0 min-[950px]:flex min-[950px]:w-auto min-[950px]:order-1 border border-gray-700 min-[950px]:border-0 bg-gray-800 min-[950px]:bg-transparent rounded-lg" id="navbar-user">
class="hidden items-center justify-between w-full pb-4 min-[950px]:pb-0 min-[950px]:flex min-[950px]:w-auto min-[950px]:order-1 border border-gray-700 min-[950px]:border-0 bg-gray-800 min-[950px]:bg-transparent rounded-lg" <ul class="flex flex-col font-medium pb-6 min-[950px]:p-0 mt-4 min-[950px]:space-x-8 rtl:space-x-reverse min-[950px]:flex-row min-[950px]:mt-0">
id="navbar-user"
>
<ul
class="flex flex-col font-medium pb-6 min-[950px]:p-0 mt-4 min-[950px]:space-x-8 rtl:space-x-reverse min-[950px]:flex-row min-[950px]:mt-0"
>
<li> <li>
{#if !isAniListLoggedIn} {#if !$userStore.anilist.isLoggedIn}
<button on:click={loginToAniList}> <button on:click={() => userStore.checkProvider("anilist")}>
<!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">--> <!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">-->
AniList Login AniList Login
</button> </button>
{/if} {/if}
{#if !isMALLoggedIn} {#if !$userStore.mal.isLoggedIn}
<button on:click={loginToMAL}> <button on:click={() => userStore.checkProvider("mal")}>
<!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded min-[950px]:p-0 text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">--> <!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded min-[950px]:p-0 text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">-->
MyAnimeList Login MyAnimeList Login
</button> </button>
{/if} {/if}
</li> </li>
<li> <li>
{#if !isSimklLoggedIn} {#if !$userStore.simkl.isLoggedIn}
<button on:click={loginToSimkl}> <button on:click={() => userStore.checkProvider("simkl")}>
<!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded min-[950px]:p-0 text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">--> <!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded min-[950px]:p-0 text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">-->
Simkl Login Simkl Login
</button> </button>
{/if} {/if}
</li> </li>
</ul> </ul>
<div class="flex justify-center min-[950px]:hidden"> <div class="flex justify-center min-[950px]:hidden">
<Search /> <Search/>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -1,220 +1,145 @@
<script lang="ts"> <script lang="ts">
import { import {
aniListLoggedIn, aniListLoggedIn,
aniListSort, aniListWatchlist,
aniListWatchlist, animePerPage,
animePerPage, watchListPage,
watchListPage, } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType"; import type {AniListCurrentUserWatchList} from "../types/AniListCurrentUserWatchListType"
import { GetAniListUserWatchingList } from "../../wailsjs/go/main/App"; import {GetAniListUserWatchingList} from "../../wailsjs/go/main/App";
import {MediaListSort} from "../types/AniListTypes";
let aniListWatchListLoaded: AniListCurrentUserWatchList; let aniListWatchListLoaded: AniListCurrentUserWatchList
let page: number; let page: number
let perPage: number; let perPage: number
let sort: string;
watchListPage.subscribe((value) => (page = value)); watchListPage.subscribe(value => page = value)
animePerPage.subscribe((value) => (perPage = value)); animePerPage.subscribe(value => perPage = value)
aniListWatchlist.subscribe((value) => (aniListWatchListLoaded = value)); aniListWatchlist.subscribe((value) => aniListWatchListLoaded = value)
aniListSort.subscribe((value) => (sort = value));
const perPageOptions = [10, 20, 50]; const perPageOptions = [10, 20, 50]
function ChangeWatchListPage(newPage: number) { function ChangeWatchListPage(newPage: number) {
GetAniListUserWatchingList(newPage, perPage, sort).then((result) => { GetAniListUserWatchingList(newPage, perPage, MediaListSort.UpdatedTimeDesc).then((result) => {
watchListPage.set(newPage); watchListPage.set(newPage)
aniListWatchlist.set(result); aniListWatchlist.set(result)
aniListLoggedIn.set(true); aniListLoggedIn.set(true)
}); })
} }
function changePage( function changePage(e): void {
e: KeyboardEvent & { currentTarget: HTMLInputElement }, if ((e.key === "Enter" || e.key === "Tab") && Number(e.target.value) !== page) ChangeWatchListPage(Number(e.target.value))
): void { }
if (
(e.key === "Enter" || e.key === "Tab") && function changeCountPerPage(e): void {
Number(e.currentTarget.value) !== page GetAniListUserWatchingList(1, Number(e.target.value), MediaListSort.UpdatedTimeDesc).then((result) => {
) animePerPage.set(Number(e.target.value))
ChangeWatchListPage(Number(e.currentTarget.value)); watchListPage.set(1)
} aniListWatchlist.set(result)
aniListLoggedIn.set(true)
})
}
function changeCountPerPage(
e: Event & { currentTarget: HTMLSelectElement },
): void {
GetAniListUserWatchingList(1, Number(e.currentTarget.value), sort).then(
(result) => {
animePerPage.set(Number(e.currentTarget.value));
watchListPage.set(1);
aniListWatchlist.set(result);
aniListLoggedIn.set(true);
},
);
}
</script> </script>
<div class="mb-8"> <div class="mb-8">
{#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12} {#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12}
<nav aria-label="Page navigation" class="hidden md:block"> <nav aria-label="Page navigation" class="hidden md:block">
<ul class="inline-flex -space-x-px text-base h-10"> <ul class="inline-flex -space-x-px text-base h-10">
{#if page === 1} {#if page === 1}
<li> <li>
<button <button disabled
disabled class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 cursor-default">
class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 cursor-default" Previous
> </button>
Previous </li>
</button> {:else}
</li> <li>
{:else} <button on:click={() => ChangeWatchListPage(page-1)}
<li> class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white">
<button Previous
on:click={() => ChangeWatchListPage(page - 1)} </button>
class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white" </li>
> {/if}
Previous {#each {length: aniListWatchListLoaded.data.Page.pageInfo.lastPage} as _, i}
</button> {#if i + 1 === page}
</li> <li>
{/if} <button on:click={() => ChangeWatchListPage(i+1)}
{#each { length: aniListWatchListLoaded.data.Page.pageInfo.lastPage } as _, i} class="flex items-center justify-center px-4 h-10 leading-tight border bg-gray-100 border-gray-700 bg-gray-700 text-white">{i + 1}</button>
{#if i + 1 === page} </li>
<li> {:else}
<button <li>
on:click={() => ChangeWatchListPage(i + 1)} <button on:click={() => ChangeWatchListPage(i+1)}
class="flex items-center justify-center px-4 h-10 leading-tight border hover:bg-gray-100 border-gray-700 bg-gray-700 text-white" class="flex items-center justify-center px-4 h-10 leading-tight border dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white">{i + 1}</button>
>{i + 1}</button </li>
> {/if}
</li> {/each}
{:else} {#if page === aniListWatchListLoaded.data.Page.pageInfo.lastPage}
<li> <li>
<button <button disabled
on:click={() => ChangeWatchListPage(i + 1)} class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 cursor-default">
class="flex items-center justify-center px-4 h-10 leading-tight border dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white" Next
>{i + 1}</button </button>
> </li>
</li> {:else}
{/if} <li>
{/each} <button on:click={() => ChangeWatchListPage(page+1)}
{#if page === aniListWatchListLoaded.data.Page.pageInfo.lastPage} class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white">
<li> Next
<button </button>
disabled </li>
class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 cursor-default" {/if}
> </ul>
Next </nav>
</button> {/if}
</li> <div class="flex mt-5">
{:else} <div class="w-20 mx-auto">
<li> <select bind:value={perPage} on:change={(e) => changeCountPerPage(e)} id="countPerPage"
<button class="border text-sm rounded-lg block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500">
on:click={() => ChangeWatchListPage(page + 1)} {#each perPageOptions as option}
class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white" <option value={option}>
> {option}
Next </option>
</button> {/each}
</li> </select>
{/if}
</ul>
</nav>
{/if}
<div class="flex mt-5">
<div class="w-20 mx-auto">
<select
bind:value={perPage}
on:change={(e) => changeCountPerPage(e)}
id="countPerPage"
class="border text-sm rounded-lg block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
>
{#each perPageOptions as option}
<option value={option}>
{option}
</option>
{/each}
</select>
</div>
<div>
<div>Total Anime: {aniListWatchListLoaded.data.Page.pageInfo.total}</div>
{#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12}
<div class="md:hidden">
Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}
</div> </div>
{:else}
<div> <div>
Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage} <div>Total Anime: {aniListWatchListLoaded.data.Page.pageInfo.total}</div>
{#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12}
<div class="md:hidden">Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}</div>
{:else}
<div>Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}</div>
{/if}
</div> </div>
{/if}
</div>
<div class="max-w-xs mx-auto"> <div class="max-w-xs mx-auto">
<div class="relative flex items-center max-w-[11rem]"> <div class="relative flex items-center max-w-[11rem]">
<button <button type="button" id="decrement-button" on:click={() => ChangeWatchListPage(page-1)}
type="button" class="bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none">
id="decrement-button" <svg class="w-3 h-3 text-white" aria-hidden="true"
on:click={() => ChangeWatchListPage(page - 1)} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 2">
class={page <= 1 <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
? "border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none" d="M1 1h16"/>
: "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>
disabled={page <= 1} </button>
> <input type="number" min="1" max="{aniListWatchListLoaded.data.Page.pageInfo.lastPage}"
<svg on:keydown={changePage} id="page-counter"
class="w-3 h-3 text-white" class="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none border-x-0 h-11 font-medium text-center text-sm block w-full pb-6 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
aria-hidden="true" value={page} required/>
xmlns="http://www.w3.org/2000/svg" <div class="absolute bottom-1 start-1/2 -translate-x-1/2 rtl:translate-x-1/2 flex items-center text-xs text-gray-400 space-x-1 rtl:space-x-reverse">
fill="none" <span>Page #</span>
viewBox="0 0 18 2" </div>
> <button type="button" id="increment-button" on:click={() => ChangeWatchListPage(page+1)}
<path class="hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none">
stroke="currentColor" <svg class="w-3 h-3 text-white" aria-hidden="true"
stroke-linecap="round" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 18">
stroke-linejoin="round" <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
stroke-width="2" d="M9 1v16M1 9h16"/>
d="M1 1h16" </svg>
/> </button>
</svg> </div>
</button>
<input
type="number"
min="1"
max={aniListWatchListLoaded.data.Page.pageInfo.lastPage}
on:keydown={changePage}
id="page-counter"
class="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none border-x-0 h-11 font-medium text-center text-sm block w-full pb-6 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
value={page}
required
/>
<div
class="absolute bottom-1 start-1/2 -translate-x-1/2 rtl:translate-x-1/2 flex items-center text-xs text-gray-400 space-x-1 rtl:space-x-reverse"
>
<span>Page #</span>
</div> </div>
<button
type="button"
id="increment-button"
on:click={() => ChangeWatchListPage(page + 1)}
class={page >= aniListWatchListLoaded.data.Page.pageInfo.lastPage
? "border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
: "bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"}
disabled={page >= aniListWatchListLoaded.data.Page.pageInfo.lastPage}
>
<svg
class="w-3 h-3 text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 18 18"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 1v16M1 9h16"
/>
</svg>
</button>
</div>
</div> </div>
</div> </div>
</div>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {AniListSearch} from "../../wailsjs/go/main/App"; import {AniListSearch} from "../../wailsjs/go/main/App";
import type {AniSearchList} from "../anilist/types/AniListTypes"; import type {AniSearchList} from "../types/AniListTypes";
import {push} from "svelte-spa-router"; import {push} from "svelte-spa-router";
let aniSearch = "" let aniSearch = ""

View File

@@ -1,99 +1,59 @@
<script lang="ts"> <script lang="ts">
import { import {
aniListLoggedIn, aniListWatchlist,
aniListWatchlist, GetAnimeSingleItem,
GetAnimeSingleItem, loading,
loading, } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"; import {push} from "svelte-spa-router";
import { push } from "svelte-spa-router"; import type {AniListCurrentUserWatchList} from "../types/AniListCurrentUserWatchListType"
import type { AniListCurrentUserWatchList } from "../anilist/types/AniListCurrentUserWatchListType"; import {Rating} from "flowbite-svelte";
import { Rating } from "flowbite-svelte"; import loader from '../helperFunctions/loader'
import loader from "../helperFunctions/loader";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
let isAniListLoggedIn: boolean;
let aniListWatchListLoaded: AniListCurrentUserWatchList;
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value)); let aniListWatchListLoaded: AniListCurrentUserWatchList
aniListWatchlist.subscribe((value) => (aniListWatchListLoaded = value));
aniListWatchlist.subscribe((value) => aniListWatchListLoaded = value)
</script> </script>
<div> <div>
{#if isAniListLoggedIn} <div class="mx-auto max-w-2xl p-4 sm:p-6 lg:max-w-7xl lg:px-8 relative items-center">
<div <h1 class="text-left text-xl font-bold mb-4">Your AniList WatchList</h1>
class="mx-auto max-w-2xl p-4 sm:p-6 lg:max-w-7xl lg:px-8 relative items-center"
>
<div class="flex justify-between items-center mb-4">
<h1 class="text-left text-xl font-bold">Your AniList WatchList</h1>
<button
type="button"
class="py-2 px-4 bg-gray-700 rounded-lg"
on:click={async () => {
loading.set(true);
await CheckIfAniListLoggedInAndLoadWatchList();
loading.set(false);
}}
>
Refresh WatchList
</button>
</div>
<div <div class="grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8">
class="grid grid-cols-1 gap-x-6 gap-y-10 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-x-8" {#each aniListWatchListLoaded.data.Page.mediaList as media}
> <div use:loader={loading}
{#each aniListWatchListLoaded.data.Page.mediaList as media} class="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-lg xl:aspect-h-8 xl:aspect-w-7">
<div <div class="flex flex-col items-center group">
use:loader={loading} <button on:click={() => {
class="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-lg xl:aspect-h-8 xl:aspect-w-7" push(`#/anime/${media.media.id}`)
> }}
<div class="flex flex-col items-center group"> >
<button <img class="rounded-lg" src={media.media.coverImage.large} alt={
on:click={() => { media.media.title.english === "" ?
push(`#/anime/${media.media.id}`); media.media.title.romaji :
// loading.set(true) media.media.title.english
// GetAniListSingleItem(media.media.id, true).then(() => { }/>
// loading.set(false) </button>
// <Rating id="anime-rating" total={5} size={35} rating={media.score/2.0}/>
// }) <button class="mt-4 text-md font-semibold text-white-700"
}} on:click={() => GetAnimeSingleItem(media.media.id, true)}>
> {
<img media.media.title.english === "" ?
class="rounded-lg w-[230px] h-[330px] object-cover" media.media.title.romaji :
src={media.media.coverImage.large} media.media.title.english
alt={media.media.title.english === "" }
? media.media.title.romaji </button>
: media.media.title.english} <p class="mt-1 text-lg font-medium text-white-900">{media.progress}
/> / {media.media.nextAiringEpisode.episode !== 0 ?
</button> media.media.nextAiringEpisode.episode - 1 : media.media.episodes}</p>
<Rating {#if media.media.episodes > 0}
id="anime-rating" <p class="mt-1 text-lg font-medium text-white-900">Total
total={5} Episodes: {media.media.episodes}</p>
size={35} {/if}
rating={media.score / 2.0} </div>
/> </div>
<button {/each}
class="mt-4 text-md font-semibold text-white-700" </div>
on:click={() => GetAnimeSingleItem(media.media.id, true)}
>
{media.media.title.english === ""
? media.media.title.romaji
: media.media.title.english}
</button>
<p class="mt-1 text-lg font-medium text-white-900">
{media.progress}
/ {media.media.nextAiringEpisode.episode !== 0
? media.media.nextAiringEpisode.episode - 1
: media.media.episodes}
</p>
{#if media.media.episodes > 0}
<p class="mt-1 text-lg font-medium text-white-900">
Total Episodes: {media.media.episodes}
</p>
{/if}
</div>
</div>
{/each}
</div>
</div> </div>
{/if}
</div> </div>

View File

@@ -0,0 +1,121 @@
// stores/user.ts
import {get, writable} from 'svelte/store';
import type {SimklUser, SimklWatchList} from "../types/simklTypes";
import type {AniListUser} from "../types/AniListTypes";
import type {MALWatchlist, MyAnimeListUser} from "../types/MALTypes";
import {
GetAniListLoggedInUser,
GetMyAnimeList,
GetMyAnimeListLoggedInUser,
GetSimklLoggedInUser,
LogoutAniList,
LogoutMyAnimeList,
LogoutSimkl,
SimklGetUserWatchlist
} from "../../wailsjs/go/main/App";
import {LoadAniListWatchList} from "../helperModules/LoadAniListWatchList.svelte";
import {aniListWatchlist, malWatchList, simklWatchList} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"
import type {AniListCurrentUserWatchList} from "../types/AniListCurrentUserWatchListType";
import {AniListUserDefaultData, MALUserDefaultData, SimklUserDefaultData} from "../defaults/UserTypes";
let aniWatchlist: AniListCurrentUserWatchList
aniListWatchlist.subscribe(value => aniWatchlist = value)
interface UserState {
simkl: { user: SimklUser; isLoggedIn: boolean; isPrimary: boolean };
anilist: { user: AniListUser; isLoggedIn: boolean; isPrimary: boolean };
mal: { user: MyAnimeListUser; isLoggedIn: boolean; isPrimary: boolean };
}
const createUserStore = () => {
const {subscribe, update} = writable<UserState>({
anilist: {user: AniListUserDefaultData, isLoggedIn: false, isPrimary: true},
mal: {user: MALUserDefaultData, isLoggedIn: false, isPrimary: false},
simkl: {user: SimklUserDefaultData, isLoggedIn: false, isPrimary: false}
});
return {
subscribe,
setAniListUser: (user: AniListUser, isPrimary = true) =>
update(s => ({
...s,
anilist: {user, isLoggedIn: true, isPrimary}
})),
setMalUser: (user: MyAnimeListUser, isPrimary = false) =>
update(s => ({
...s,
mal: {user, isLoggedIn: true, isPrimary}
})),
setSimklUser: (user: SimklUser, isPrimary = false) =>
update(s => ({
...s,
simkl: {user, isLoggedIn: true, isPrimary}
})),
setPrimary: (provider: 'simkl' | 'anilist' | 'mal') =>
update(s => ({
...s,
simkl: {...s.simkl, isPrimary: provider === 'simkl'},
anilist: {...s.anilist, isPrimary: provider === 'anilist'},
mal: {...s.mal, isPrimary: provider === 'mal'}
})),
checkProvider: async (provider: 'simkl' | 'anilist' | 'mal') => {
const state = get(userStore);
if (state[provider].isLoggedIn) return;
if (provider === 'anilist') {
const user: AniListUser = await GetAniListLoggedInUser();
console.log(user)
userStore.setAniListUser(user, state.anilist.isPrimary);
console.log(state.anilist.isPrimary)
if (state.anilist.isPrimary) await LoadAniListWatchList();
} else if (provider === 'mal') {
const user: MyAnimeListUser = await GetMyAnimeListLoggedInUser();
userStore.setMalUser(user, state.mal.isPrimary);
if (state.mal.isPrimary) {
const watchList = await GetMyAnimeList(1000);
malWatchList.set(watchList);
}
} else if (provider === 'simkl') {
const user: SimklUser = await GetSimklLoggedInUser();
userStore.setSimklUser(user, state.simkl.isPrimary);
if (state.simkl.isPrimary) {
const watchList = await SimklGetUserWatchlist();
simklWatchList.set(watchList)
}
}
},
logout: async (provider: 'simkl' | 'anilist' | 'mal') => {
update(s => {
s[provider].user = {} as SimklUser | AniListUser | MyAnimeListUser
s[provider].isLoggedIn = false;
if (s[provider].isPrimary) {
s[provider].isPrimary = false;
const others = ['simkl', 'anilist', 'mal'] as const;
const newPrimary = others.find(p => p !== provider && s[p].isLoggedIn);
if (newPrimary) s[newPrimary].isPrimary = true;
}
return s;
});
// Clear provider-specific watchlist
if (provider === 'anilist') {
if (Object.keys(aniWatchlist).length !== 0) {
aniListWatchlist.set({} as AniListCurrentUserWatchList)
}
await LogoutAniList();
} else if (provider === 'mal') {
if (Object.keys(aniWatchlist).length !== 0) {
malWatchList.set({} as MALWatchlist);
}
await LogoutMyAnimeList();
} else if (provider === 'simkl') {
if (Object.keys(aniWatchlist).length !== 0) {
simklWatchList.set({} as SimklWatchList);
}
await LogoutSimkl();
}
}
}
};
export const userStore = createUserStore();

View File

@@ -1,5 +1,5 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import type {TableItem} from "../helperTypes/TableTypes"; import type {TableItem} from "../types/TableTypes";
import { tableItems } from "./GlobalVariablesAndHelperFunctions.svelte" import { tableItems } from "./GlobalVariablesAndHelperFunctions.svelte"
export function AddAnimeServiceToTable(animeItem: TableItem) { export function AddAnimeServiceToTable(animeItem: TableItem) {

View File

@@ -1,81 +0,0 @@
<script lang="ts" context="module">
import {
CheckIfAniListLoggedIn,
GetAniListLoggedInUser,
GetAniListUserWatchingList,
} from "../../wailsjs/go/main/App";
import {
aniListUser,
watchListPage,
animePerPage,
aniListPrimary,
aniListLoggedIn,
aniListWatchlist,
aniListSort,
clearApiError,
setApiError,
} from "./GlobalVariablesAndHelperFunctions.svelte";
let isAniListPrimary: boolean;
let page: number;
let perPage: number;
let sort: string;
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
watchListPage.subscribe((value) => (page = value));
animePerPage.subscribe((value) => (perPage = value));
aniListSort.subscribe((value) => (sort = value));
export const LoadAniListUser = async () => {
try {
await GetAniListLoggedInUser().then((user) => {
aniListUser.set(user);
});
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
setApiError(
"anilist",
`Failed to load user: ${errorMsg}`,
undefined,
true,
);
throw err;
}
};
export const LoadAniListWatchList = async () => {
try {
const watchList = await GetAniListUserWatchingList(page, perPage, sort);
aniListWatchlist.set(watchList);
clearApiError();
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
setApiError(
"anilist",
`Failed to load watch list: ${errorMsg}`,
undefined,
true,
);
throw err;
}
};
export const CheckIfAniListLoggedInAndLoadWatchList = async () => {
try {
const loggedIn = await CheckIfAniListLoggedIn();
if (loggedIn) {
await LoadAniListUser();
if (isAniListPrimary) await LoadAniListWatchList();
}
aniListLoggedIn.set(loggedIn);
} catch (err) {
const errorMsg = err instanceof Error ? err.message : String(err);
setApiError(
"anilist",
`Authentication failed: ${errorMsg}`,
undefined,
true,
);
aniListLoggedIn.set(false);
}
};
</script>

View File

@@ -1,25 +0,0 @@
<script lang="ts" context="module">
import {CheckIfMyAnimeListLoggedIn, GetMyAnimeList, GetMyAnimeListLoggedInUser} from "../../wailsjs/go/main/App";
import {malUser, malPrimary, malWatchList, malLoggedIn} from "./GlobalVariablesAndHelperFunctions.svelte"
let isMalPrimary: boolean
malPrimary.subscribe(value => isMalPrimary = value)
export const CheckIfMALLoggedInAndSetUser = async () => {
await CheckIfMyAnimeListLoggedIn().then(loggedIn => {
if (loggedIn) {
GetMyAnimeListLoggedInUser().then(user => {
malUser.set(user)
if (isMalPrimary) {
GetMyAnimeList(1000).then(watchList => {
malWatchList.set(watchList)
malLoggedIn.set(loggedIn)
})
} else {
malLoggedIn.set(loggedIn)
}
})
}
})
}
</script>

View File

@@ -1,29 +0,0 @@
<script lang="ts" context="module">
import {CheckIfSimklLoggedIn, GetSimklLoggedInUser, SimklGetUserWatchlist} from "../../wailsjs/go/main/App";
import { simklLoggedIn, simklUser, simklPrimary, simklWatchList } from "./GlobalVariablesAndHelperFunctions.svelte";
let isSimklPrimary: boolean
simklPrimary.subscribe(value => isSimklPrimary = value)
export const CheckIfSimklLoggedInAndSetUser = async () => {
await CheckIfSimklLoggedIn().then(loggedIn => {
if (loggedIn) {
GetSimklLoggedInUser().then(user => {
if (Object.keys(user).length === 0) {
simklLoggedIn.set(false)
} else {
simklUser.set(user)
if (isSimklPrimary) {
SimklGetUserWatchlist().then(result => {
simklWatchList.set(result)
simklLoggedIn.set(loggedIn)
})
} else {
simklLoggedIn.set(loggedIn)
}
}
})
}
})
}
</script>

View File

@@ -1,212 +1,73 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { import {
GetAniListItem, GetAniListItem,
GetAniListLoggedInUser, GetMyAnimeListAnime,
GetAniListUserWatchingList, SimklSearch
GetMyAnimeListAnime, } from "../../wailsjs/go/main/App";
GetMyAnimeListLoggedInUser, import {userStore} from "../helperFunctions/userStore";
GetSimklLoggedInUser, import type {
LogoutAniList, AniListGetSingleAnime
LogoutMyAnimeList, } from "../types/AniListCurrentUserWatchListType.js";
LogoutSimkl, import {get, writable} from 'svelte/store'
SimklGetUserWatchlist, import type {SimklAnime, SimklWatchList} from "../types/simklTypes";
SimklSearch, import type {MALAnime, MALWatchlist} from "../types/MALTypes";
} from "../../wailsjs/go/main/App"; import type {TableItems} from "../types/TableTypes";
import type { import {AniListGetSingleAnimeDefaultData} from "../defaults/AniListGetSingleAnime";
AniListCurrentUserWatchList, import {AniListWatchListDefaultData} from "../defaults/AniListWatchListDefaultData";
AniListGetSingleAnime,
} from "../anilist/types/AniListCurrentUserWatchListType.js";
import { writable } from "svelte/store";
import type {
SimklAnime,
SimklUser,
SimklWatchList,
} from "../simkl/types/simklTypes";
import {
type AniListUser,
MediaListSort,
} from "../anilist/types/AniListTypes";
import type {
MALAnime,
MALWatchlist,
MyAnimeListUser,
} from "../mal/types/MALTypes";
import type { TableItems } from "../helperTypes/TableTypes";
import { AniListGetSingleAnimeDefaultData } from "../helperDefaults/AniListGetSingleAnime";
export let aniListAnime = writable(AniListGetSingleAnimeDefaultData); export let aniListAnime = writable(AniListGetSingleAnimeDefaultData)
export let title = writable(""); export let title = writable("")
export let aniListLoggedIn = writable(false); export let aniListLoggedIn = writable(false)
export let simklLoggedIn = writable(false); export let simklWatchList = writable({} as SimklWatchList)
export let malLoggedIn = writable(false); export let aniListWatchlist = writable(AniListWatchListDefaultData)
export let simklWatchList = writable({} as SimklWatchList); export let malWatchList = writable({} as MALWatchlist)
export let aniListPrimary = writable(true); export let malAnime = writable({} as MALAnime)
export let simklPrimary = writable(false); export let simklAnime = writable({} as SimklAnime)
export let malPrimary = writable(false); export let loading = writable(false)
export let simklUser = writable({} as SimklUser); export let tableItems = writable([] as TableItems)
export let aniListUser = writable({} as AniListUser);
export let malUser = writable({} as MyAnimeListUser);
export let aniListWatchlist = writable({} as AniListCurrentUserWatchList);
export let malWatchList = writable({} as MALWatchlist);
export let malAnime = writable({} as MALAnime);
export let simklAnime = writable({} as SimklAnime);
export let loading = writable(false);
export let tableItems = writable([] as TableItems);
export let watchlistNeedsRefresh = writable(false);
export let aniListSort = writable(MediaListSort.UpdatedTimeDesc);
export let watchListPage = writable(1); export let watchListPage = writable(1)
export let animePerPage = writable(20); export let animePerPage = writable(20)
let isAniListPrimary: boolean; let currentAniListAnime: AniListGetSingleAnime
let page: number;
let perPage: number;
let sort: string;
let aniWatchlist: AniListCurrentUserWatchList;
let currentAniListAnime: AniListGetSingleAnime;
let isMalLoggedIn: boolean; aniListAnime.subscribe(value => currentAniListAnime = value)
let isSimklLoggedIn: boolean;
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
watchListPage.subscribe((value) => (page = value));
animePerPage.subscribe((value) => (perPage = value));
aniListWatchlist.subscribe((value) => (aniWatchlist = value));
malLoggedIn.subscribe((value) => (isMalLoggedIn = value));
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
aniListAnime.subscribe((value) => (currentAniListAnime = value));
aniListSort.subscribe((value) => (sort = value));
export interface ApiError { export async function GetAnimeSingleItem(aniId: number, login: boolean): Promise<""> {
service: string; const store = get(userStore)
message: string; if (store.anilist.isLoggedIn)
statusCode?: string; await GetAniListItem(aniId, login).then(aniListResult => {
canRetry: boolean; let finalResult: AniListGetSingleAnime
} finalResult = aniListResult
export const apiError = writable<ApiError | null>(null); if (login === false) {
export const isApiDown = writable(false); finalResult.data.MediaList.status = ""
export function setApiError( finalResult.data.MediaList.score = 0
service: string, finalResult.data.MediaList.progress = 0
message: string, finalResult.data.MediaList.notes = ""
statusCode?: string, finalResult.data.MediaList.repeat = 0
canRetry: boolean = true, finalResult.data.MediaList.startedAt.day = 0
) { finalResult.data.MediaList.startedAt.month = 0
apiError.set({ finalResult.data.MediaList.startedAt.year = 0
service, finalResult.data.MediaList.completedAt.day = 0
message, finalResult.data.MediaList.completedAt.month = 0
statusCode, finalResult.data.MediaList.completedAt.year = 0
canRetry, }
}); aniListAnime.set(finalResult)
isApiDown.set(true); title.set(currentAniListAnime.data.MediaList.media.title.english === "" ?
} currentAniListAnime.data.MediaList.media.title.romaji :
export function clearApiError() { currentAniListAnime.data.MediaList.media.title.english)
apiError.set(null); })
isApiDown.set(false); if (store.mal.isLoggedIn) {
} await GetMyAnimeListAnime(currentAniListAnime.data.MediaList.media.idMal).then(malResult => {
malAnime.set(malResult)
export async function GetAnimeSingleItem( })
aniId: number, }
login: boolean, if (store.simkl.isLoggedIn) {
): Promise<""> { await SimklSearch(currentAniListAnime.data.MediaList).then((value: SimklAnime) => {
await GetAniListItem(aniId, login).then((aniListResult) => { simklAnime.set(value)
let finalResult: AniListGetSingleAnime; })
finalResult = aniListResult; }
if (login === false) { return ""
finalResult.data.MediaList.status = "";
finalResult.data.MediaList.score = 0;
finalResult.data.MediaList.progress = 0;
finalResult.data.MediaList.notes = "";
finalResult.data.MediaList.repeat = 0;
finalResult.data.MediaList.startedAt.day = 0;
finalResult.data.MediaList.startedAt.month = 0;
finalResult.data.MediaList.startedAt.year = 0;
finalResult.data.MediaList.completedAt.day = 0;
finalResult.data.MediaList.completedAt.month = 0;
finalResult.data.MediaList.completedAt.year = 0;
}
aniListAnime.set(finalResult);
title.set(
currentAniListAnime.data.MediaList.media.title.english === ""
? currentAniListAnime.data.MediaList.media.title.romaji
: currentAniListAnime.data.MediaList.media.title.english,
);
});
if (isMalLoggedIn) {
await GetMyAnimeListAnime(
currentAniListAnime.data.MediaList.media.idMal,
).then((malResult) => {
malAnime.set(malResult);
});
} }
if (isSimklLoggedIn) { </script>
await SimklSearch(currentAniListAnime.data.MediaList).then(
(value: SimklAnime) => {
simklAnime.set(value);
},
);
}
return "";
}
export function loginToSimkl(): void {
GetSimklLoggedInUser().then((user) => {
if (Object.keys(user).length === 0) {
simklLoggedIn.set(false);
} else {
simklUser.set(user);
SimklGetUserWatchlist().then((result) => {
simklWatchList.set(result);
simklLoggedIn.set(true);
});
}
});
}
export function loginToAniList(): void {
GetAniListLoggedInUser().then((result) => {
aniListUser.set(result);
if (isAniListPrimary) {
GetAniListUserWatchingList(page, perPage, sort).then((result) => {
aniListWatchlist.set(result);
aniListLoggedIn.set(true);
});
} else {
aniListLoggedIn.set(true);
}
});
}
export function loginToMAL(): void {
GetMyAnimeListLoggedInUser().then((result) => {
malUser.set(result);
malLoggedIn.set(true);
});
}
export function logoutOfAniList(): void {
LogoutAniList().then((result) => {
console.log(result);
if (Object.keys(aniWatchlist).length !== 0) {
aniListWatchlist.set({} as AniListCurrentUserWatchList);
}
aniListUser.set({} as AniListUser);
aniListLoggedIn.set(false);
});
}
export function logoutOfMAL(): void {
LogoutMyAnimeList().then((result) => {
console.log(result);
malUser.set({} as MyAnimeListUser);
malLoggedIn.set(false);
});
}
export function logoutOfSimkl(): void {
LogoutSimkl().then((result) => {
console.log(result);
simklUser.set({} as SimklUser);
simklLoggedIn.set(false);
});
}
</script>

View File

@@ -0,0 +1,17 @@
<script lang="ts" context="module">
import {GetAniListUserWatchingList} from "../../wailsjs/go/main/App";
import {MediaListSort} from "../types/AniListTypes";
import { watchListPage, animePerPage, aniListWatchlist } from "./GlobalVariablesAndHelperFunctions.svelte"
let page: number
let perPage: number
watchListPage.subscribe(value => page = value)
animePerPage.subscribe(value => perPage = value)
export const LoadAniListWatchList = async () => {
await GetAniListUserWatchingList(page, perPage, MediaListSort.UpdatedTimeDesc).then((watchList) => {
aniListWatchlist.set(watchList)
})
}
</script>

View File

@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { aniListAnime, GetAnimeSingleItem } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"; import { aniListAnime, GetAnimeSingleItem } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import Anime from "../helperComponents/Anime.svelte" import Anime from "../helperComponents/Anime.svelte"
import { AniListGetSingleAnimeDefaultData } from "../helperDefaults/AniListGetSingleAnime"; import { AniListGetSingleAnimeDefaultData } from "../defaults/AniListGetSingleAnime";
import Spinner from "../helperComponents/Spinner.svelte"; import Spinner from "../helperComponents/Spinner.svelte";
export let params: Record<string, string> export let params: Record<string, string>

View File

@@ -1,55 +1,19 @@
<script lang="ts"> <script lang="ts">
import Pagination from "../helperComponents/Pagination.svelte"; import Pagination from "../helperComponents/Pagination.svelte";
import WatchList from "../helperComponents/WatchList.svelte"; import WatchList from "../helperComponents/WatchList.svelte";
import { import {
aniListLoggedIn,
aniListPrimary,
loading, loading,
isApiDown, } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
apiError, import loader from '../helperFunctions/loader'
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"; import {userStore} from "../helperFunctions/userStore";
import loader from "../helperFunctions/loader";
let isAniListPrimary: boolean;
let isAniListLoggedIn: boolean;
aniListPrimary.subscribe((value) => (isAniListPrimary = value));
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
</script> </script>
{#if $userStore.anilist.isLoggedIn && $userStore.anilist.isPrimary}
{#if $isApiDown} <div class="container py-10">
<div class="container py-10">
<div
class="bg-yellow-50 border border-yellow-200 rounded-lg p-6 text-center"
>
<svg
class="mx-auto h-12 w-12 text-yellow-600 mb-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
<h2 class="text-xl font-semibold text-yellow-900 mb-2">
API Unavailable
</h2>
<p class="text-yellow-700 mb-4">
The {$apiError?.service || "service"} is currently unavailable. The app will
remain open, and you can retry when the service is back online.
</p>
</div>
</div>
{:else if isAniListLoggedIn && isAniListPrimary}
<div class="container py-10">
<Pagination /> <Pagination />
<WatchList /> <WatchList />
<Pagination /> <Pagination />
</div> </div>
{:else} {:else}
<div use:loader={loading}></div> <div use:loader={loading}></div>
{/if} {/if}

View File

@@ -0,0 +1,10 @@
// vite.config.ts
import { defineConfig } from "file:///home/nymusicman/Code/AniTrack/frontend/node_modules/vite/dist/node/index.js";
import { svelte } from "file:///home/nymusicman/Code/AniTrack/frontend/node_modules/@sveltejs/vite-plugin-svelte/src/index.js";
var vite_config_default = defineConfig({
plugins: [svelte()]
});
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS9ueW11c2ljbWFuL0NvZGUvQW5pVHJhY2svZnJvbnRlbmRcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIi9ob21lL255bXVzaWNtYW4vQ29kZS9BbmlUcmFjay9mcm9udGVuZC92aXRlLmNvbmZpZy50c1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vaG9tZS9ueW11c2ljbWFuL0NvZGUvQW5pVHJhY2svZnJvbnRlbmQvdml0ZS5jb25maWcudHNcIjtpbXBvcnQge2RlZmluZUNvbmZpZ30gZnJvbSAndml0ZSdcbmltcG9ydCB7c3ZlbHRlfSBmcm9tICdAc3ZlbHRlanMvdml0ZS1wbHVnaW4tc3ZlbHRlJ1xuXG4vLyBodHRwczovL3ZpdGVqcy5kZXYvY29uZmlnL1xuZXhwb3J0IGRlZmF1bHQgZGVmaW5lQ29uZmlnKHtcbiAgcGx1Z2luczogW3N2ZWx0ZSgpXVxufSlcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBdVMsU0FBUSxvQkFBbUI7QUFDbFUsU0FBUSxjQUFhO0FBR3JCLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQzFCLFNBQVMsQ0FBQyxPQUFPLENBQUM7QUFDcEIsQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K

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": { "info": {
"productName": "AniTrack", "productName": "AniTrack",
"productVersion": "0.7.0" "productVersion": "0.6.0"
} }
} }