51 Commits

Author SHA1 Message Date
a3b4857175 docs: add release notes for v0.7.0
Comprehensive release notes documenting:
- Major error handling improvements
- Backend API changes (AniList, MAL, Simkl)
- Frontend error modal and state management
- Bug fixes and stability improvements
- Technical details and upgrade instructions
2026-03-30 20:12:47 -04:00
6e4b9c041a chore: remove vite timestamp file
Remove generated vite timestamp file that was accidentally tracked.
2026-03-30 20:08:39 -04:00
ae54fd20dd feat(frontend): integrate error handling into application
App.svelte:
- Import and render ErrorModal component
- Add ErrorModal to main app layout below Header

CheckIfAniListLoggedInAndLoadWatchList.svelte:
- Import error state helpers (setApiError, clearApiError)
- Wrap LoadAniListUser in try-catch with error handling
- Wrap LoadAniListWatchList in try-catch with error handling
- Update CheckIfAniListLoggedInAndLoadWatchList with error handling
- Remove old alert() calls in favor of modal system

Home.svelte:
- Import isApiDown and apiError stores
- Add conditional rendering for API down state
- Display user-friendly "API Unavailable" message when apiError is set
- Show warning icon and helpful messaging

Error handling is now fully integrated across the frontend application.
2026-03-30 20:08:34 -04:00
2c7e2d0eff feat(frontend): add error modal component
Add new ErrorModal component for API error display:
- Auto-displays when apiError store is set
- Shows service name, error message, and status code
- Provides "Retry Connection" button to attempt reconnection
- Provides "Dismiss" button to close modal and continue
- Integrates with all three services (AniList, MAL, Simkl)
- Uses flowbite-svelte Modal and Button components
- Proper Svelte event handling with on:click

Uses Tailwind CSS for styling with red error theme and helpful messaging.
2026-03-30 20:08:29 -04:00
9a9f055c38 feat(frontend): add API error state management
Add centralized error state system:
- New ApiError interface (service, message, statusCode, canRetry)
- apiError writable store for current error state
- isApiDown writable store for API availability status
- setApiError() helper to set error states
- clearApiError() helper to reset error states

Provides reactive error state across entire application.
2026-03-30 20:08:23 -04:00
bc497521e7 feat(simkl): add comprehensive error handling
SimklFunctions.go:
- Update SimklHelper to return (json.RawMessage, error)
- Add status code validation (200-299 range)
- Update SimklGetUserWatchlist to return (SimklWatchListType, error)
- Update SimklSyncEpisodes to return (SimklAnime, error)
- Update SimklSyncRating to return (SimklAnime, error)
- Update SimklSyncStatus to return (SimklAnime, error)
- Update SimklSearch to return (SimklAnime, error)
- Update SimklSyncRemove to return (bool, error)
- Add proper error wrapping for all failure scenarios

SimklUserFunctions.go:
- Replace log.Fatalf with log.Printf in server error handling (line 119)
- Replace log.Fatal with log.Printf in JSON marshaling (line 141)

All Simkl API calls now properly propagate errors to frontend.
2026-03-30 20:08:20 -04:00
48a6005725 feat(mal): add comprehensive error handling
MALFunctions.go:
- Update MALHelper to return (json.RawMessage, string, error)
- Add network error handling and proper request error checking
- Update GetMyAnimeList to return (MALWatchlist, error)
- Update MyAnimeListUpdate to return (MalListStatus, error)
- Update GetMyAnimeListAnime to return (MALAnime, error)
- Update DeleteMyAnimeListEntry to return (bool, error)

MALUserFunctions.go:
- Replace log.Fatalf with log.Printf in server error handling
- Prevent server shutdown on OAuth callback errors

All MAL API calls now properly propagate errors to frontend.
2026-03-30 20:08:16 -04:00
93a94d0797 chore: bump version to 0.7.0
Preparing release for comprehensive error handling improvements across all API integrations (AniList, MAL, Simkl).
2026-03-30 20:08:08 -04:00
a2576b044c feat: implement dynamic sort parameter for AniList watchlist
Add configurable sort functionality to AniList watchlist system:

- Add aniListSort writable store to GlobalVariablesAndHelperFunctions
- Update Pagination component to subscribe to and use dynamic sort parameter
- Refactor CheckIfAniListLoggedInAndLoadWatchList to use sort from store
- Remove hardcoded MediaListSort.UpdatedTimeDesc in favor of configurable sort
- Improve code formatting with arrow functions and consistent spacing
- Add sort parameter to all GetAniListUserWatchingList calls

This allows users to customize their watchlist sorting preference instead of being limited to the default 'updated time descending' sort order.
2026-03-29 10:23:58 -04:00
2ee2d85e9e feat: add Sort component placeholder
Add new Sort component to infrastructure for future sort functionality implementation in the watchlist UI.
2026-03-29 10:23:55 -04:00
1090f112f3 refactor: standardize code formatting in AvatarMenu component
- Convert from 4-space to 2-space indentation for consistency
- Improve import statement formatting and alignment
- Fix semicolon usage throughout the component
- Add conditional avatar dot indicator: green when all services (AniList, MAL, Simkl) are logged in, yellow otherwise
- Add missing newline at end of file
- General code style improvements for better readability and consistency with project standards
2026-03-22 22:00:48 -04:00
58c9f449e0 chore: bump version to 0.6.5 2026-03-22 21:46:05 -04:00
f016c90353 chore: bump version to 0.6.5
Update product version in preparation for release.
2026-03-22 21:45:57 -04:00
6bbe0f0f48 Add disabled state to pagination navigation buttons
- Disable decrement button when on first page (page <= 1)
- Disable increment button when on last page (page >= lastPage)
- Prevents users from triggering invalid page navigation requests
- Improves UX by providing visual feedback for boundary conditions

This change prevents unnecessary API calls and improves user experience by clearly indicating when navigation bounds have been reached. The buttons will now be disabled at the appropriate boundaries, matching the behavior already present in the numbered page navigation section.
2026-03-22 21:17:20 -04:00
d841fee1e7 Fix TypeScript type safety in Pagination event handlers
- Add proper type annotations to changePage function parameter using KeyboardEvent with HTMLInputElement currentTarget
- Add proper type annotations to changeCountPerPage function parameter using Event with HTMLSelectElement currentTarget
- Replace all e.target references with e.currentTarget to access properly typed DOM elements
- Add hover state styling to active page button for better UI feedback

This change resolves TypeScript errors where EventTarget type didn't have access to element-specific properties like 'value'. Using currentTarget instead of target provides the correct type since currentTarget refers to the element that has the event listener attached, ensuring type-safe access to input and select element properties.
2026-03-22 21:09:13 -04:00
f29d8f378e Format Pagination component code for consistency
- Update indentation from 4-space to 2-space convention throughout the component
- Reformat import statements to follow consistent spacing and alignment
- Standardize function and variable declarations with proper spacing
- Improve code readability with consistent formatting across all elements
- Clean up HTML template structure with proper indentation
- Add newline at end of file for standards compliance

This change ensures the Pagination component follows the project's code style conventions and improves maintainability through consistent formatting.
2026-03-22 20:50:24 -04:00
35e93c0ca9 Fix media cover image sizing in WatchList component
- Add explicit width (230px) and height (330px) to media cover images
- Apply object-cover class to maintain aspect ratio and prevent distortion
- Ensures uniform sizing across all media cover images in the watch list

This change improves visual consistency in the UI by standardizing the display dimensions of anime/manga cover images.
2026-03-22 20:48:56 -04:00
8c169d549a Add disabled state constraints to progress adjustment buttons in Anime component
- Disable decrement button when progress is at 0 or below to prevent negative values
- Disable increment button when:
  * Media has defined episodes and progress is complete (>= total episodes)
  * Or when progress has reached next airing episode boundary (nextAiringEpisode - 2)
- Improves user experience by preventing invalid progress adjustments
- Maintains data integrity by stopping users from setting impossible progress values
2026-03-22 20:27:15 -04:00
b2a8a504f3 fix: resolve syntax error in App.svelte
Fixed TypeScript compilation error caused by import statement and function declaration being on the same line.

Changes:
- Separated import statement and onMount declaration onto different lines
- Resolved svelte-preprocess type error
- File now compiles correctly

This was a typo from previous commit where the loc import line was incorrectly merged with the existing onMount function declaration.
2026-03-21 13:28:33 -04:00
c85a53a278 chore: remove VSCode extensions.json
Removed the .vscode/extensions.json file from the frontend directory.

This file contained workspace-level VSCode extension recommendations which are better managed:
- At user level through personal VSCode settings
- Through project README documentation
- Via devcontainer or editors preferences if needed

Cleanup reduces repository clutter and avoids imposing specific extension recommendations on contributors.
2026-03-21 13:25:49 -04:00
2cf3844e76 chore: bump version to 0.6.0
Incremented version from 0.5.3 to 0.6.0 for release with new features.

This release includes:
- Smart watchlist refresh on navigation
- Improved WatchList UI with manual refresh button
- Client-side routing for logo navigation
- Better UX with automatic data updates

Version bump reflects significant feature additions and improvements to the user experience.
2026-03-21 13:25:46 -04:00
6ed5fe8b71 feat: improve WatchList UI with refresh button
Enhanced the WatchList component with better layout and manual refresh functionality.

Changes:
- Added Refresh WatchList button with loading state handling
- Restructured header layout using flexbox with justify-between
- Title on left, refresh button on right, vertically aligned with items-center
- Improved button styling with consistent py-2 px-4 padding
- Added CheckIfAniListLoggedInAndLoadWatchList import for refresh functionality
- Maintained mb-4 spacing for consistent vertical rhythm

This gives users manual control over watchlist updates and provides better visual balance to the header section.

UI improvements:
- Horizontal flex container for proper left/right alignment
- Responsive button sizing
- Clear visual separation between title and action
2026-03-21 13:25:45 -04:00
8a8baf7f8f feat: implement smart watchlist refresh on navigation
Added intelligent watchlist refresh mechanism that only refetches data when changes are actually made, preventing unnecessary API calls and improving performance.

Changes:
- Added watchlistNeedsRefresh store to track when watchlist data has changed
- Implemented reactive watcher in App.svelte that uses svelte-spa-router's loc store to detect navigation to home
- Set dirty flag in Anime.svelte after successful status updates and entry deletions
- Added conditional refresh logic that checks user's primary service (AniList, MAL, or Simkl)
- Parallel refresh support for multiple services when logged in

This resolves the issue where clicking the logo would cause full page reloads and unnecessary re-authentication checks, while also ensuring watchlist data stays current when users make changes.

Technical details:
- Uses $loc.location to detect route changes
- IIFE pattern for async operations in reactive statements
- Only refreshes for logged-in primary services
- Flag resets after successful refresh

Related to: Header.svelte client-side routing fix
2026-03-21 13:25:43 -04:00
ca8c8beaf3 Bump version to 0.5.3
- Update productVersion in wails.json from 0.5.2 to 0.5.3
2026-03-20 15:53:33 -04:00
3e7f7d1c95 fix(frontend): resolve submit spinner hang and data loss issues
- Add try-catch-finally error handling to handleSubmit and deleteEntries
  functions to ensure submitting state is always reset, even when API calls
  fail or timeout. This fixes the infinite loading spinner bug.

- Preserve genres field after AniList updates, matching the existing tags
  preservation pattern. Prevents genres array from being lost after form
  submission, which was causing "{#each} only works with iterable values"
  error when the page re-rendered.

- Add fallback (|| []) to genres each block to prevent rendering errors
  when genres is undefined or null for entries without genre data.

These fixes ensure robust error handling and data consistency during anime
list updates across AniList, MAL, and Simkl services.

Fixes: submit button spinner never stopping after form submission
Fixes: "{#each} only works with iterable values" error on genres display
2026-03-20 15:51:55 -04:00
b0ca864dfe chore: exclude build tarball artifacts from version control
Add *.tar.gz pattern to build directory exclusion in .gitignore to prevent
build artifacts like Anitrack-0.5.2.tar.gz from being committed to the
repository. These generated files are ephemeral build outputs that should
not be tracked in version control.
2026-03-20 15:51:37 -04:00
5ed6dedeab chore(version): bump version to 0.5.2
Increment version number from 0.5.1 to 0.5.2 for the Logo click fix
2026-03-20 11:02:15 -04:00
3271af445a chore(version): bump version to 0.5.1
Increment version number from 0.5.0 to 0.5.1 for the upcoming release.
2026-03-20 10:59:07 -04:00
e7e9e5b826 bugfix(frontend): added use:link to the logos href to prevent full page reload
Bug: Every click of the logo would consistently do a full page reload
- Logo clicking now uses svelte-spa-router's link

Apply consistent formatting to Header.svelte:
- Add semicolons to all statements
- Improve JSX/HTML attribute formatting for better readability
- Add link import from svelte-spa-router for SPA navigation
- Format multi-line attributes with proper indentation

These changes improve code consistency and maintainability without
altering any functionality.
2026-03-20 10:59:07 -04:00
5337758dee chore(rest): remove obsolete REST API test files
Remove all .http test files and environment configuration used for API testing.
These files were used during development for testing AniList, MAL, and Simkl API endpoints
but are no longer needed as the application has matured.

Removed files:
- rest/AniTrack/ - AniList API test endpoints (search, queries, mutations, OAuth)
- rest/MAL/ - MyAnimeList API test endpoints (OAuth, anime lists, updates)
- rest/Simkl/ - Simkl API test endpoints (OAuth, watchlist, updates)
- rest/http-client.env.json - Environment configuration for test files
2026-03-20 10:56:34 -04:00
426793e56a docs: update AniList search query in Bruno collection
Add the genre field to the AniList Search GraphQL query in the Bruno API
collection. This keeps the API documentation collection in sync with the
application's query structure, allowing for testing and verification of
genre data retrieval from the AniList API.
2026-03-19 21:09:27 -04:00
a794b77654 build: update Wails generated models for genres support
Regenerate the Wails TypeScript models to include the new Genres field
in the MediaList type definition. This is an auto-generated file that
reflects the updated Go backend type structure with the genres []string
field added to the media object.
2026-03-19 21:09:27 -04:00
c510c2a138 feat(frontend): add genre display UI and enhance link component
- Anime.svelte: Add genre display section with clickable badges that link
  to AniList search results for each genre. Genres are now displayed above
  the existing tags section with consistent styling.

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

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

This enhancement allows the application to retrieve and display anime genre
information from the AniList API, providing users with better categorization
and discovery capabilities.
2026-03-19 21:06:46 -04:00
d70153064f upgraded go packages due to CVEs 2025-12-24 11:28:38 -05:00
7960f8e26d removed unused imports 2025-12-23 23:33:25 -05:00
2e5a4a4493 finally fixed being able to reload the anime page when searching or changing the url 2025-12-21 13:01:51 -05:00
874b3952ee upped version to 0.4 due to big change 2025-12-21 01:15:39 -05:00
60eac10545 user dropdown now closes when certain actions are taken or clicked outside of dialog 2025-12-21 01:14:46 -05:00
6d66d711ff added a showversion button with popup to the interface 2025-12-21 01:14:05 -05:00
cd62e6c658 fixed MAL not auto logging in when app starts 2025-12-21 00:25:48 -05:00
063c5016f3 fixed MAL not auto logging in when app starts 2025-12-21 00:24:25 -05:00
5c4caf68e6 added tooltip and version change 2025-06-22 21:08:56 -04:00
c10e853564 updated minor version for tag fix 2025-06-06 23:25:24 -04:00
cd043d093f fixed issue where tags would not reload after submit 2025-06-06 23:25:08 -04:00
6db01f7f9f added tags to frontend anime item 2025-05-14 14:31:43 -04:00
8460d56d55 created simple linux install script for AniTrack 2025-05-07 10:03:58 -04:00
8fedbe4607 created xdg icon set instead of one big icon 2025-05-07 10:03:37 -04:00
e069c47242 automated adding anime by moving progress from 0 to 1 2025-03-30 10:03:05 -04:00
aba0f2d1d5 removed console.log 2025-03-30 10:02:38 -04:00
62 changed files with 1816 additions and 1697 deletions

3
.gitignore vendored
View File

@@ -33,3 +33,6 @@ 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,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"log" "log"
"net/http" "net/http"
@@ -93,6 +94,7 @@ func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
timeUntilAiring timeUntilAiring
episode episode
} }
genres
tags{ tags{
id id
name name
@@ -182,7 +184,7 @@ func (a *App) GetAniListItem(aniId int, login bool) AniListGetSingleAnime {
return post return post
} }
func (a *App) AniListSearch(query string) any { func (a *App) AniListSearch(query string) (interface{}, error) {
type Variables struct { type Variables struct {
Search string `json:"search"` Search string `json:"search"`
ListType string `json:"listType"` ListType string `json:"listType"`
@@ -222,6 +224,7 @@ func (a *App) AniListSearch(query string) any {
timeUntilAiring timeUntilAiring
episode episode
} }
genres
tags{ tags{
id id
name name
@@ -240,18 +243,20 @@ func (a *App) AniListSearch(query string) any {
ListType: "ANIME", ListType: "ANIME",
}, },
} }
returnedBody, _ := AniListQuery(body, false) returnedBody, status := AniListQuery(body, false)
if status != "200 OK" {
return nil, fmt.Errorf("API search failed with status: %s", status)
}
var post interface{} var post interface{}
err := json.Unmarshal(returnedBody, &post) err := json.Unmarshal(returnedBody, &post)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return nil, fmt.Errorf("Failed to parse search results")
}
return post, nil
} }
return post func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) (AniListCurrentUserWatchList, error) {
}
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"`
@@ -307,6 +312,7 @@ func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) Ani
timeUntilAiring timeUntilAiring
episode episode
} }
genres
tags{ tags{
id id
name name
@@ -401,11 +407,15 @@ func (a *App) GetAniListUserWatchingList(page int, perPage int, sort string) Ani
err := json.Unmarshal(returnedBody, &badPost) err := json.Unmarshal(returnedBody, &badPost)
if err != nil { if err != nil {
log.Printf("Failed at unmarshal, %s\n", err) log.Printf("Failed at unmarshal, %s\n", err)
return post, fmt.Errorf("API authentication error")
} }
log.Fatal(badPost.Errors[0].Message) return post, fmt.Errorf("AniList API error: %s", badPost.Errors[0].Message)
}
if status != "200 OK" {
return post, fmt.Errorf("API request failed with status: %s", status)
} }
return post return post, nil
} }
func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSingleAnime { func (a *App) AniListUpdateEntry(updateBody AniListUpdateVariables) AniListGetSingleAnime {

View File

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

View File

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

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.Fatalf("listen: %s\n", err) log.Printf("Server error: %s\n", err)
} }
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
}() }()
@@ -308,7 +308,16 @@ func refreshMyAnimeListAuthorizationToken() {
func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser { func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser {
a.MyAnimeListLogin() a.MyAnimeListLogin()
user := createUser()
if user.Name == "" {
refreshMyAnimeListAuthorizationToken()
user = createUser()
}
return user
}
func createUser() MyAnimeListUser {
client := &http.Client{} client := &http.Client{}
req, _ := http.NewRequest("GET", "https://api.myanimelist.net/v2/users/@me?fields=anime_statistics", nil) req, _ := http.NewRequest("GET", "https://api.myanimelist.net/v2/users/@me?fields=anime_statistics", nil)
@@ -335,6 +344,7 @@ func (a *App) GetMyAnimeListLoggedInUser() MyAnimeListUser {
} }
return user return user
} }
func (a *App) LogoutMyAnimeList() string { func (a *App) LogoutMyAnimeList() string {

166
RELEASE_NOTES_v0.7.0.md Normal file
View File

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

View File

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

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.Fatalf("listen: %s\n", err) log.Printf("Server error: %s\n", err)
} }
fmt.Println("Shutting down...") fmt.Println("Shutting down...")
}() }()
@@ -138,7 +138,8 @@ func getSimklAuthorizationToken(content string) SimklJWT {
} }
jsonData, err := json.Marshal(data) jsonData, err := json.Marshal(data)
if err != nil { if err != nil {
log.Fatal(err) log.Printf("Failed to marshal data: %s\n", err)
return SimklJWT{}
} }
response, err := http.NewRequest("POST", "https://api.simkl.com/oauth/token", bytes.NewBuffer(jsonData)) response, err := http.NewRequest("POST", "https://api.simkl.com/oauth/token", bytes.NewBuffer(jsonData))

15
app.go
View File

@@ -3,9 +3,11 @@ package main
import ( import (
"context" "context"
_ "embed" _ "embed"
"log"
"strings"
"github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options"
"github.com/wailsapp/wails/v2/pkg/runtime" "github.com/wailsapp/wails/v2/pkg/runtime"
"strings"
"github.com/tidwall/gjson" "github.com/tidwall/gjson"
) )
@@ -43,3 +45,14 @@ func (a *App) onSecondInstanceLaunch(secondInstanceData options.SecondInstanceDa
runtime.Show(*wailsContext) runtime.Show(*wailsContext)
go runtime.EventsEmit(*wailsContext, "launchArgs", secondInstanceArgs) go runtime.EventsEmit(*wailsContext, "launchArgs", secondInstanceArgs)
} }
func (a *App) ShowVersion() {
version := gjson.Get(wailsJSON, "info.productVersion")
_, err := runtime.MessageDialog(*wailsContext, runtime.MessageDialogOptions{
Title: "Version",
Message: "AniTrack Version: " + version.String(),
})
if err != nil {
log.Println(err)
}
}

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

BIN
build/icon/128/AniTrack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
build/icon/32/AniTrack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
build/icon/48/AniTrack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
build/icon/64/AniTrack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

26
build/install_linux.sh Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/bash
# copy desktop file
if [ -e "~/.local/share/applications/AniTrack.desktop" ]; then
if [ -d "~/.local/share/applications/" ]; then
cp ./AniTrack.desktop ~/.local/share/applications/
else
mkdir -p ~/.local/share/applications/
cp ./AniTrack.desktop ~/.local/share/applications/
fi
fi
# copy icons to xdg folders
for size in 32 48 64 128; do
xdg-icon-resource install --novendor --context apps --size $size ./icon/$size/AniTrack.png AniTrack
done
# copy AniTrack Binary to $HOME/Applications/
if ! [ -d "~/Applications" ]; then
mkdir -p ~/Applications
cp ./bin/AniTrack ~/Applications/
elif ! [[ -e ~/Applications/AniTrack ]]; then
cp ./bin/AniTrack ~/Applications/
fi
echo "AniTrack has been successfully installed."

View File

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

View File

@@ -1,44 +1,72 @@
<script lang="ts"> <script lang="ts">
import { import {
aniListAnime, aniListLoggedIn,
GetAnimeSingleItem, malLoggedIn,
simklLoggedIn,
watchlistNeedsRefresh,
aniListPrimary,
malPrimary,
simklPrimary,
malWatchList,
simklWatchList,
} from "./helperModules/GlobalVariablesAndHelperFunctions.svelte"; } from "./helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { onMount } from "svelte"; import { onMount } from "svelte";
import Router from "svelte-spa-router" import Router from "svelte-spa-router";
import Home from "./routes/Home.svelte"; import Home from "./routes/Home.svelte";
import { wrap } from "svelte-spa-router/wrap"; import { wrap } from "svelte-spa-router/wrap";
import Spinner from "./helperComponents/Spinner.svelte"; import Spinner from "./helperComponents/Spinner.svelte";
import Header from "./helperComponents/Header.svelte"; import Header from "./helperComponents/Header.svelte";
import { CheckIfAniListLoggedInAndLoadWatchList } from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte"; import { CheckIfAniListLoggedInAndLoadWatchList } from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte"; import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte";
import {CheckIfSimklLoggedInAndSetUser} from "./helperModules/CheckIsSimklLoggedIn.svelte" import { CheckIfSimklLoggedInAndSetUser } from "./helperModules/CheckIsSimklLoggedIn.svelte";
import {CheckIfAniListLoggedIn} from "../wailsjs/go/main/App"; import {
import {AniListGetSingleAnimeDefaultData} from "./helperDefaults/AniListGetSingleAnime"; CheckIfAniListLoggedIn,
GetMyAnimeList,
SimklGetUserWatchlist,
} from "../wailsjs/go/main/App";
import { loc } from "svelte-spa-router";
import ErrorModal from "./helperComponents/ErrorModal.svelte";
onMount(async () => { onMount(async () => {
await CheckIfAniListLoggedInAndLoadWatchList() let isAniListLoggedIn: boolean;
await CheckIfMALLoggedInAndSetUser() let isMALLoggedIn: boolean;
await CheckIfSimklLoggedInAndSetUser() let isSimklLoggedIn: boolean;
}) aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
!isAniListLoggedIn && (await CheckIfAniListLoggedInAndLoadWatchList());
!isMALLoggedIn && (await CheckIfMALLoggedInAndSetUser());
!isSimklLoggedIn && (await CheckIfSimklLoggedInAndSetUser());
});
$: if ($loc?.location === "/" && $watchlistNeedsRefresh) {
(async () => {
if ($aniListLoggedIn && $aniListPrimary) {
await CheckIfAniListLoggedInAndLoadWatchList();
}
if ($malLoggedIn && $malPrimary) {
await GetMyAnimeList(1000).then((w) => malWatchList.set(w));
}
if ($simklLoggedIn && $simklPrimary) {
await SimklGetUserWatchlist().then((w) => simklWatchList.set(w));
}
watchlistNeedsRefresh.set(false);
})();
}
</script> </script>
<Header /> <Header />
<Router routes={{ <ErrorModal />
'/': Home, <Router
'/anime/:id': wrap({ routes={{
asyncComponent: () => import('./routes/AnimeRoutePage.svelte'), "/": Home,
conditions: [ "/anime/:id": wrap({
async () => await CheckIfAniListLoggedIn(), asyncComponent: () => import("./routes/AnimeRoutePage.svelte"),
async (detail) => { conditions: [async () => await CheckIfAniListLoggedIn()],
aniListAnime.update(value => { loadingComponent: Spinner,
value = AniListGetSingleAnimeDefaultData
return value
})
await GetAnimeSingleItem(Number(detail.params.id), true)
return Object.keys($aniListAnime).length!==0
},
],
loadingComponent: Spinner
}), }),
// '*': "Not Found" // '*': "Not Found"
}} /> }}
/>

View File

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

View File

@@ -6,8 +6,10 @@
malLoggedIn, malLoggedIn,
simklAnime, simklAnime,
simklLoggedIn, simklLoggedIn,
watchlistNeedsRefresh,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte"; } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { push } from "svelte-spa-router"; import { push } from "svelte-spa-router";
import WebsiteLink from "./WebsiteLink.svelte";
import type { AniListGetSingleAnime } from "../anilist/types/AniListCurrentUserWatchListType"; import type { AniListGetSingleAnime } from "../anilist/types/AniListCurrentUserWatchListType";
import Rating from "./Rating.svelte"; import Rating from "./Rating.svelte";
import { import {
@@ -38,6 +40,7 @@
import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte"; import { AddAnimeServiceToTable } from "../helperModules/AddAnimeServiceToTable.svelte";
import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte"; import { CheckIfAniListLoggedInAndLoadWatchList } from "../helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte";
import Datepicker from "./Datepicker.svelte"; import Datepicker from "./Datepicker.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 isAniListLoggedIn: boolean;
@@ -188,7 +191,11 @@
submitData[key] = value; submitData[key] = value;
} }
if (isAniListLoggedIn && currentAniListAnime.data.MediaList.mediaId !== 0) { try {
if (
isAniListLoggedIn &&
currentAniListAnime.data.MediaList.mediaId !== 0
) {
let body: AniListUpdateVariables = { let body: AniListUpdateVariables = {
mediaId: currentAniListAnime.data.MediaList.mediaId, mediaId: currentAniListAnime.data.MediaList.mediaId,
progress: submitData.episodes, progress: submitData.episodes,
@@ -200,9 +207,10 @@
completedAt: convertDateToAniList(completedAtDate), completedAt: convertDateToAniList(completedAtDate),
}; };
await AniListUpdateEntry(body).then((value: AniListGetSingleAnime) => { await AniListUpdateEntry(body).then((value: AniListGetSingleAnime) => {
/* TODO in future when you inevitably add tags to typescript, until Anilist fixes the api bug value.data.MediaList.media.tags =
where tags break the SaveMediaListEntry return, you'll want to use this delete line currentAniListAnime.data.MediaList.media.tags;
delete value.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;
@@ -327,8 +335,10 @@
} }
if (currentSimklAnime.status !== submitData.status.simkl) { if (currentSimklAnime.status !== submitData.status.simkl) {
await SimklSyncStatus(currentSimklAnime, submitData.status.simkl).then( await SimklSyncStatus(
(value) => { currentSimklAnime,
submitData.status.simkl,
).then((value) => {
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `s-${value.show.ids.simkl}`, id: `s-${value.show.ids.simkl}`,
title: value.show.title, title: value.show.title,
@@ -345,19 +355,26 @@
newValue = value; newValue = value;
return newValue; return newValue;
}); });
}, });
);
} }
} }
} 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);
if (isAniListLoggedIn && currentAniListAnime.data.MediaList.mediaId !== 0) { try {
if (
isAniListLoggedIn &&
currentAniListAnime.data.MediaList.mediaId !== 0
) {
await AniListDeleteEntry(currentAniListAnime.data.MediaList.id); await AniListDeleteEntry(currentAniListAnime.data.MediaList.id);
AddAnimeServiceToTable({ AddAnimeServiceToTable({
id: `a-${currentAniListAnime.data.MediaList.mediaId}`, id: `a-${currentAniListAnime.data.MediaList.mediaId}`,
@@ -402,9 +419,14 @@
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;
@@ -422,7 +444,6 @@
</script> </script>
<form on:submit|preventDefault={handleSubmit} class="container pt-3 pb-10"> <form on:submit|preventDefault={handleSubmit} class="container pt-3 pb-10">
{console.log(startingAnilistStatusOption)}
<h1 class="text-white font-bold text-left text-xl pb-3"> <h1 class="text-white font-bold text-left text-xl pb-3">
{title} {title}
</h1> </h1>
@@ -462,7 +483,10 @@
completedAtDate = null; completedAtDate = null;
} }
}} }}
class="bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none" disabled={currentAniListAnime.data.MediaList.progress <= 0}
class={currentAniListAnime.data.MediaList.progress <= 0
? "border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
: "bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-s-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"}
> >
<svg <svg
class="w-3 h-3 text-white" class="w-3 h-3 text-white"
@@ -515,8 +539,32 @@
startingAnilistStatusOption = statusOptions[2]; startingAnilistStatusOption = statusOptions[2];
completedAtDate = new Date(); completedAtDate = new Date();
} }
if (currentAniListAnime.data.MediaList.progress - 1 === 0) {
startingAnilistStatusOption = statusOptions[0];
if (startedAtDate === null) startedAtDate = new Date();
}
}} }}
class="bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none" disabled={(currentAniListAnime.data.MediaList.media.episodes >
0 &&
currentAniListAnime.data.MediaList.progress >=
currentAniListAnime.data.MediaList.media.episodes) ||
(currentAniListAnime.data.MediaList.media.nextAiringEpisode
.episode > 0 &&
currentAniListAnime.data.MediaList.progress >
currentAniListAnime.data.MediaList.media.nextAiringEpisode
.episode -
2)}
class={(currentAniListAnime.data.MediaList.media.episodes > 0 &&
currentAniListAnime.data.MediaList.progress >=
currentAniListAnime.data.MediaList.media.episodes) ||
(currentAniListAnime.data.MediaList.media.nextAiringEpisode
.episode > 0 &&
currentAniListAnime.data.MediaList.progress >
currentAniListAnime.data.MediaList.media.nextAiringEpisode
.episode -
2)
? "border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
: "bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"}
> >
<svg <svg
class="w-3 h-3 text-white" class="w-3 h-3 text-white"
@@ -680,6 +728,7 @@
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"
@@ -776,8 +825,46 @@
</div> </div>
</div> </div>
<div class="flex m-5">
<div> <div>
<h3 class="text-2xl">Genres</h3>
{#each currentAniListAnime.data.MediaList.media.genres || [] as genre}
<div>
<Badge large border color="blue" class="m-1 w-52">
<div>
<WebsiteLink
id={genre}
url="https://anilist.co/search/anime/{genre}"
/>
</div>
</Badge>
<Tooltip>{genre}</Tooltip>
</div>
{/each}
<h3 class="text-2xl">Tags</h3>
<div class="mt-2">
{#each currentAniListAnime.data.MediaList.media.tags as tag}
<div>
<Badge large border color="blue" class="m-1 w-52">
<div>
<WebsiteLink
id={tag.name}
url="https://anilist.co/search/anime/?genres={tag.name}"
/>
<span class="text-xs">({tag.rank}%)</span>
</div>
</Badge>
<Tooltip>{tag.description}</Tooltip>
</div>
{/each}
</div>
</div>
<div class="ml-5">
<h3 class="text-2xl">Summary</h3> <h3 class="text-2xl">Summary</h3>
<p>{@html currentAniListAnime.data.MediaList.media.description}</p> <p class="rounded border border-gray-700 p-2 mt-2">
{@html currentAniListAnime.data.MediaList.media.description}
</p>
</div>
</div> </div>
</form> </form>

View File

@@ -18,6 +18,7 @@
import * as runtime from "../../wailsjs/runtime"; import * as runtime from "../../wailsjs/runtime";
import type { MyAnimeListUser } from "../mal/types/MALTypes"; import type { MyAnimeListUser } from "../mal/types/MALTypes";
import type { SimklUser } from "../simkl/types/simklTypes"; import type { SimklUser } from "../simkl/types/simklTypes";
import { ShowVersion } from "../../wailsjs/go/main/App";
let currentAniListUser: AniListUser; let currentAniListUser: AniListUser;
let currentMALUser: MyAnimeListUser; let currentMALUser: MyAnimeListUser;
@@ -27,8 +28,8 @@
let isMALLoggedIn: boolean; let isMALLoggedIn: boolean;
aniListUser.subscribe((value) => (currentAniListUser = value)); aniListUser.subscribe((value) => (currentAniListUser = value));
malUser.subscribe((value) => (currentMALUser = value)) malUser.subscribe((value) => (currentMALUser = value));
simklUser.subscribe(value => currentSimklUser = value) simklUser.subscribe((value) => (currentSimklUser = value));
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value)); aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value)); simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
malLoggedIn.subscribe((value) => (isMALLoggedIn = value)); malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
@@ -36,6 +37,23 @@
function dropdownUser(): void { function dropdownUser(): void {
let dropdown = document.querySelector("#userDropdown"); let dropdown = document.querySelector("#userDropdown");
dropdown.classList.toggle("hidden"); dropdown.classList.toggle("hidden");
if (!dropdown.classList.contains("hidden")) {
document.addEventListener("click", clickOutside);
}
}
function clickOutside(event: Event): void {
let dropdown = document.querySelector("#userDropdown");
let toggleBtn = document.querySelector("#userDropdownButton");
if (
!dropdown.contains(event.target as Node) &&
!toggleBtn.contains(event.target as Node)
) {
dropdown.classList.add("hidden");
document.removeEventListener("click", clickOutside);
}
} }
</script> </script>
@@ -45,7 +63,9 @@
<Avatar <Avatar
src={currentAniListUser.data.Viewer.avatar.medium} src={currentAniListUser.data.Viewer.avatar.medium}
class="cursor-pointer" class="cursor-pointer"
dot={{ color: "green" }} dot={isAniListLoggedIn && isMALLoggedIn && isSimklLoggedIn
? { color: "green" }
: { color: "yellow" }}
/> />
{:else} {:else}
<Avatar class="cursor-pointer" dot={{ color: "red" }} /> <Avatar class="cursor-pointer" dot={{ color: "red" }} />
@@ -72,13 +92,19 @@
on:click={logoutOfAniList} on:click={logoutOfAniList}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-green-800 hover:text-white" class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-green-800 hover:text-white"
> >
<span class="maple-font text-lg text-green-200 mr-4">A</span>Logout {currentAniListUser.data.Viewer.name} <span class="maple-font text-lg text-green-200 mr-4">A</span>Logout {currentAniListUser
.data.Viewer.name}
</button> </button>
</li> </li>
{:else} {:else}
<li> <li>
<button on:click={loginToAniList} <button
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white"> on:click={() => {
dropdownUser();
loginToAniList();
}}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white"
>
<span class="maple-font text-lg mr-4">A</span>Login to AniList <span class="maple-font text-lg mr-4">A</span>Login to AniList
</button> </button>
</li> </li>
@@ -94,8 +120,13 @@
</li> </li>
{:else} {:else}
<li> <li>
<button on:click={loginToMAL} <button
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white"> on:click={() => {
dropdownUser();
loginToMAL();
}}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white"
>
<span class="maple-font text-lg mr-4">M</span>Login to MyAnimeList <span class="maple-font text-lg mr-4">M</span>Login to MyAnimeList
</button> </button>
</li> </li>
@@ -106,19 +137,34 @@
on:click={logoutOfSimkl} on:click={logoutOfSimkl}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-indigo-800 hover:text-white" class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-indigo-800 hover:text-white"
> >
<span class="maple-font text-lg text-indigo-200 mr-4">S</span>Logout {currentSimklUser.user.name} <span class="maple-font text-lg text-indigo-200 mr-4">S</span>Logout {currentSimklUser
.user.name}
</button> </button>
</li> </li>
{:else} {:else}
<li> <li>
<button on:click={loginToSimkl} <button
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white"> on:click={() => {
dropdownUser();
loginToSimkl();
}}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate hover:text-white"
>
<span class="maple-font text-lg mr-4">S</span>Login to Simkl <span class="maple-font text-lg mr-4">S</span>Login to Simkl
</button> </button>
</li> </li>
{/if} {/if}
</ul> </ul>
<div class="py-2"> <div class="py-2">
<button
on:click={() => {
dropdownUser();
ShowVersion();
}}
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
>
Version
</button>
<button <button
on:click={() => runtime.Quit()} on:click={() => runtime.Quit()}
class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white" class="block px-4 py-2 w-full text-sm hover:bg-gray-600 text-gray-200 over:text-white"
@@ -128,3 +174,4 @@
</div> </div>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,23 @@
<script lang="ts"> <script lang="ts">
import { aniListAnime, GetAnimeSingleItem } from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import Anime from "../helperComponents/Anime.svelte" import Anime from "../helperComponents/Anime.svelte"
import { AniListGetSingleAnimeDefaultData } from "../helperDefaults/AniListGetSingleAnime";
import Spinner from "../helperComponents/Spinner.svelte";
export let params: Record<string, string> export let params: Record<string, string>
let loadPromise = load(params.id)
$: loadPromise = load(params.id)
async function load(id: string) {
aniListAnime.update(() => AniListGetSingleAnimeDefaultData)
await GetAnimeSingleItem(Number(id), true)
}
</script> </script>
{#key params.id} {#key params.id}
{#await loadPromise}
<Spinner />
{:then _}
<Anime /> <Anime />
{/await}
{/key} {/key}

View File

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

View File

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

View File

@@ -42,6 +42,8 @@ export function MyAnimeListLogin():Promise<void>;
export function MyAnimeListUpdate(arg1:main.MALAnime,arg2:main.MALUploadStatus):Promise<main.MalListStatus>; export function MyAnimeListUpdate(arg1:main.MALAnime,arg2:main.MALUploadStatus):Promise<main.MalListStatus>;
export function ShowVersion():Promise<void>;
export function SimklGetUserWatchlist():Promise<main.SimklWatchListType>; export function SimklGetUserWatchlist():Promise<main.SimklWatchListType>;
export function SimklLogin():Promise<void>; export function SimklLogin():Promise<void>;

View File

@@ -82,6 +82,10 @@ export function MyAnimeListUpdate(arg1, arg2) {
return window['go']['main']['App']['MyAnimeListUpdate'](arg1, arg2); return window['go']['main']['App']['MyAnimeListUpdate'](arg1, arg2);
} }
export function ShowVersion() {
return window['go']['main']['App']['ShowVersion']();
}
export function SimklGetUserWatchlist() { export function SimklGetUserWatchlist() {
return window['go']['main']['App']['SimklGetUserWatchlist'](); return window['go']['main']['App']['SimklGetUserWatchlist']();
} }

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

10
go.mod
View File

@@ -39,11 +39,11 @@ require (
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wailsapp/go-webview2 v1.0.19 // indirect github.com/wailsapp/go-webview2 v1.0.19 // indirect
github.com/wailsapp/mimetype v1.4.1 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect
golang.org/x/crypto v0.35.0 // indirect golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.35.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/sys v0.30.0 // indirect golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.29.0 // indirect golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.31.0 // indirect
) )
// replace github.com/wailsapp/wails/v2 v2.9.1 => /home/nymusicman/go/pkg/mod // replace github.com/wailsapp/wails/v2 v2.9.1 => /home/nymusicman/go/pkg/mod

20
go.sum
View File

@@ -85,24 +85,24 @@ github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhw
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns= github.com/wailsapp/wails/v2 v2.10.1 h1:QWHvWMXII2nI/nXz77gpPG8P3ehl6zKe+u4su5BWIns=
github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY= github.com/wailsapp/wails/v2 v2.10.1/go.mod h1:zrebnFV6MQf9kx8HI4iAv63vsR5v67oS7GTEZ7Pz1TY=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -1,79 +0,0 @@
# @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

@@ -1,83 +0,0 @@
# @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

@@ -1,70 +0,0 @@
# @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

@@ -1,44 +0,0 @@
# @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

@@ -1,93 +0,0 @@
# @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

@@ -1,3 +0,0 @@
# @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

@@ -1,11 +0,0 @@
# @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

@@ -1,76 +0,0 @@
# @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

@@ -1,19 +0,0 @@
# @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

@@ -1,65 +0,0 @@
# @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

@@ -1,17 +0,0 @@
# @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

@@ -1,6 +0,0 @@
# @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

@@ -1,12 +0,0 @@
# @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

@@ -1,5 +0,0 @@
# @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

@@ -1,5 +0,0 @@
# @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

@@ -1,8 +0,0 @@
# @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

View File

@@ -1,3 +0,0 @@
# @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

@@ -1,5 +0,0 @@
# @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

@@ -1,7 +0,0 @@
# @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

@@ -1,5 +0,0 @@
# @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

@@ -1,17 +0,0 @@
# @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

@@ -1,40 +0,0 @@
# @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

@@ -1,12 +0,0 @@
# @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}}
}

View File

@@ -1,19 +0,0 @@
{
"$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.1.8" "productVersion": "0.7.0"
} }
} }