7 Commits

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

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

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

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

3
.gitignore vendored
View File

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

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

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +1,9 @@
<script lang="ts">
import { Avatar } from "flowbite-svelte";
import type { AniListUser } from "../anilist/types/AniListTypes";
import {
aniListLoggedIn,
aniListUser,
malUser,
simklUser,
malLoggedIn,
simklLoggedIn,
loginToAniList,
loginToMAL,
loginToSimkl,
logoutOfAniList,
logoutOfMAL,
logoutOfSimkl
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import {userStore} from "../helperFunctions/userStore"
import * as runtime from "../../wailsjs/runtime";
import type {MyAnimeListUser} from "../mal/types/MALTypes";
import type {SimklUser} from "../simkl/types/simklTypes";
import { ShowVersion } from "../../wailsjs/go/main/App";
let currentAniListUser: AniListUser;
let currentMALUser: MyAnimeListUser;
let currentSimklUser: SimklUser;
let isAniListLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isMALLoggedIn: boolean;
aniListUser.subscribe((value) => (currentAniListUser = value));
malUser.subscribe((value) => (currentMALUser = value))
simklUser.subscribe(value => currentSimklUser = value)
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
function dropdownUser(): void {
let dropdown = document.querySelector("#userDropdown");
dropdown.classList.toggle("hidden");
@@ -56,9 +26,9 @@
<div class="relative">
<button id="userDropdownButton" on:click={dropdownUser}>
{#if isAniListLoggedIn}
{#if $userStore.anilist.isLoggedIn}
<Avatar
src={currentAniListUser.data.Viewer.avatar.medium}
src={$userStore.anilist.user.data.Viewer.avatar.medium}
class="cursor-pointer"
dot={{ color: "green" }}
/>
@@ -71,8 +41,8 @@
class="absolute hidden right-0 2xl:left-1/2 2xl:-translate-x-1/2 z-10 divide-y rounded-lg shadow w-44 bg-gray-700 divide-gray-600"
>
<div class="px-4 py-3 text-sm text-white">
{#if isAniListLoggedIn}
<div>{currentAniListUser.data.Viewer.name}</div>
{#if $userStore.anilist.isLoggedIn}
<div>{$userStore.anilist.user.data.Viewer.name}</div>
{:else}
<div>You are not logged into AniList</div>
{/if}
@@ -81,60 +51,60 @@
class="py-2 text-sm text-gray-200"
aria-labelledby="dropdownUserAvatarButton"
>
{#if isAniListLoggedIn}
{#if $userStore.anilist.isLoggedIn}
<li>
<button
on:click={logoutOfAniList}
on:click={() => userStore.logout("anilist")}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-green-800 hover:text-white"
>
<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 {$userStore.anilist.user.data.Viewer.name}
</button>
</li>
{:else}
<li>
<button on:click={() => {
dropdownUser()
loginToAniList()
userStore.checkProvider("anilist")
}}
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
</button>
</li>
{/if}
{#if isMALLoggedIn}
{#if $userStore.mal.isLoggedIn}
<li>
<button
on:click={logoutOfMAL}
on:click={() => userStore.logout("mal")}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-blue-800 hover:text-white"
>
<span class="maple-font text-lg text-blue-200 mr-4">M</span>Logout {currentMALUser.name}
<span class="maple-font text-lg text-blue-200 mr-4">M</span>Logout {$userStore.mal.user.name}
</button>
</li>
{:else}
<li>
<button on:click={() => {
dropdownUser()
loginToMAL()
userStore.checkProvider("mal")
}}
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
</button>
</li>
{/if}
{#if isSimklLoggedIn}
{#if $userStore.simkl.isLoggedIn}
<li>
<button
on:click={logoutOfSimkl}
on:click={() => userStore.logout("simkl")}
class="block px-4 py-2 w-full hover:bg-gray-600 truncate bg-indigo-800 hover:text-white"
>
<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 {$userStore.simkl.user.user.name}
</button>
</li>
{:else}
<li>
<button on:click={() => {
dropdownUser()
loginToSimkl()
userStore.checkProvider("simkl")
}}
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

View File

@@ -1,102 +1,63 @@
<script lang="ts">
import Search from "./Search.svelte";
import {
aniListLoggedIn,
loginToAniList,
loginToMAL,
loginToSimkl,
malLoggedIn,
simklLoggedIn,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import Search from "./Search.svelte"
import AvatarMenu from "./AvatarMenu.svelte";
import logo from "../assets/images/AniTrackLogo.svg";
import { link } from "svelte-spa-router";
import logo from "../assets/images/AniTrackLogo.svg"
import {userStore} from "../helperFunctions/userStore"
let isAniListLoggedIn: boolean;
let isSimklLoggedIn: boolean;
let isMALLoggedIn: boolean;
aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
</script>
<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">
<a href="/" use:link
><img src={logo} class="h-8" alt="AniTrack Logo" /></a
>
<a href="/"><img src={logo} class="h-8" alt="AniTrack Logo"/></a>
</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">
<Search />
</div>
<AvatarMenu />
<button
on:click={() => {
let menu = document.querySelector("#navbar-user");
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"
aria-controls="navbar-user"
aria-expanded="false"
>
<AvatarMenu/>
<button on:click={() => {
let menu = document.querySelector("#navbar-user")
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"
aria-controls="navbar-user" aria-expanded="false">
<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"
viewBox="0 0 17 14"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M1 1h15M1 7h15M1 13h15"
/>
<svg class="w-5 h-5" aria-hidden="true" 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>
</button>
</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"
>
<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"
>
<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">
<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>
{#if !isAniListLoggedIn}
<button on:click={loginToAniList}>
<!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">-->
{#if !$userStore.anilist.isLoggedIn}
<button on:click={() => userStore.checkProvider("anilist")}>
<!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">-->
AniList Login
</button>
{/if}
{#if !isMALLoggedIn}
<button on:click={loginToMAL}>
<!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded min-[950px]:p-0 text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">-->
{#if !$userStore.mal.isLoggedIn}
<button on:click={() => userStore.checkProvider("mal")}>
<!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded min-[950px]:p-0 text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">-->
MyAnimeList Login
</button>
{/if}
</li>
<li>
{#if !isSimklLoggedIn}
<button on:click={loginToSimkl}>
<!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded min-[950px]:p-0 text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">-->
{#if !$userStore.simkl.isLoggedIn}
<button on:click={() => userStore.checkProvider("simkl")}>
<!-- class="block py-2 px-3 w-full min-[950px]:w-auto rounded min-[950px]:p-0 text-gray-300 min-[950px]:hover:text-blue-500 hover:bg-gray-700 hover:text-white min-[950px]:hover:bg-transparent border-gray-700">-->
Simkl Login
</button>
{/if}
</li>
</ul>
<div class="flex justify-center min-[950px]:hidden">
<Search />
<Search/>
</div>
</div>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,34 +0,0 @@
<script lang="ts" context="module">
import {CheckIfAniListLoggedIn, GetAniListLoggedInUser, GetAniListUserWatchingList} from "../../wailsjs/go/main/App";
import {MediaListSort} from "../anilist/types/AniListTypes";
import { aniListUser, watchListPage, animePerPage, aniListPrimary, aniListLoggedIn, aniListWatchlist } from "./GlobalVariablesAndHelperFunctions.svelte"
let isAniListPrimary: boolean
let page: number
let perPage: number
aniListPrimary.subscribe(value => isAniListPrimary = value)
watchListPage.subscribe(value => page = value)
animePerPage.subscribe(value => perPage = value)
export const LoadAniListUser = async () => {
await GetAniListLoggedInUser().then(user => {
aniListUser.set(user)
})
}
export const LoadAniListWatchList = async () => {
await GetAniListUserWatchingList(page, perPage, MediaListSort.UpdatedTimeDesc).then((watchList) => {
aniListWatchlist.set(watchList)
})
}
export const CheckIfAniListLoggedInAndLoadWatchList = async () => {
const loggedIn = await CheckIfAniListLoggedIn()
if (loggedIn) {
await LoadAniListUser()
if (isAniListPrimary) await LoadAniListWatchList()
}
aniListLoggedIn.set(loggedIn)
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,19 +2,13 @@
import Pagination from "../helperComponents/Pagination.svelte";
import WatchList from "../helperComponents/WatchList.svelte";
import {
aniListLoggedIn,
aniListPrimary,
loading,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import loader from '../helperFunctions/loader'
import {userStore} from "../helperFunctions/userStore";
let isAniListPrimary: boolean
let isAniListLoggedIn: boolean
aniListPrimary.subscribe((value) => isAniListPrimary = value)
aniListLoggedIn.subscribe((value) => isAniListLoggedIn = value)
</script>
{#if isAniListLoggedIn && isAniListPrimary}
{#if $userStore.anilist.isLoggedIn && $userStore.anilist.isPrimary}
<div class="container py-10">
<Pagination />
<WatchList />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,5 @@
# @name Get Single Anime
GET https://api.myanimelist.net/v2/anime/57380?fields=id,title,main_picture,alternative_titles,start_date,end_date,synopsis,mean,rank,popularity,num_list_users,num_scoring_users,nsfw,genres,created_at,updated_at,media_type,status,my_list_status,num_episodes,start_season,broadcast,source,average_episode_duration,rating,pictures,background,related_anime,recommendations,studios,statistics
Accept: application/json
Authorization: Bearer {{MAL_ACCESS_TOKEN}}

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -12,6 +12,6 @@
},
"info": {
"productName": "AniTrack",
"productVersion": "0.6.5"
"productVersion": "0.6.0"
}
}