12 Commits
0.5.3 ... 0.6.5

Author SHA1 Message Date
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
7 changed files with 567 additions and 382 deletions

View File

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

View File

@@ -1,42 +1,70 @@
<script lang="ts"> <script lang="ts">
import { import {
aniListLoggedIn, aniListLoggedIn,
malLoggedIn, malLoggedIn,
simklLoggedIn, simklLoggedIn,
} from "./helperModules/GlobalVariablesAndHelperFunctions.svelte"; watchlistNeedsRefresh,
import {onMount} from "svelte"; aniListPrimary,
import Router from "svelte-spa-router" malPrimary,
import Home from "./routes/Home.svelte"; simklPrimary,
import {wrap} from "svelte-spa-router/wrap"; malWatchList,
import Spinner from "./helperComponents/Spinner.svelte"; simklWatchList,
import Header from "./helperComponents/Header.svelte"; } from "./helperModules/GlobalVariablesAndHelperFunctions.svelte";
import {CheckIfAniListLoggedInAndLoadWatchList} from "./helperModules/CheckIfAniListLoggedInAndLoadWatchList.svelte"; import { onMount } from "svelte";
import { CheckIfMALLoggedInAndSetUser } from "./helperModules/CheckIfMyAnimeListLoggedIn.svelte"; import Router from "svelte-spa-router";
import {CheckIfSimklLoggedInAndSetUser} from "./helperModules/CheckIsSimklLoggedIn.svelte" import Home from "./routes/Home.svelte";
import {CheckIfAniListLoggedIn} from "../wailsjs/go/main/App"; 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";
onMount(async () => { onMount(async () => {
let isAniListLoggedIn: boolean let isAniListLoggedIn: boolean;
let isMALLoggedIn: boolean let isMALLoggedIn: boolean;
let isSimklLoggedIn: boolean let isSimklLoggedIn: boolean;
aniListLoggedIn.subscribe((value) => isAniListLoggedIn = value) aniListLoggedIn.subscribe((value) => (isAniListLoggedIn = value));
malLoggedIn.subscribe((value) => isMALLoggedIn = value) malLoggedIn.subscribe((value) => (isMALLoggedIn = value));
simklLoggedIn.subscribe((value) => isSimklLoggedIn = value) simklLoggedIn.subscribe((value) => (isSimklLoggedIn = value));
console.log(isAniListLoggedIn) !isAniListLoggedIn && (await CheckIfAniListLoggedInAndLoadWatchList());
!isAniListLoggedIn && await CheckIfAniListLoggedInAndLoadWatchList() !isMALLoggedIn && (await CheckIfMALLoggedInAndSetUser());
!isMALLoggedIn && await CheckIfMALLoggedInAndSetUser() !isSimklLoggedIn && (await CheckIfSimklLoggedInAndSetUser());
!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={{ <Router
'/': Home, routes={{
'/anime/:id': wrap({ "/": Home,
asyncComponent: () => import('./routes/AnimeRoutePage.svelte'), "/anime/:id": wrap({
conditions: [async () => await CheckIfAniListLoggedIn()], asyncComponent: () => import("./routes/AnimeRoutePage.svelte"),
loadingComponent: Spinner conditions: [async () => await CheckIfAniListLoggedIn()],
loadingComponent: Spinner,
}), }),
// '*': "Not Found" // '*': "Not Found"
}} /> }}
/>

View File

@@ -6,6 +6,7 @@
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 WebsiteLink from "./WebsiteLink.svelte";
@@ -362,6 +363,7 @@
} finally { } 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);
} }
}; };
@@ -422,6 +424,7 @@
} finally { } 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);
} }
}; };
@@ -480,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"
@@ -538,7 +544,27 @@
if (startedAtDate === null) startedAtDate = new Date(); 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"
@@ -702,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"

View File

@@ -1,145 +1,224 @@
<script lang="ts"> <script lang="ts">
import { import {
aniListLoggedIn, aniListLoggedIn,
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"; import { MediaListSort } from "../anilist/types/AniListTypes";
let aniListWatchListLoaded: AniListCurrentUserWatchList let aniListWatchListLoaded: AniListCurrentUserWatchList;
let page: number let page: number;
let perPage: number let perPage: number;
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));
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(
watchListPage.set(newPage) newPage,
aniListWatchlist.set(result) perPage,
aniListLoggedIn.set(true) MediaListSort.UpdatedTimeDesc,
}) ).then((result) => {
} watchListPage.set(newPage);
aniListWatchlist.set(result);
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 (
function changeCountPerPage(e): void { (e.key === "Enter" || e.key === "Tab") &&
GetAniListUserWatchingList(1, Number(e.target.value), MediaListSort.UpdatedTimeDesc).then((result) => { Number(e.currentTarget.value) !== page
animePerPage.set(Number(e.target.value)) )
watchListPage.set(1) ChangeWatchListPage(Number(e.currentTarget.value));
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> </script>
<div class="mb-8"> <div class="mb-8">
{#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12} {#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12}
<nav aria-label="Page navigation" class="hidden md:block"> <nav aria-label="Page navigation" class="hidden md:block">
<ul class="inline-flex -space-x-px text-base h-10"> <ul class="inline-flex -space-x-px text-base h-10">
{#if page === 1} {#if page === 1}
<li> <li>
<button 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
Previous 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"
</button> >
</li> Previous
{:else} </button>
<li> </li>
<button on:click={() => ChangeWatchListPage(page-1)} {:else}
class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white"> <li>
Previous <button
</button> on:click={() => ChangeWatchListPage(page - 1)}
</li> class="flex items-center justify-center px-4 h-10 ms-0 leading-tight border border-e-0 rounded-s-lg border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white"
{/if} >
{#each {length: aniListWatchListLoaded.data.Page.pageInfo.lastPage} as _, i} Previous
{#if i + 1 === page} </button>
<li> </li>
<button on:click={() => ChangeWatchListPage(i+1)} {/if}
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> {#each { length: aniListWatchListLoaded.data.Page.pageInfo.lastPage } as _, i}
</li> {#if i + 1 === page}
{:else} <li>
<li> <button
<button on:click={() => ChangeWatchListPage(i+1)} on:click={() => ChangeWatchListPage(i + 1)}
class="flex items-center justify-center px-4 h-10 leading-tight border dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white">{i + 1}</button> class="flex items-center justify-center px-4 h-10 leading-tight border hover:bg-gray-100 border-gray-700 bg-gray-700 text-white"
</li> >{i + 1}</button
{/if} >
{/each} </li>
{#if page === aniListWatchListLoaded.data.Page.pageInfo.lastPage} {:else}
<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"> on:click={() => ChangeWatchListPage(i + 1)}
Next 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"
</button> >{i + 1}</button
</li> >
{:else} </li>
<li> {/if}
<button on:click={() => ChangeWatchListPage(page+1)} {/each}
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"> {#if page === aniListWatchListLoaded.data.Page.pageInfo.lastPage}
Next <li>
</button> <button
</li> disabled
{/if} 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"
</ul> >
</nav> Next
{/if} </button>
<div class="flex mt-5"> </li>
<div class="w-20 mx-auto"> {:else}
<select bind:value={perPage} on:change={(e) => changeCountPerPage(e)} id="countPerPage" <li>
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"> <button
{#each perPageOptions as option} on:click={() => ChangeWatchListPage(page + 1)}
<option value={option}> class="flex items-center justify-center px-4 h-10 leading-tight border rounded-e-lg dark border-gray-700 text-gray-400 hover:bg-gray-700 hover:text-white"
{option} >
</option> Next
{/each} </button>
</select> </li>
</div> {/if}
</ul>
<div> </nav>
<div>Total Anime: {aniListWatchListLoaded.data.Page.pageInfo.total}</div> {/if}
{#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12} <div class="flex mt-5">
<div class="md:hidden">Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}</div> <div class="w-20 mx-auto">
{:else} <select
<div>Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}</div> bind:value={perPage}
{/if} on:change={(e) => changeCountPerPage(e)}
</div> 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"
<div class="max-w-xs mx-auto"> >
<div class="relative flex items-center max-w-[11rem]"> {#each perPageOptions as option}
<button type="button" id="decrement-button" on:click={() => ChangeWatchListPage(page-1)} <option value={option}>
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"> {option}
<svg class="w-3 h-3 text-white" aria-hidden="true" </option>
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 2"> {/each}
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" </select>
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>
</div> </div>
<div>
<div>Total Anime: {aniListWatchListLoaded.data.Page.pageInfo.total}</div>
{#if aniListWatchListLoaded.data.Page.pageInfo.lastPage <= 12}
<div class="md:hidden">
Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}
</div>
{:else}
<div>
Page: {page} of {aniListWatchListLoaded.data.Page.pageInfo.lastPage}
</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>
<button
type="button"
id="increment-button"
on:click={() => ChangeWatchListPage(page + 1)}
class={page >= aniListWatchListLoaded.data.Page.pageInfo.lastPage
? "border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"
: "bg-gray-700 hover:bg-gray-600 border-gray-600 border rounded-e-lg p-3 h-11 focus:ring-gray-700 focus:ring-2 focus:outline-none"}
disabled={page >= aniListWatchListLoaded.data.Page.pageInfo.lastPage}
>
<svg
class="w-3 h-3 text-white"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 18 18"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 1v16M1 9h16"
/>
</svg>
</button>
</div>
</div>
</div>
</div> </div>

View File

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

View File

@@ -1,162 +1,187 @@
<script lang="ts" context="module"> <script lang="ts" context="module">
import { import {
GetAniListItem, GetAniListItem,
GetAniListLoggedInUser, GetAniListLoggedInUser,
GetAniListUserWatchingList, GetAniListUserWatchingList,
GetMyAnimeListAnime, GetMyAnimeListAnime,
GetMyAnimeListLoggedInUser, GetMyAnimeListLoggedInUser,
GetSimklLoggedInUser, GetSimklLoggedInUser,
LogoutAniList, LogoutAniList,
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,
import type {TableItems} from "../helperTypes/TableTypes"; SimklWatchList,
import {AniListGetSingleAnimeDefaultData} from "../helperDefaults/AniListGetSingleAnime"; } from "../simkl/types/simklTypes";
import {
type AniListUser,
MediaListSort,
} from "../anilist/types/AniListTypes";
import type {
MALAnime,
MALWatchlist,
MyAnimeListUser,
} from "../mal/types/MALTypes";
import type { TableItems } from "../helperTypes/TableTypes";
import { AniListGetSingleAnimeDefaultData } from "../helperDefaults/AniListGetSingleAnime";
export let aniListAnime = writable(AniListGetSingleAnimeDefaultData) export let aniListAnime = writable(AniListGetSingleAnimeDefaultData);
export let title = writable("") export let title = writable("");
export let aniListLoggedIn = writable(false) export let aniListLoggedIn = writable(false);
export let simklLoggedIn = writable(false) export let 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 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 aniWatchlist: AniListCurrentUserWatchList;
let currentAniListAnime: AniListGetSingleAnime 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));
export async function GetAnimeSingleItem(
export async function GetAnimeSingleItem(aniId: number, login: boolean): Promise<""> { aniId: number,
await GetAniListItem(aniId, login).then(aniListResult => { login: boolean,
let finalResult: AniListGetSingleAnime ): Promise<""> {
finalResult = aniListResult await GetAniListItem(aniId, login).then((aniListResult) => {
if (login === false) { let finalResult: AniListGetSingleAnime;
finalResult.data.MediaList.status = "" finalResult = aniListResult;
finalResult.data.MediaList.score = 0 if (login === false) {
finalResult.data.MediaList.progress = 0 finalResult.data.MediaList.status = "";
finalResult.data.MediaList.notes = "" finalResult.data.MediaList.score = 0;
finalResult.data.MediaList.repeat = 0 finalResult.data.MediaList.progress = 0;
finalResult.data.MediaList.startedAt.day = 0 finalResult.data.MediaList.notes = "";
finalResult.data.MediaList.startedAt.month = 0 finalResult.data.MediaList.repeat = 0;
finalResult.data.MediaList.startedAt.year = 0 finalResult.data.MediaList.startedAt.day = 0;
finalResult.data.MediaList.completedAt.day = 0 finalResult.data.MediaList.startedAt.month = 0;
finalResult.data.MediaList.completedAt.month = 0 finalResult.data.MediaList.startedAt.year = 0;
finalResult.data.MediaList.completedAt.year = 0 finalResult.data.MediaList.completedAt.day = 0;
} finalResult.data.MediaList.completedAt.month = 0;
aniListAnime.set(finalResult) finalResult.data.MediaList.completedAt.year = 0;
title.set(currentAniListAnime.data.MediaList.media.title.english === "" ? }
currentAniListAnime.data.MediaList.media.title.romaji : aniListAnime.set(finalResult);
currentAniListAnime.data.MediaList.media.title.english) title.set(
}) currentAniListAnime.data.MediaList.media.title.english === ""
if (isMalLoggedIn) { ? currentAniListAnime.data.MediaList.media.title.romaji
await GetMyAnimeListAnime(currentAniListAnime.data.MediaList.media.idMal).then(malResult => { : currentAniListAnime.data.MediaList.media.title.english,
malAnime.set(malResult) );
}) });
} if (isMalLoggedIn) {
if (isSimklLoggedIn) { await GetMyAnimeListAnime(
await SimklSearch(currentAniListAnime.data.MediaList).then((value: SimklAnime) => { currentAniListAnime.data.MediaList.media.idMal,
simklAnime.set(value) ).then((malResult) => {
}) malAnime.set(malResult);
} });
return ""
} }
if (isSimklLoggedIn) {
export function loginToSimkl(): void { await SimklSearch(currentAniListAnime.data.MediaList).then(
GetSimklLoggedInUser().then(user => { (value: SimklAnime) => {
if (Object.keys(user).length === 0) { simklAnime.set(value);
simklLoggedIn.set(false) },
} else { );
simklUser.set(user)
SimklGetUserWatchlist().then(result => {
simklWatchList.set(result)
simklLoggedIn.set(true)
})
}
})
} }
return "";
}
export function loginToAniList(): void { export function loginToSimkl(): void {
GetAniListLoggedInUser().then(result => { GetSimklLoggedInUser().then((user) => {
aniListUser.set(result) if (Object.keys(user).length === 0) {
if (isAniListPrimary) { simklLoggedIn.set(false);
GetAniListUserWatchingList(page, perPage, MediaListSort.UpdatedTimeDesc).then((result) => { } else {
aniListWatchlist.set(result) simklUser.set(user);
aniListLoggedIn.set(true) SimklGetUserWatchlist().then((result) => {
}) simklWatchList.set(result);
} else { simklLoggedIn.set(true);
aniListLoggedIn.set(true) });
} }
}) });
} }
export function loginToMAL(): void { export function loginToAniList(): void {
GetMyAnimeListLoggedInUser().then(result => { GetAniListLoggedInUser().then((result) => {
malUser.set(result) aniListUser.set(result);
malLoggedIn.set(true) if (isAniListPrimary) {
}) GetAniListUserWatchingList(
} page,
perPage,
MediaListSort.UpdatedTimeDesc,
).then((result) => {
aniListWatchlist.set(result);
aniListLoggedIn.set(true);
});
} else {
aniListLoggedIn.set(true);
}
});
}
export function logoutOfAniList(): void { export function loginToMAL(): void {
LogoutAniList().then(result => { GetMyAnimeListLoggedInUser().then((result) => {
console.log(result) malUser.set(result);
if (Object.keys(aniWatchlist).length !== 0) { malLoggedIn.set(true);
aniListWatchlist.set({} as AniListCurrentUserWatchList) });
} }
aniListUser.set({} as AniListUser)
aniListLoggedIn.set(false)
})
}
export function logoutOfMAL(): void { export function logoutOfAniList(): void {
LogoutMyAnimeList().then(result => { LogoutAniList().then((result) => {
console.log(result) console.log(result);
malUser.set({} as MyAnimeListUser) if (Object.keys(aniWatchlist).length !== 0) {
malLoggedIn.set(false) aniListWatchlist.set({} as AniListCurrentUserWatchList);
}) }
} aniListUser.set({} as AniListUser);
aniListLoggedIn.set(false);
});
}
export function logoutOfSimkl(): void { export function logoutOfMAL(): void {
LogoutSimkl().then(result => { LogoutMyAnimeList().then((result) => {
console.log(result) console.log(result);
simklUser.set({} as SimklUser) malUser.set({} as MyAnimeListUser);
simklLoggedIn.set(false) malLoggedIn.set(false);
}) });
} }
export function logoutOfSimkl(): void {
LogoutSimkl().then((result) => {
console.log(result);
simklUser.set({} as SimklUser);
simklLoggedIn.set(false);
});
}
</script> </script>

View File

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