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
This commit is contained in:
2026-03-21 13:25:43 -04:00
parent ca8c8beaf3
commit 8a8baf7f8f
3 changed files with 238 additions and 182 deletions

View File

@@ -1,42 +1,69 @@
<script lang="ts">
import {
aniListLoggedIn,
malLoggedIn,
simklLoggedIn,
} 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} from "../wailsjs/go/main/App";
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"; onMount(async () => {
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));
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());
});
console.log(isAniListLoggedIn)
!isAniListLoggedIn && await CheckIfAniListLoggedInAndLoadWatchList()
!isMALLoggedIn && await CheckIfMALLoggedInAndSetUser()
!isSimklLoggedIn && await CheckIfSimklLoggedInAndSetUser()
})
$: if ($loc?.location === "/" && $watchlistNeedsRefresh) {
(async () => {
if ($aniListLoggedIn && $aniListPrimary) {
await CheckIfAniListLoggedInAndLoadWatchList();
}
if ($malLoggedIn && $malPrimary) {
await GetMyAnimeList(1000).then((w) => malWatchList.set(w));
}
if ($simklLoggedIn && $simklPrimary) {
await SimklGetUserWatchlist().then((w) => simklWatchList.set(w));
}
watchlistNeedsRefresh.set(false);
})();
}
</script>
<Header />
<Router routes={{
'/': Home,
'/anime/:id': wrap({
asyncComponent: () => import('./routes/AnimeRoutePage.svelte'),
conditions: [async () => await CheckIfAniListLoggedIn()],
loadingComponent: Spinner
<Router
routes={{
"/": Home,
"/anime/:id": wrap({
asyncComponent: () => import("./routes/AnimeRoutePage.svelte"),
conditions: [async () => await CheckIfAniListLoggedIn()],
loadingComponent: Spinner,
}),
// '*': "Not Found"
}} />
}}
/>

View File

@@ -6,6 +6,7 @@
malLoggedIn,
simklAnime,
simklLoggedIn,
watchlistNeedsRefresh,
} from "../helperModules/GlobalVariablesAndHelperFunctions.svelte";
import { push } from "svelte-spa-router";
import WebsiteLink from "./WebsiteLink.svelte";
@@ -362,6 +363,7 @@
} finally {
submitting.set(false);
submitSuccess.set(true);
watchlistNeedsRefresh.set(true);
setTimeout(() => submitSuccess.set(false), 2000);
}
};
@@ -422,6 +424,7 @@
} finally {
submitting.set(false);
submitSuccess.set(true);
watchlistNeedsRefresh.set(true);
setTimeout(() => submitSuccess.set(false), 2000);
}
};
@@ -702,6 +705,7 @@
Sync Changes
</button>
<button
type="button"
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
hover:border-gray-600"

View File

@@ -1,162 +1,187 @@
<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,
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";
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 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 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 isAniListPrimary: boolean;
let page: number;
let perPage: number;
let aniWatchlist: AniListCurrentUserWatchList;
let currentAniListAnime: AniListGetSingleAnime;
let isMalLoggedIn: boolean
let isSimklLoggedIn: boolean
let isMalLoggedIn: boolean;
let isSimklLoggedIn: boolean;
aniListPrimary.subscribe(value => isAniListPrimary = value)
watchListPage.subscribe(value => page = value)
animePerPage.subscribe(value => perPage = value)
aniListWatchlist.subscribe(value => aniWatchlist = value)
malLoggedIn.subscribe(value => isMalLoggedIn = value)
simklLoggedIn.subscribe(value => isSimklLoggedIn = value)
aniListAnime.subscribe(value => currentAniListAnime = value)
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)
})
}
if (isSimklLoggedIn) {
await SimklSearch(currentAniListAnime.data.MediaList).then((value: SimklAnime) => {
simklAnime.set(value)
})
}
return ""
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 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)
})
}
})
if (isSimklLoggedIn) {
await SimklSearch(currentAniListAnime.data.MediaList).then(
(value: SimklAnime) => {
simklAnime.set(value);
},
);
}
return "";
}
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 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 loginToMAL(): void {
GetMyAnimeListLoggedInUser().then(result => {
malUser.set(result)
malLoggedIn.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 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 loginToMAL(): void {
GetMyAnimeListLoggedInUser().then((result) => {
malUser.set(result);
malLoggedIn.set(true);
});
}
export function logoutOfMAL(): void {
LogoutMyAnimeList().then(result => {
console.log(result)
malUser.set({} as MyAnimeListUser)
malLoggedIn.set(false)
})
}
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>
export function logoutOfSimkl(): void {
LogoutSimkl().then(result => {
console.log(result)
simklUser.set({} as SimklUser)
simklLoggedIn.set(false)
})
}
</script>