2 Commits

65 changed files with 2354 additions and 2245 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"
@@ -94,7 +93,6 @@ func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
timeUntilAiring timeUntilAiring
episode episode
} }
genres
tags{ tags{
id id
name name
@@ -184,7 +182,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"`
@@ -224,7 +222,6 @@ func (a *App) AniListSearch(query string) (interface{}, error) {
timeUntilAiring timeUntilAiring
episode episode
} }
genres
tags{ tags{
id id
name name
@@ -243,20 +240,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
} }
func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) (AniListCurrentUserWatchList, error) { return post
}
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"`
@@ -312,7 +307,6 @@ func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) (An
timeUntilAiring timeUntilAiring
episode episode
} }
genres
tags{ tags{
id id
name name
@@ -407,15 +401,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

@@ -74,7 +74,6 @@ type MediaList struct {
TimeUntilAiring int `json:"timeUntilAiring"` TimeUntilAiring int `json:"timeUntilAiring"`
Episode int `json:"episode"` Episode int `json:"episode"`
} `json:"nextAiringEpisode"` } `json:"nextAiringEpisode"`
Genres []string `json:"genres"`
Tags []struct { Tags []struct {
Id int `json:"id"` Id int `json:"id"`
Name string `json:"name"` Name string `json:"name"`

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
}
func (a *App) GetMyAnimeListAnime(id int) (MALAnime, error) { return status
}
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
}
func (a *App) DeleteMyAnimeListEntry(id int) (bool, error) { return malAnime
}
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

@@ -1,166 +0,0 @@
# 🎉 AniTrack v0.7.0 Release Notes
## 🚨 Major Feature: Comprehensive Error Handling
This release brings **massive improvements** to application stability and user experience. The app will **no longer crash** when API services are down or experiencing errors!
---
## ✨ What's New
### 🛡️ Graceful API Error Handling
- **Application Stability**: App now remains running even when AniList, MAL, or Simkl APIs are down
- **Visual Error Feedback**: Beautiful modal dialogs display error details instead of silent crashes
- **Manual Retry**: Users can retry failed API connections with a single button click
- **Continue Using App**: Dismiss error messages and continue with limited functionality
### 🔧 Backend Improvements
#### AniList Integration
- Replaced all `log.Fatal()` calls with proper error returns
- Added error handling for API failures, network issues, and authentication errors
- `AniListSearch` now returns detailed error messages
- `GetAniListUserWatchingList` gracefully handles 403/404 responses
#### MyAnimeList Integration
- Updated `MALHelper` to return structured errors
- All MAL API functions now return errors instead of crashing:
- `GetMyAnimeList`
- `MyAnimeListUpdate`
- `GetMyAnimeListAnime`
- `DeleteMyAnimeListEntry`
- Fixed OAuth callback server to prevent shutdown on errors
#### Simkl Integration
- Updated `SimklHelper` with status code validation (200-299 range)
- All Simkl API functions now return errors:
- `SimklGetUserWatchlist`
- `SimklSyncEpisodes`
- `SimklSyncRating`
- `SimklSyncStatus`
- `SimklSearch`
- `SimklSyncRemove`
- Replaced `log.Fatal()` with proper error logging
### 💻 Frontend Improvements
#### New Error Management System
- **Centralized Error State**: New reactive stores for API errors
- `apiError` - Tracks current error with service, message, and status
- `isApiDown` - Quick flag for API availability
- **Error Helper Functions**:
- `setApiError()` - Set error state from any component
- `clearApiError()` - Reset error state
#### New Error Modal Component
- Auto-displays when API errors occur
- Shows service name, error message, and HTTP status code
- **"Retry Connection"** button - Attempts to reconnect to the failed service
- **"Dismiss"** button - Close modal and continue using the app
- Beautiful red-themed styling with Tailwind CSS
- Supports all three services (AniList, MAL, Simkl)
#### Updated User Interface
- Home page now displays "API Unavailable" state when services are down
- Helpful warning messages guide users during outages
- App structure remains visible and functional during errors
---
## 🐛 Bug Fixes
- **Critical**: Fixed app crashes when AniList API returns 403 Forbidden errors
- **Critical**: Fixed app crashes when AniList API is down or unreachable
- **Critical**: Fixed MAL OAuth server crashing on connection errors
- **Critical**: Fixed Simkl JSON marshaling errors crashing the app
- **Improved**: Better error messages for all API failures
---
## 📊 Technical Details
### Go Backend Changes
- **AniListFunctions.go**: Updated 3 functions with error handling
- **MALFunctions.go**: Updated 5 functions with error handling
- **MALUserFunctions.go**: Fixed server error handling
- **SimklFunctions.go**: Updated 7 functions with error handling
- **SimklUserFunctions.go**: Fixed 2 critical error handling issues
### Frontend Changes
- **ErrorModal.svelte**: New component (73 lines)
- **GlobalVariablesAndHelperFunctions.svelte**: Added error state system (27 lines)
- **App.svelte**: Integrated ErrorModal into main layout
- **CheckIfAniListLoggedInAndLoadWatchList.svelte**: Added error handling
- **Home.svelte**: Added API down state display
---
## 🎯 Impact
### Before (v0.6.5)
- ❌ App crashes to desktop when AniList API is down
- ❌ Users see only console error messages
- ❌ Must restart app after API failures
- ❌ No way to retry failed connections
### After (v0.7.0)
- ✅ App stays open during any API failures
- ✅ Beautiful modal dialogs explain what went wrong
- ✅ One-click retry to reconnect services
- ✅ Continue using app with limited functionality
---
## 🔄 Upgrading
No migration needed! This release is fully backward compatible.
Simply run:
```bash
git pull
wails build
```
Or download the latest release from the releases page.
---
## 🙏 Acknowledgments
This release addresses a critical stability issue that was affecting users during API service outages. Special thanks to the community for reporting these crashes and providing detailed error logs.
---
## 📝 Full Changelog
### Added
- Error modal component with retry/dismiss functionality
- Centralized error state management system
- API availability status tracking
- Visual feedback for all API failures
- Manual retry functionality for all services
### Changed
- AniList API functions to return errors instead of crashing
- MAL API functions to return errors instead of crashing
- Simkl API functions to return errors instead of crashing
- Error handling from console-only to UI modal dialogs
- App behavior from crash-on-error to graceful-degradation
### Fixed
- App crashes on AniList 403 errors
- App crashes on network failures
- MAL OAuth server shutdown on errors
- Simkl JSON marshaling crashes
- All `log.Fatal()` calls that terminated the app
### Removed
- `alert()` calls in favor of modal system
- Vite timestamp file (cleanup)
---
**Version**: 0.7.0
**Release Date**: March 2026
**Upgrade Difficulty**: ⭐ Easy (backward compatible)
**Recommended**: ⭐⭐⭐⭐⭐ Highly recommended for all users

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))

View File

@@ -33,7 +33,6 @@ body:graphql {
english english
native native
} }
genre
description description
coverImage { coverImage {
large large

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 {
aniListLoggedIn,
malLoggedIn,
simklLoggedIn,
watchlistNeedsRefresh,
aniListPrimary,
malPrimary,
simklPrimary,
malWatchList,
simklWatchList,
} from "./helperModules/GlobalVariablesAndHelperFunctions.svelte";
import {onMount} from "svelte"; import {onMount} from "svelte";
import Router from "svelte-spa-router"; import {userStore} from "./helperFunctions/userStore"
import Router from "svelte-spa-router"
import Home from "./routes/Home.svelte"; import Home from "./routes/Home.svelte";
import {wrap} from "svelte-spa-router/wrap"; import {wrap} from "svelte-spa-router/wrap";
import Spinner from "./helperComponents/Spinner.svelte"; import Spinner from "./helperComponents/Spinner.svelte";
import Header from "./helperComponents/Header.svelte"; import Header from "./helperComponents/Header.svelte";
import { CheckIfAniListLoggedInAndLoadWatchList } from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import { 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>
{#if $userStore.anilist.isLoggedIn}
<Header /> <Header />
<ErrorModal /> <Router routes={{
<Router '/': Home,
routes={{ '/anime/:id': wrap({
"/": Home, asyncComponent: () => import('./routes/AnimeRoutePage.svelte'),
"/anime/:id": wrap({ loadingComponent: Spinner
asyncComponent: () => import("./routes/AnimeRoutePage.svelte"),
conditions: [async () => await CheckIfAniListLoggedIn()],
loadingComponent: Spinner,
}), }),
// '*': "Not Found" // '*': "Not Found"
}} }} />
/> {/if}

View File

@@ -1,4 +1,4 @@
import type {AniListGetSingleAnime} from "../anilist/types/AniListCurrentUserWatchListType"; import type {AniListGetSingleAnime} from "../types/AniListCurrentUserWatchListType";
export const AniListGetSingleAnimeDefaultData: AniListGetSingleAnime = { export const AniListGetSingleAnimeDefaultData: AniListGetSingleAnime = {
data: { data: {
@@ -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
}
}

View File

@@ -1,16 +1,12 @@
<script lang="ts"> <script lang="ts">
import { import {
aniListAnime, aniListAnime,
aniListLoggedIn,
malAnime, malAnime,
malLoggedIn,
simklAnime, simklAnime,
simklLoggedIn,
watchlistNeedsRefresh,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"; } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import {userStore} from "../helperFunctions/userStore"
import { push } from "svelte-spa-router"; import { push } from "svelte-spa-router";
import WebsiteLink from "./WebsiteLink.svelte"; import type { AniListGetSingleAnime } from "../types/AniListCurrentUserWatchListType";
import type { AniListGetSingleAnime } from "../anilist/types/AniListCurrentUserWatchListType";
import Rating from "./Rating.svelte"; import Rating from "./Rating.svelte";
import { import {
convertAniListDateToString, convertAniListDateToString,
@@ -21,11 +17,14 @@
MALAnime, MALAnime,
MalListStatus, MalListStatus,
MALUploadStatus, MALUploadStatus,
} from "../mal/types/MALTypes"; } from "../types/MALTypes";
import type { SimklAnime } from "../simkl/types/simklTypes"; import type { SimklAnime } from "../types/simklTypes";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import type { StatusOption, StatusOptions } from "../helperTypes/StatusTypes"; import type {
import type { AniListUpdateVariables } from "../anilist/types/AniListTypes"; StatusOption,
StatusOptions,
} from "../types/StatusTypes";
import type { AniListUpdateVariables } from "../types/AniListTypes";
import { convertDateToAniList } from "../helperFunctions/convertDateToAniList"; import { convertDateToAniList } from "../helperFunctions/convertDateToAniList";
import { import {
AniListDeleteEntry, AniListDeleteEntry,
@@ -38,14 +37,10 @@
SimklSyncStatus, SimklSyncStatus,
} from "../../wailsjs/go/main/App"; } from "../../wailsjs/go/main/App";
import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte"; import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import Datepicker from "./Datepicker.svelte"; import Datepicker from "./Datepicker.svelte";
import {Badge, Tooltip} from "flowbite-svelte"; import {Badge, Tooltip} from "flowbite-svelte";
const re = /^([0-9]{4})-([0-9]{2})-([0-9]{2})/; const re = /^([0-9]{4})-([0-9]{2})-([0-9]{2})/;
let isAniListLoggedIn: boolean;
let isMalLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let currentAniListAnime: AniListGetSingleAnime; let currentAniListAnime: AniListGetSingleAnime;
let currentMalAnime: MALAnime; let currentMalAnime: MALAnime;
let currentSimklAnime: SimklAnime; let currentSimklAnime: SimklAnime;
@@ -53,9 +48,6 @@
let isSubmitting: boolean; let isSubmitting: boolean;
let submitSuccess = writable(false); let submitSuccess = writable(false);
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
malLoggedIn.subscribe((value) => (isMalLoggedIn = value));
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
aniListAnime.subscribe((value) => (currentAniListAnime = value)); aniListAnime.subscribe((value) => (currentAniListAnime = value));
malAnime.subscribe((value) => (currentMalAnime = value)); malAnime.subscribe((value) => (currentMalAnime = value));
simklAnime.subscribe((value) => (currentSimklAnime = value)); simklAnime.subscribe((value) => (currentSimklAnime = value));
@@ -79,7 +71,8 @@
{ id: 5, aniList: "REPEATING", mal: "rewatching", simkl: "watching" }, { id: 5, aniList: "REPEATING", mal: "rewatching", simkl: "watching" },
]; ];
let startingAnilistStatusOption: StatusOption = statusOptions.filter( let startingAnilistStatusOption: StatusOption = statusOptions.filter(
(option) => currentAniListAnime.data.MediaList.status === option.aniList, (option) =>
currentAniListAnime.data.MediaList.status === option.aniList,
)[0]; )[0];
let startedAtDate: Date | null = convertAniListDateToDate( let startedAtDate: Date | null = convertAniListDateToDate(
currentAniListAnime.data.MediaList.startedAt, currentAniListAnime.data.MediaList.startedAt,
@@ -88,7 +81,7 @@
currentAniListAnime.data.MediaList.completedAt, currentAniListAnime.data.MediaList.completedAt,
); );
if (isAniListLoggedIn) if ($userStore.anilist.isLoggedIn)
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `a-${currentAniListAnime.data.MediaList.mediaId}`, id: `a-${currentAniListAnime.data.MediaList.mediaId}`,
title, title,
@@ -106,15 +99,19 @@
notes: currentAniListAnime.data.MediaList.notes, notes: currentAniListAnime.data.MediaList.notes,
}); });
if (isMalLoggedIn) { if ($userStore.mal.isLoggedIn) {
let startDate = ""; let startDate = "";
let finishDate = ""; let finishDate = "";
if (currentMalAnime.my_list_status.start_date !== "") { if (currentMalAnime.my_list_status.start_date !== "") {
const startArray = re.exec(currentMalAnime.my_list_status.start_date); const startArray = re.exec(
currentMalAnime.my_list_status.start_date,
);
startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`; startDate = `${startArray[2]}-${startArray[3]}-${startArray[1]}`;
} }
if (currentMalAnime.my_list_status.finish_date !== "") { if (currentMalAnime.my_list_status.finish_date !== "") {
const finishArray = re.exec(currentMalAnime.my_list_status.finish_date); const finishArray = re.exec(
currentMalAnime.my_list_status.finish_date,
);
finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`; finishDate = `${finishArray[2]}-${finishArray[3]}-${finishArray[1]}`;
} }
AddAnimeServiceToTable({ AddAnimeServiceToTable({
@@ -131,7 +128,7 @@
}); });
} }
if (isSimklLoggedIn && Object.keys(currentSimklAnime).length > 0) if ($userStore.simkl.isLoggedIn && Object.keys(currentSimklAnime).length > 0)
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${currentSimklAnime.show.ids.simkl}`, id: `s-${currentSimklAnime.show.ids.simkl}`,
title: currentSimklAnime.show.title, title: currentSimklAnime.show.title,
@@ -191,9 +188,8 @@
submitData[key] = value; submitData[key] = value;
} }
try {
if ( if (
isAniListLoggedIn && $userStore.anilist.isLoggedIn &&
currentAniListAnime.data.MediaList.mediaId !== 0 currentAniListAnime.data.MediaList.mediaId !== 0
) { ) {
let body: AniListUpdateVariables = { let body: AniListUpdateVariables = {
@@ -206,11 +202,10 @@
startedAt: convertDateToAniList(startedAtDate), startedAt: convertDateToAniList(startedAtDate),
completedAt: convertDateToAniList(completedAtDate), completedAt: convertDateToAniList(completedAtDate),
}; };
await AniListUpdateEntry(body).then((value: AniListGetSingleAnime) => { await AniListUpdateEntry(body).then(
(value: AniListGetSingleAnime) => {
value.data.MediaList.media.tags = value.data.MediaList.media.tags =
currentAniListAnime.data.MediaList.media.tags; currentAniListAnime.data.MediaList.media.tags;
value.data.MediaList.media.genres =
currentAniListAnime.data.MediaList.media.genres;
aniListAnime.update((newValue) => { aniListAnime.update((newValue) => {
newValue = value; newValue = value;
return newValue; return newValue;
@@ -231,10 +226,11 @@
repeat: currentAniListAnime.data.MediaList.repeat, repeat: currentAniListAnime.data.MediaList.repeat,
notes: currentAniListAnime.data.MediaList.notes, notes: currentAniListAnime.data.MediaList.notes,
}); });
}); },
);
} }
if (malLoggedIn && currentMalAnime.id !== 0) { if ($userStore.mal.isLoggedIn && currentMalAnime.id !== 0) {
let body: MALUploadStatus = { let body: MALUploadStatus = {
status: submitData.status.mal, status: submitData.status.mal,
is_rewatching: submitData.repeat > 0, is_rewatching: submitData.repeat > 0,
@@ -248,7 +244,8 @@
(malAnimeReturn: MalListStatus) => { (malAnimeReturn: MalListStatus) => {
malAnime.update((value) => { malAnime.update((value) => {
value.my_list_status.status = malAnimeReturn.status; value.my_list_status.status = malAnimeReturn.status;
value.my_list_status.is_rewatching = malAnimeReturn.is_rewatching; value.my_list_status.is_rewatching =
malAnimeReturn.is_rewatching;
value.my_list_status.score = malAnimeReturn.score; value.my_list_status.score = malAnimeReturn.score;
value.my_list_status.num_episodes_watched = value.my_list_status.num_episodes_watched =
malAnimeReturn.num_episodes_watched; malAnimeReturn.num_episodes_watched;
@@ -275,22 +272,28 @@
id: `m-${currentMalAnime.id}`, id: `m-${currentMalAnime.id}`,
title: currentMalAnime.title, title: currentMalAnime.title,
service: "MyAnimeList", service: "MyAnimeList",
progress: currentMalAnime.my_list_status.num_episodes_watched, progress:
currentMalAnime.my_list_status.num_episodes_watched,
status: currentMalAnime.my_list_status.status, status: currentMalAnime.my_list_status.status,
startedAt: startDate, startedAt: startDate,
completedAt: finishDate, completedAt: finishDate,
score: currentMalAnime.my_list_status.score, score: currentMalAnime.my_list_status.score,
repeat: currentMalAnime.my_list_status.num_times_rewatched, repeat: currentMalAnime.my_list_status
.num_times_rewatched,
notes: currentMalAnime.my_list_status.comments, notes: currentMalAnime.my_list_status.comments,
}); });
}, },
); );
} }
if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) { if ($userStore.simkl.isLoggedIn && currentSimklAnime.show.ids.simkl !== 0) {
if (currentSimklAnime.watched_episodes_count !== submitData.episodes) { if (
await SimklSyncEpisodes(currentSimklAnime, submitData.episodes).then( currentSimklAnime.watched_episodes_count !== submitData.episodes
(value: SimklAnime) => { ) {
await SimklSyncEpisodes(
currentSimklAnime,
submitData.episodes,
).then((value: SimklAnime) => {
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${value.show.ids.simkl}`, id: `s-${value.show.ids.simkl}`,
title: value.show.title, title: value.show.title,
@@ -307,13 +310,14 @@
newValue = value; newValue = value;
return newValue; return newValue;
}); });
}, });
);
} }
if (currentSimklAnime.user_rating !== submitData.rating) { if (currentSimklAnime.user_rating !== submitData.rating) {
await SimklSyncRating(currentSimklAnime, submitData.rating).then( await SimklSyncRating(
(value) => { currentSimklAnime,
submitData.rating,
).then((value) => {
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${value.show.ids.simkl}`, id: `s-${value.show.ids.simkl}`,
title: value.show.title, title: value.show.title,
@@ -330,8 +334,7 @@
newValue = value; newValue = value;
return newValue; return newValue;
}); });
}, });
);
} }
if (currentSimklAnime.status !== submitData.status.simkl) { if (currentSimklAnime.status !== submitData.status.simkl) {
@@ -358,21 +361,16 @@
}); });
} }
} }
} catch (error) {
console.error("Error submitting changes:", error);
} finally {
submitting.set(false); submitting.set(false);
submitSuccess.set(true); submitSuccess.set(true);
watchlistNeedsRefresh.set(true);
setTimeout(() => submitSuccess.set(false), 2000); setTimeout(() => submitSuccess.set(false), 2000);
}
}; };
const deleteEntries = async () => { const deleteEntries = async () => {
submitting.set(true); submitting.set(true);
try {
if ( if (
isAniListLoggedIn && $userStore.anilist.isLoggedIn &&
currentAniListAnime.data.MediaList.mediaId !== 0 currentAniListAnime.data.MediaList.mediaId !== 0
) { ) {
await AniListDeleteEntry(currentAniListAnime.data.MediaList.id); await AniListDeleteEntry(currentAniListAnime.data.MediaList.id);
@@ -389,7 +387,7 @@
notes: "", notes: "",
}); });
} }
if (malLoggedIn && currentMalAnime.id !== 0) { if ($userStore.mal.isLoggedIn && currentMalAnime.id !== 0) {
await DeleteMyAnimeListEntry(currentMalAnime.id); await DeleteMyAnimeListEntry(currentMalAnime.id);
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `m-${currentMalAnime.id}`, id: `m-${currentMalAnime.id}`,
@@ -404,7 +402,7 @@
notes: "", notes: "",
}); });
} }
if (simklLoggedIn && currentSimklAnime.show.ids.simkl !== 0) { if ($userStore.simkl.isLoggedIn && currentSimklAnime.show.ids.simkl !== 0) {
await SimklSyncRemove(currentSimklAnime); await SimklSyncRemove(currentSimklAnime);
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${currentSimklAnime.show.ids.simkl}`, id: `s-${currentSimklAnime.show.ids.simkl}`,
@@ -419,14 +417,9 @@
notes: "", notes: "",
}); });
} }
} catch (error) {
console.error("Error deleting entries:", error);
} finally {
submitting.set(false); submitting.set(false);
submitSuccess.set(true); submitSuccess.set(true);
watchlistNeedsRefresh.set(true);
setTimeout(() => submitSuccess.set(false), 2000); setTimeout(() => submitSuccess.set(false), 2000);
}
}; };
let max = 999; let max = 999;
@@ -439,8 +432,10 @@
currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode !== 0 currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode !== 0
) { ) {
max = max =
currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode - 1; currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode -
1;
} }
</script> </script>
<form on:submit|preventDefault={handleSubmit} class="container pt-3 pb-10"> <form on:submit|preventDefault={handleSubmit} class="container pt-3 pb-10">
@@ -475,18 +470,21 @@
on:click={() => { on:click={() => {
currentAniListAnime.data.MediaList.progress -= 1; currentAniListAnime.data.MediaList.progress -= 1;
if ( if (
currentAniListAnime.data.MediaList.progress < currentAniListAnime.data.MediaList
currentAniListAnime.data.MediaList.media.episodes .progress <
currentAniListAnime.data.MediaList.media
.episodes
) { ) {
startingAnilistStatusOption = statusOptions[0]; startingAnilistStatusOption =
if (currentAniListAnime.data.MediaList.repeat === 0) statusOptions[0];
if (
currentAniListAnime.data.MediaList
.repeat === 0
)
completedAtDate = null; completedAtDate = null;
} }
}} }}
disabled={currentAniListAnime.data.MediaList.progress <= 0} 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"
class={currentAniListAnime.data.MediaList.progress <= 0
? "border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
: "bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"}
> >
<svg <svg
class="w-3 h-3 text-white" class="w-3 h-3 text-white"
@@ -512,18 +510,22 @@
id="episodes" id="episodes"
class="border border-x-0 p-2.5 h-11 text-center text-sm block w-full placeholder-gray-400 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none class="border border-x-0 p-2.5 h-11 text-center text-sm block w-full placeholder-gray-400 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none
{currentAniListAnime.data.MediaList.progress < 0 || {currentAniListAnime.data.MediaList.progress < 0 ||
(currentAniListAnime.data.MediaList.media.episodes > 0 && (currentAniListAnime.data.MediaList.media.episodes >
0 &&
currentAniListAnime.data.MediaList.progress > currentAniListAnime.data.MediaList.progress >
currentAniListAnime.data.MediaList.media.episodes) || currentAniListAnime.data.MediaList.media
(currentAniListAnime.data.MediaList.media.nextAiringEpisode .episodes) ||
.episode > 0 && (currentAniListAnime.data.MediaList.media
.nextAiringEpisode.episode > 0 &&
currentAniListAnime.data.MediaList.progress > currentAniListAnime.data.MediaList.progress >
currentAniListAnime.data.MediaList.media.nextAiringEpisode currentAniListAnime.data.MediaList.media
.episode - .nextAiringEpisode.episode -
1) 1)
? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500' ? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500'
: 'bg-gray-700 hover:bg-gray-600 border-gray-600 text-white focus:ring-blue-500 focus:border-blue-500'} w-24" : 'bg-gray-700 hover:bg-gray-600 border-gray-600 text-white focus:ring-blue-500 focus:border-blue-500'}"
bind:value={currentAniListAnime.data.MediaList.progress} bind:value={
currentAniListAnime.data.MediaList.progress
}
required required
/> />
<button <button
@@ -533,38 +535,27 @@
on:click={() => { on:click={() => {
currentAniListAnime.data.MediaList.progress += 1; currentAniListAnime.data.MediaList.progress += 1;
if ( if (
currentAniListAnime.data.MediaList.media.episodes === currentAniListAnime.data.MediaList.media
.episodes ===
currentAniListAnime.data.MediaList.progress currentAniListAnime.data.MediaList.progress
) { ) {
startingAnilistStatusOption = statusOptions[2]; startingAnilistStatusOption =
statusOptions[2];
completedAtDate = new Date(); completedAtDate = new Date();
} }
if (currentAniListAnime.data.MediaList.progress - 1 === 0) { if (
startingAnilistStatusOption = statusOptions[0]; currentAniListAnime.data.MediaList
if (startedAtDate === null) startedAtDate = new Date(); .progress -
1 ===
0
) {
startingAnilistStatusOption =
statusOptions[0];
if (startedAtDate === null)
startedAtDate = new Date();
} }
}} }}
disabled={(currentAniListAnime.data.MediaList.media.episodes > class="bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
0 &&
currentAniListAnime.data.MediaList.progress >=
currentAniListAnime.data.MediaList.media.episodes) ||
(currentAniListAnime.data.MediaList.media.nextAiringEpisode
.episode > 0 &&
currentAniListAnime.data.MediaList.progress >
currentAniListAnime.data.MediaList.media.nextAiringEpisode
.episode -
2)}
class={(currentAniListAnime.data.MediaList.media.episodes > 0 &&
currentAniListAnime.data.MediaList.progress >=
currentAniListAnime.data.MediaList.media.episodes) ||
(currentAniListAnime.data.MediaList.media.nextAiringEpisode
.episode > 0 &&
currentAniListAnime.data.MediaList.progress >
currentAniListAnime.data.MediaList.media.nextAiringEpisode
.episode -
2)
? "border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
: "bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"}
> >
<svg <svg
class="w-3 h-3 text-white" class="w-3 h-3 text-white"
@@ -584,15 +575,16 @@
</button> </button>
</div> </div>
<div> <div>
/ {currentAniListAnime.data.MediaList.media.nextAiringEpisode / {currentAniListAnime.data.MediaList.media
.episode !== 0 .nextAiringEpisode.episode !== 0
? currentAniListAnime.data.MediaList.media.nextAiringEpisode ? currentAniListAnime.data.MediaList.media
.episode - 1 .nextAiringEpisode.episode - 1
: currentAniListAnime.data.MediaList.media.episodes} : currentAniListAnime.data.MediaList.media.episodes}
</div> </div>
{#if currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode !== 0} {#if currentAniListAnime.data.MediaList.media.nextAiringEpisode.episode !== 0}
<div> <div>
of {currentAniListAnime.data.MediaList.media.episodes} of {currentAniListAnime.data.MediaList.media
.episodes}
</div> </div>
{/if} {/if}
</div> </div>
@@ -664,9 +656,10 @@
name="repeat" name="repeat"
min="0" min="0"
id="repeat" id="repeat"
class="border {currentAniListAnime.data.MediaList.repeat < 0 class="border {currentAniListAnime.data.MediaList
.repeat < 0
? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500' ? 'border-red-500 border-[2px] text-rose-300 focus:ring-red-500 focus:border-red-500'
: 'border-gray-500 text-white focus:ring-blue-500 focus:border-blue-500'} text-sm rounded-lg block w-24 p-2.5 bg-gray-600 placeholder-gray-400 text-white" : 'border-gray-500 focus:ring-blue-500 focus:border-blue-500'} text-sm rounded-lg block w-24 p-2.5 bg-gray-600 placeholder-gray-400 text-white"
bind:value={currentAniListAnime.data.MediaList.repeat} bind:value={currentAniListAnime.data.MediaList.repeat}
required required
/> />
@@ -700,9 +693,9 @@
<button <button
disabled={isSubmitting} disabled={isSubmitting}
id="sync-button" id="sync-button"
class="text-white {$submitSuccess class="text-white focus:ring-4 {$submitSuccess
? 'bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-800' ? 'bg-green-600 hover:bg-green-700 focus:ring-green-800'
: 'bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-800'} font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none" : 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-800'} font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none"
type="submit" type="submit"
> >
<svg <svg
@@ -728,12 +721,10 @@
Sync Changes Sync Changes
</button> </button>
<button <button
type="button"
class="text-white bg-gray-800 border border-gray-600 focus:outline-none hover:bg-gray-700 focus:ring-4 class="text-white bg-gray-800 border border-gray-600 focus:outline-none hover:bg-gray-700 focus:ring-4
focus:ring-gray-700 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:ring-gray-700 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2
hover:border-gray-600" hover:border-gray-600"
on:click={async () => { on:click={async () => {
await CheckIfAniListLoggedInAndLoadWatchList();
return push("/"); return push("/");
}} }}
> >
@@ -750,9 +741,9 @@
<button <button
disabled={isSubmitting} disabled={isSubmitting}
id="delete-button" id="delete-button"
class="text-white {$submitSuccess class="text-white focus:ring-4 {$submitSuccess
? 'bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-800' ? 'bg-green-600 hover:bg-green-700 focus:ring-green-800'
: 'bg-red-600 hover:bg-red-700 focus:ring-4 focus:ring-red-800'} font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none" : 'bg-red-600 hover:bg-red-700 focus:ring-red-800'} font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none"
on:click={deleteEntries} on:click={deleteEntries}
> >
<svg <svg
@@ -784,9 +775,9 @@
<button <button
disabled={isSubmitting} disabled={isSubmitting}
id="sync-button" id="sync-button"
class="text-white {$submitSuccess class="text-white focus:ring-4 {$submitSuccess
? 'bg-green-600 hover:bg-green-700 focus:ring-4 focus:ring-green-800' ? 'bg-green-600 hover:bg-green-700 focus:ring-green-800'
: 'bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:ring-blue-800'} font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none" : 'bg-blue-600 hover:bg-blue-700 focus:ring-blue-800'} font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none"
type="submit" type="submit"
> >
<svg <svg
@@ -816,7 +807,6 @@
focus:ring-gray-700 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:ring-gray-700 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2
hover:border-gray-600" hover:border-gray-600"
on:click={async () => { on:click={async () => {
await CheckIfAniListLoggedInAndLoadWatchList();
return push("/"); return push("/");
}} }}
> >
@@ -827,31 +817,14 @@
<div class="flex m-5"> <div class="flex m-5">
<div> <div>
<h3 class="text-2xl">Genres</h3>
{#each currentAniListAnime.data.MediaList.media.genres || [] as genre}
<div>
<Badge large border color="blue" class="m-1 w-52">
<div>
<WebsiteLink
id={genre}
url="https://anilist.co/search/anime/{genre}"
/>
</div>
</Badge>
<Tooltip>{genre}</Tooltip>
</div>
{/each}
<h3 class="text-2xl">Tags</h3> <h3 class="text-2xl">Tags</h3>
<div class="mt-2"> <div class="mt-2">
{#each currentAniListAnime.data.MediaList.media.tags as tag} {#each currentAniListAnime.data.MediaList.media.tags as tag}
<div> <div>
<Badge large border color="blue" class="m-1 w-52"> <Badge large border color="blue" class="m-1 w-52">
<div> <div>
<WebsiteLink {tag.name} -
id={tag.name} <span class="text-xs">{tag.rank}%</span>
url="https://anilist.co/search/anime/?genres={tag.name}"
/>
<span class="text-xs">({tag.rank}%)</span>
</div> </div>
</Badge> </Badge>
<Tooltip>{tag.description}</Tooltip> <Tooltip>{tag.description}</Tooltip>

View File

@@ -1,71 +1,36 @@
<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 {
aniListLoggedIn,
aniListUser,
malUser,
simklUser,
malLoggedIn,
simklLoggedIn,
loginToAniList,
loginToMAL,
loginToSimkl,
logoutOfAniList,
logoutOfMAL,
logoutOfSimkl,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import * as runtime from "../../wailsjs/runtime"; 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"; import { ShowVersion } from "../../wailsjs/go/main/App";
let currentAniListUser: AniListUser;
let currentMALUser: MyAnimeListUser;
let currentSimklUser: SimklUser;
let isAniListLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isMALLoggedIn: boolean;
aniListUser.subscribe((value) => (currentAniListUser = value));
malUser.subscribe((value) => (currentMALUser = value));
simklUser.subscribe((value) => (currentSimklUser = value));
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
function dropdownUser(): void { function dropdownUser(): void {
let dropdown = document.querySelector("#userDropdown"); let dropdown = document.querySelector("#userDropdown");
dropdown.classList.toggle("hidden"); dropdown.classList.toggle("hidden");
if (!dropdown.classList.contains("hidden")) { if (!dropdown.classList.contains("hidden")) {
document.addEventListener("click", clickOutside); 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} {:else}
<Avatar class="cursor-pointer" dot={{ color: "red" }} /> <Avatar class="cursor-pointer" dot={{ color: "red" }} />
@@ -76,8 +41,8 @@
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" 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"> <div class="px-4 py-3 text-sm text-white">
{#if isAniListLoggedIn} {#if $userStore.anilist.isLoggedIn}
<div>{currentAniListUser.data.Viewer.name}</div> <div>{$userStore.anilist.user.data.Viewer.name}</div>
{:else} {:else}
<div>You are not logged into AniList</div> <div>You are not logged into AniList</div>
{/if} {/if}
@@ -86,70 +51,62 @@
class="py-2 text-sm text-gray-200" class="py-2 text-sm text-gray-200"
aria-labelledby="dropdownUserAvatarButton" aria-labelledby="dropdownUserAvatarButton"
> >
{#if isAniListLoggedIn} {#if $userStore.anilist.isLoggedIn}
<li> <li>
<button <button
on:click={logoutOfAniList} on:click={() => userStore.logout("anilist")}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-green-800 hover:text-white" class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-green-800 hover:text-white"
> >
<span class="maple-font text-lg text-green-200 mr-4">A</span>Logout {currentAniListUser <span class="maple-font text-lg text-green-200 mr-4">A</span>Logout {$userStore.anilist.user.data.Viewer.name}
.data.Viewer.name}
</button> </button>
</li> </li>
{:else} {:else}
<li> <li>
<button <button on:click={() => {
on:click={() => { dropdownUser()
dropdownUser(); userStore.checkProvider("anilist")
loginToAniList();
}} }}
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 hover:text-white">
>
<span class="maple-font text-lg mr-4">A</span>Login to AniList <span class="maple-font text-lg mr-4">A</span>Login to AniList
</button> </button>
</li> </li>
{/if} {/if}
{#if isMALLoggedIn} {#if $userStore.mal.isLoggedIn}
<li> <li>
<button <button
on:click={logoutOfMAL} on:click={() => userStore.logout("mal")}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-blue-800 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 text-blue-200 mr-4">M</span>Logout {currentMALUser.name} <span class="maple-font text-lg text-blue-200 mr-4">M</span>Logout {$userStore.mal.user.name}
</button> </button>
</li> </li>
{:else} {:else}
<li> <li>
<button <button on:click={() => {
on:click={() => { dropdownUser()
dropdownUser(); userStore.checkProvider("mal")
loginToMAL();
}} }}
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 hover:text-white">
>
<span class="maple-font text-lg mr-4">M</span>Login to MyAnimeList <span class="maple-font text-lg mr-4">M</span>Login to MyAnimeList
</button> </button>
</li> </li>
{/if} {/if}
{#if isSimklLoggedIn} {#if $userStore.simkl.isLoggedIn}
<li> <li>
<button <button
on:click={logoutOfSimkl} on:click={() => userStore.logout("simkl")}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-indigo-800 hover:text-white" class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-indigo-800 hover:text-white"
> >
<span class="maple-font text-lg text-indigo-200 mr-4">S</span>Logout {currentSimklUser <span class="maple-font text-lg text-indigo-200 mr-4">S</span>Logout {$userStore.simkl.user.user.name}
.user.name}
</button> </button>
</li> </li>
{:else} {:else}
<li> <li>
<button <button on:click={() => {
on:click={() => { dropdownUser()
dropdownUser(); userStore.checkProvider("simkl")
loginToSimkl();
}} }}
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 hover:text-white">
>
<span class="maple-font text-lg mr-4">S</span>Login to Simkl <span class="maple-font text-lg mr-4">S</span>Login to Simkl
</button> </button>
</li> </li>
@@ -158,8 +115,8 @@
<div class="py-2"> <div class="py-2">
<button <button
on:click={() => { on:click={() => {
dropdownUser(); dropdownUser()
ShowVersion(); ShowVersion()
}} }}
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white" class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
> >
@@ -174,4 +131,3 @@
</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,94 +1,55 @@
<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"
}}
type="button"
class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded-lg min-[950px]:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-700 focus:ring-gray-600" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm rounded-lg min-[950px]:hidden focus:outline-none focus:ring-2 text-gray-400 hover:bg-gray-700 focus:ring-gray-600"
aria-controls="navbar-user" aria-controls="navbar-user" aria-expanded="false">
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>

View File

@@ -1,57 +1,46 @@
<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") &&
Number(e.currentTarget.value) !== page
)
ChangeWatchListPage(Number(e.currentTarget.value));
} }
function changeCountPerPage( function changeCountPerPage(e): void {
e: Event & { currentTarget: HTMLSelectElement }, GetAniListUserWatchingList(1, Number(e.target.value), MediaListSort.UpdatedTimeDesc).then((result) => {
): void { animePerPage.set(Number(e.target.value))
GetAniListUserWatchingList(1, Number(e.currentTarget.value), sort).then( watchListPage.set(1)
(result) => { aniListWatchlist.set(result)
animePerPage.set(Number(e.currentTarget.value)); aniListLoggedIn.set(true)
watchListPage.set(1); })
aniListWatchlist.set(result);
aniListLoggedIn.set(true);
},
);
} }
</script> </script>
<div class="mb-8"> <div class="mb-8">
@@ -60,19 +49,15 @@
<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 Previous
</button> </button>
</li> </li>
{:else} {:else}
<li> <li>
<button <button on:click={() => ChangeWatchListPage(page-1)}
on:click={() => ChangeWatchListPage(page - 1)} class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white">
class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white"
>
Previous Previous
</button> </button>
</li> </li>
@@ -80,37 +65,27 @@
{#each {length: aniListWatchListLoaded.data.Page.pageInfo.lastPage} as _, i} {#each {length: aniListWatchListLoaded.data.Page.pageInfo.lastPage} as _, i}
{#if i + 1 === page} {#if i + 1 === page}
<li> <li>
<button <button on:click={() => ChangeWatchListPage(i+1)}
on:click={() => ChangeWatchListPage(i + 1)} 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>
class="flex items-center justify-center px-4 h-10 leading-tight border hover:bg-gray-100 border-gray-700 bg-gray-700 text-white"
>{i + 1}</button
>
</li> </li>
{:else} {:else}
<li> <li>
<button <button on:click={() => ChangeWatchListPage(i+1)}
on:click={() => ChangeWatchListPage(i + 1)} class="flex items-center justify-center px-4 h-10 leading-tight border dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white">{i + 1}</button>
class="flex items-center justify-center px-4 h-10 leading-tight border dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white"
>{i + 1}</button
>
</li> </li>
{/if} {/if}
{/each} {/each}
{#if page === aniListWatchListLoaded.data.Page.pageInfo.lastPage} {#if page === aniListWatchListLoaded.data.Page.pageInfo.lastPage}
<li> <li>
<button <button disabled
disabled class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 cursor-default">
class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 cursor-default"
>
Next Next
</button> </button>
</li> </li>
{:else} {:else}
<li> <li>
<button <button on:click={() => ChangeWatchListPage(page+1)}
on:click={() => ChangeWatchListPage(page + 1)} class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white">
class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white"
>
Next Next
</button> </button>
</li> </li>
@@ -120,12 +95,8 @@
{/if} {/if}
<div class="flex mt-5"> <div class="flex mt-5">
<div class="w-20 mx-auto"> <div class="w-20 mx-auto">
<select <select bind:value={perPage} on:change={(e) => changeCountPerPage(e)} id="countPerPage"
bind:value={perPage} 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:change={(e) => changeCountPerPage(e)}
id="countPerPage"
class="border text-sm rounded-lg block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
>
{#each perPageOptions as option} {#each perPageOptions as option}
<option value={option}> <option value={option}>
{option} {option}
@@ -137,81 +108,35 @@
<div> <div>
<div>Total Anime: {aniListWatchListLoaded.data.Page.pageInfo.total}</div> <div>Total Anime: {aniListWatchListLoaded.data.Page.pageInfo.total}</div>
{#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12} {#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12}
<div class="md:hidden"> <div class="md:hidden">Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}</div>
Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}
</div>
{:else} {:else}
<div> <div>Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}</div>
Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}
</div>
{/if} {/if}
</div> </div>
<div class="max-w-xs mx-auto"> <div class="max-w-xs mx-auto">
<div class="relative flex items-center max-w-[11rem]"> <div class="relative flex items-center max-w-[11rem]">
<button <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"}
disabled={page <= 1}
>
<svg
class="w-3 h-3 text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 18 2"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M1 1h16"
/>
</svg> </svg>
</button> </button>
<input <input type="number" min="1" max="{aniListWatchListLoaded.data.Page.pageInfo.lastPage}"
type="number" on:keydown={changePage} id="page-counter"
min="1"
max={aniListWatchListLoaded.data.Page.pageInfo.lastPage}
on:keydown={changePage}
id="page-counter"
class="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none border-x-0 h-11 font-medium text-center text-sm block w-full pb-6 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500" class="[appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none border-x-0 h-11 font-medium text-center text-sm block w-full pb-6 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
value={page} value={page} required/>
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">
/>
<div
class="absolute bottom-1 start-1/2 -translate-x-1/2 rtl:translate-x-1/2 flex items-center text-xs text-gray-400 space-x-1 rtl:space-x-reverse"
>
<span>Page #</span> <span>Page #</span>
</div> </div>
<button <button type="button" id="increment-button" on:click={() => ChangeWatchListPage(page+1)}
type="button" class="hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none">
id="increment-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 18">
class={page >= aniListWatchListLoaded.data.Page.pageInfo.lastPage <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
? "border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none" d="M9 1v16M1 9h16"/>
: "bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"}
disabled={page >= aniListWatchListLoaded.data.Page.pageInfo.lastPage}
>
<svg
class="w-3 h-3 text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 18 18"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 1v16M1 9h16"
/>
</svg> </svg>
</button> </button>
</div> </div>

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 "../anilist/types/AniListCurrentUserWatchListType"; import type {AniListCurrentUserWatchList} from "../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} {#each aniListWatchListLoaded.data.Page.mediaList as media}
<div <div use:loader={loading}
use:loader={loading} class="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-lg xl:aspect-h-8 xl:aspect-w-7">
class="aspect-h-1 aspect-w-1 w-full overflow-hidden rounded-lg xl:aspect-h-8 xl:aspect-w-7"
>
<div class="flex flex-col items-center group"> <div class="flex flex-col items-center group">
<button <button on:click={() => {
on:click={() => { push(`#/anime/${media.media.id}`)
push(`#/anime/${media.media.id}`);
// loading.set(true)
// GetAniListSingleItem(media.media.id, true).then(() => {
// loading.set(false)
//
// })
}} }}
> >
<img <img class="rounded-lg" src={media.media.coverImage.large} alt={
class="rounded-lg w-[230px] h-[330px] object-cover" media.media.title.english === "" ?
src={media.media.coverImage.large} media.media.title.romaji :
alt={media.media.title.english === "" media.media.title.english
? media.media.title.romaji }/>
: media.media.title.english}
/>
</button> </button>
<Rating <Rating id="anime-rating" total={5} size={35} rating={media.score/2.0}/>
id="anime-rating" <button class="mt-4 text-md font-semibold text-white-700"
total={5} on:click={() => GetAnimeSingleItem(media.media.id, true)}>
size={35} {
rating={media.score / 2.0} media.media.title.english === "" ?
/> media.media.title.romaji :
<button media.media.title.english
class="mt-4 text-md font-semibold text-white-700" }
on:click={() => GetAnimeSingleItem(media.media.id, true)}
>
{media.media.title.english === ""
? media.media.title.romaji
: media.media.title.english}
</button> </button>
<p class="mt-1 text-lg font-medium text-white-900"> <p class="mt-1 text-lg font-medium text-white-900">{media.progress}
{media.progress} / {media.media.nextAiringEpisode.episode !== 0 ?
/ {media.media.nextAiringEpisode.episode !== 0 media.media.nextAiringEpisode.episode - 1 : media.media.episodes}</p>
? media.media.nextAiringEpisode.episode - 1
: media.media.episodes}
</p>
{#if media.media.episodes > 0} {#if media.media.episodes > 0}
<p class="mt-1 text-lg font-medium text-white-900"> <p class="mt-1 text-lg font-medium text-white-900">Total
Total Episodes: {media.media.episodes} Episodes: {media.media.episodes}</p>
</p>
{/if} {/if}
</div> </div>
</div> </div>
{/each} {/each}
</div> </div>
</div> </div>
{/if}
</div> </div>

View File

@@ -1,32 +1,28 @@
<script lang="ts"> <script lang="ts">
import { BrowserOpenURL } from "../../wailsjs/runtime"; import {BrowserOpenURL} from "../../wailsjs/runtime"
export let id: string; export let id: string
export let url = ""; let url = ""
let isAniList = false; let isAniList = false
let isMAL = false; let isMAL = false
let isSimkl = false; let isSimkl = false
let newId = id; let newId = id
let re = /[ams]?-?(.*)/; let re = /[ams]?-?(.*)/
if (id !== undefined && id.length > 0) { if (id !== undefined && id.length > 0) {
isAniList = id.includes("a-"); isAniList = id.includes("a-")
isMAL = id.includes("m-"); isMAL = id.includes("m-")
isSimkl = id.includes("s-"); isSimkl = id.includes("s-")
if (isAniList || isMAL || isSimkl) newId = id.match(re)[1]; newId = id.match(re)[1]
else newId = id;
} }
if (isAniList) url = `https://anilist.co/anime/${newId}`;
if (isMAL) url = `https://myanimelist.net/anime/${newId}`; if (isAniList) url = `https://anilist.co/anime/${newId}`
if (isSimkl) url = `https://simkl.com/anime/${newId}`; if (isMAL) url = `https://myanimelist.net/anime/${newId}`
if (isSimkl) url = `https://simkl.com/anime/${newId}`
</script> </script>
{#if url.length > 0} {#if url.length > 0}
<button <button class="underline underline-offset-2 px-4 py-1" on:click={() => BrowserOpenURL(url)}>{newId}</button>
type="button"
class="underline underline-offset-2 px-4 py-1"
on:click={() => BrowserOpenURL(url)}>{newId}</button
>
{:else} {:else}
{id} {id}
{/if} {/if}

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,
GetAniListUserWatchingList,
GetMyAnimeListAnime, GetMyAnimeListAnime,
GetMyAnimeListLoggedInUser, SimklSearch
GetSimklLoggedInUser,
LogoutAniList,
LogoutMyAnimeList,
LogoutSimkl,
SimklGetUserWatchlist,
SimklSearch,
} from "../../wailsjs/go/main/App"; } from "../../wailsjs/go/main/App";
import {userStore} from "../helperFunctions/userStore";
import type { import type {
AniListCurrentUserWatchList, AniListGetSingleAnime
AniListGetSingleAnime, } from "../types/AniListCurrentUserWatchListType.js";
} from "../anilist/types/AniListCurrentUserWatchListType.js"; import {get, writable} from 'svelte/store'
import { writable } from "svelte/store"; import type {SimklAnime, SimklWatchList} from "../types/simklTypes";
import type { import type {MALAnime, MALWatchlist} from "../types/MALTypes";
SimklAnime, import type {TableItems} from "../types/TableTypes";
SimklUser, import {AniListGetSingleAnimeDefaultData} from "../defaults/AniListGetSingleAnime";
SimklWatchList, import {AniListWatchListDefaultData} from "../defaults/AniListWatchListDefaultData";
} 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);
export const isApiDown = writable(false);
export function setApiError(
service: string,
message: string,
statusCode?: string,
canRetry: boolean = true,
) {
apiError.set({
service,
message,
statusCode,
canRetry,
});
isApiDown.set(true);
}
export function clearApiError() {
apiError.set(null);
isApiDown.set(false);
}
export async function GetAnimeSingleItem(
aniId: number,
login: boolean,
): Promise<""> {
await GetAniListItem(aniId, login).then((aniListResult) => {
let finalResult: AniListGetSingleAnime;
finalResult = aniListResult;
if (login === false) { if (login === false) {
finalResult.data.MediaList.status = ""; finalResult.data.MediaList.status = ""
finalResult.data.MediaList.score = 0; finalResult.data.MediaList.score = 0
finalResult.data.MediaList.progress = 0; finalResult.data.MediaList.progress = 0
finalResult.data.MediaList.notes = ""; finalResult.data.MediaList.notes = ""
finalResult.data.MediaList.repeat = 0; finalResult.data.MediaList.repeat = 0
finalResult.data.MediaList.startedAt.day = 0; finalResult.data.MediaList.startedAt.day = 0
finalResult.data.MediaList.startedAt.month = 0; finalResult.data.MediaList.startedAt.month = 0
finalResult.data.MediaList.startedAt.year = 0; finalResult.data.MediaList.startedAt.year = 0
finalResult.data.MediaList.completedAt.day = 0; finalResult.data.MediaList.completedAt.day = 0
finalResult.data.MediaList.completedAt.month = 0; finalResult.data.MediaList.completedAt.month = 0
finalResult.data.MediaList.completedAt.year = 0; finalResult.data.MediaList.completedAt.year = 0
} }
aniListAnime.set(finalResult); aniListAnime.set(finalResult)
title.set( title.set(currentAniListAnime.data.MediaList.media.title.english === "" ?
currentAniListAnime.data.MediaList.media.title.english === "" currentAniListAnime.data.MediaList.media.title.romaji :
? currentAniListAnime.data.MediaList.media.title.romaji currentAniListAnime.data.MediaList.media.title.english)
: currentAniListAnime.data.MediaList.media.title.english, })
); if (store.mal.isLoggedIn) {
}); await GetMyAnimeListAnime(currentAniListAnime.data.MediaList.media.idMal).then(malResult => {
if (isMalLoggedIn) { malAnime.set(malResult)
await GetMyAnimeListAnime( })
currentAniListAnime.data.MediaList.media.idMal,
).then((malResult) => {
malAnime.set(malResult);
});
} }
if (isSimklLoggedIn) { if (store.simkl.isLoggedIn) {
await SimklSearch(currentAniListAnime.data.MediaList).then( await SimklSearch(currentAniListAnime.data.MediaList).then((value: SimklAnime) => {
(value: SimklAnime) => { simklAnime.set(value)
simklAnime.set(value); })
},
);
} }
return ""; 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> </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

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

View File

@@ -44,7 +44,6 @@ export interface MediaList {
timeUntilAiring: number; timeUntilAiring: number;
episode: number; episode: number;
}; };
genres: string[];
tags: [ tags: [
{ {
id: number; id: number;

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

@@ -364,7 +364,7 @@ export namespace main {
id: number; id: number;
mediaId: number; mediaId: number;
userId: number; userId: number;
// Go type: struct { ID int "json:\"id\""; IDMal int "json:\"idMal\""; Title struct { Romaji string "json:\"romaji\""; English string "json:\"english\""; Native string "json:\"native\"" } "json:\"title\""; Description string "json:\"description\""; CoverImage struct { Large string "json:\"large\"" } "json:\"coverImage\""; Season string "json:\"season\""; SeasonYear int "json:\"seasonYear\""; Status string "json:\"status\""; Episodes int "json:\"episodes\""; NextAiringEpisode struct { AiringAt int "json:\"airingAt\""; TimeUntilAiring int "json:\"timeUntilAiring\""; Episode int "json:\"episode\"" } "json:\"nextAiringEpisode\""; Genres []string "json:\"genres\""; Tags []struct { Id int "json:\"id\""; Name string "json:\"name\""; Description string "json:\"description\""; Rank int "json:\"rank\""; IsMediaSpoiler bool "json:\"isMediaSpoiler\""; IsAdult bool "json:\"isAdult\"" } "json:\"tags\""; IsAdult bool "json:\"isAdult\"" } // Go type: struct { ID int "json:\"id\""; IDMal int "json:\"idMal\""; Title struct { Romaji string "json:\"romaji\""; English string "json:\"english\""; Native string "json:\"native\"" } "json:\"title\""; Description string "json:\"description\""; CoverImage struct { Large string "json:\"large\"" } "json:\"coverImage\""; Season string "json:\"season\""; SeasonYear int "json:\"seasonYear\""; Status string "json:\"status\""; Episodes int "json:\"episodes\""; NextAiringEpisode struct { AiringAt int "json:\"airingAt\""; TimeUntilAiring int "json:\"timeUntilAiring\""; Episode int "json:\"episode\"" } "json:\"nextAiringEpisode\""; Tags []struct { Id int "json:\"id\""; Name string "json:\"name\""; Description string "json:\"description\""; Rank int "json:\"rank\""; IsMediaSpoiler bool "json:\"isMediaSpoiler\""; IsAdult bool "json:\"isAdult\"" } "json:\"tags\""; IsAdult bool "json:\"isAdult\"" }
media: any; media: any;
status: string; status: string;
// Go type: struct { Year int "json:\"year\""; Month int "json:\"month\""; Day int "json:\"day\"" } // Go type: struct { Year int "json:\"year\""; Month int "json:\"month\""; Day int "json:\"day\"" }

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"
} }
} }