Compare commits

..
2 Commits
Author SHA1 Message Date
dtourolleandClaude Opus 4.8 7b8a8f66e5 CI: make versionCode step POSIX sh compatible
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m24s
Traceability Validation / Check Requirement Traces (push) Successful in 24s
Build & Release / Run Tests (push) Successful in 5m24s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m31s
Build & Release / Build Linux (push) Successful in 17m40s
Build & Release / Build Android (push) Successful in 22m33s
Build & Release / Create Release (push) Successful in 14s
The runner executes workflow steps with /bin/sh (dash), which has no
here-strings: `IFS='.' read -r MAJ MIN PAT <<< "$VERSION"` failed with
"Syntax error: redirection unexpected" and aborted the Android release build.

Parse the semver with `cut` instead, drop the GNU-only `\s` from the sed
expression in favour of [[:space:]], and default any missing component to 0 so a
malformed version can never emit versionCode 0. Verified under sh:
0.0.14 -> 1014, 0.0.15 -> 1015, 0.1.0 -> 1100, 1.0.0 -> 11000 (monotonic).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 12:23:04 +02:00
dtourolleandClaude Opus 4.8 2e479d05b3 Navigation up/back split, faster startup, and CI versionCode fix
🏗️ Build and Test JellyTau / Run Tests (push) Successful in 4m57s
Traceability Validation / Check Requirement Traces (push) Successful in 22s
Build & Release / Run Tests (push) Successful in 5m13s
🏗️ Build and Test JellyTau / Android Compile Check (push) Successful in 4m30s
Build & Release / Build Linux (push) Successful in 17m52s
Build & Release / Build Android (push) Failing after 58s
Build & Release / Create Release (push) Has been skipped
Navigation:
- Split conflated "back" into navigateUp (deterministic route parent) and a
  history-safe navigateBack that tracks in-app depth via afterNavigate instead
  of history.length. Fixes the resume-from-background trap where a stale WebView
  stack left the header arrow stuck on the current page.
- /library self-corrects for music/tv/movies (which have dedicated landing
  pages): a leftover currentLibrary no longer forces the inline content-list
  view, so "up"/back shows the libraries overview. Live TV / channels / other
  types still render inline.

Startup (unblock first paint):
- auth.initialize() no longer awaits security-status, player-config, or session
  verification before flipping isInitialized. These run fire-and-forget after the
  session is restored, so the library overview paints without waiting on several
  serial IPC round-trips.

Versioning / CI:
- tauri.conf.json + package.json aligned to 0.0.15 (the tag series had drifted to
  0.1.0, whose formula-derived versionCode 1000 outran the v0.0.x tags).
- Release workflow now pins a monotonic Android versionCode
  (1000 + major*10000 + minor*100 + patch) so tagged builds never downgrade
  below prior installs and always increase in semver order.

Tests: navigation (4), auth (29), playbackMode (23) green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 21:12:36 +02:00
13 changed files with 283 additions and 94 deletions
+32 -3
View File
@@ -161,10 +161,10 @@ jobs:
- name: Set app version from tag - name: Set app version from tag
run: | run: |
REF="${GITHUB_REF#refs/tags/v}" # On a tag build, the tag is the single source of truth for the
VERSION="${REF#refs/heads/}" # version name. On non-tag runs keep whatever is in tauri.conf.json.
# On non-tag runs keep whatever is in tauri.conf.json
if echo "$GITHUB_REF" | grep -q '^refs/tags/v'; then if echo "$GITHUB_REF" | grep -q '^refs/tags/v'; then
VERSION="${GITHUB_REF#refs/tags/v}"
echo "Setting version to $VERSION" echo "Setting version to $VERSION"
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" src-tauri/tauri.conf.json sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" src-tauri/tauri.conf.json
fi fi
@@ -173,6 +173,35 @@ jobs:
- name: Initialize Android project - name: Initialize Android project
run: bun run tauri android init run: bun run tauri android init
- name: Pin a monotonic Android versionCode
run: |
# `tauri android init` autogenerates src-tauri/gen/android/app/tauri.properties
# with a versionCode derived from the semver (e.g. 0.0.15 -> 15). That
# number is (a) tiny and (b) NOT monotonic across our history: earlier
# local/dev builds shipped versionCode 1000 (from a 0.1.0 config), so a
# plain 15 would be a *downgrade* and Android would refuse the update.
#
# Derive an explicit code that is both monotonic in semver order and
# always above the 1000 floor already in the field:
# code = 1000 + major*10000 + minor*100 + patch
# e.g. 0.0.14 -> 1014, 0.0.15 -> 1015, 0.1.0 -> 1100, 1.0.0 -> 11000.
# POSIX sh only (the runner uses dash): no here-strings, no \s in sed.
PROPS="src-tauri/gen/android/app/tauri.properties"
VERSION=$(grep '"version"' src-tauri/tauri.conf.json | head -1 | sed -E 's/.*"version"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')
MAJ=$(echo "$VERSION" | cut -d. -f1)
MIN=$(echo "$VERSION" | cut -d. -f2)
PAT=$(echo "$VERSION" | cut -d. -f3)
# Guard against a malformed/missing component so we never emit code 0.
: "${MAJ:=0}" "${MIN:=0}" "${PAT:=0}"
CODE=$(( 1000 + MAJ*10000 + MIN*100 + PAT ))
echo "version=$VERSION -> versionCode=$CODE"
if grep -q '^tauri.android.versionCode=' "$PROPS"; then
sed -i "s/^tauri.android.versionCode=.*/tauri.android.versionCode=$CODE/" "$PROPS"
else
echo "tauri.android.versionCode=$CODE" >> "$PROPS"
fi
cat "$PROPS"
- name: Sync custom Android sources & gradle config - name: Sync custom Android sources & gradle config
run: ./scripts/sync-android-sources.sh run: ./scripts/sync-android-sources.sh
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "jellytau", "name": "jellytau",
"version": "0.1.0", "version": "0.0.15",
"description": "", "description": "",
"type": "module", "type": "module",
"packageManager": "bun@1.3.5", "packageManager": "bun@1.3.5",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "jellytau", "productName": "jellytau",
"version": "0.1.0", "version": "0.0.15",
"identifier": "com.dtourolle.jellytau", "identifier": "com.dtourolle.jellytau",
"build": { "build": {
"beforeDevCommand": "bun run dev", "beforeDevCommand": "bun run dev",
@@ -3,7 +3,7 @@
import { page } from "$app/stores"; import { page } from "$app/stores";
import { truncateMiddle } from "$lib/utils/truncateMiddle"; import { truncateMiddle } from "$lib/utils/truncateMiddle";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { navigateBack } from "$lib/utils/navigation"; import { navigateUp } from "$lib/utils/navigation";
import { currentLibrary } from "$lib/stores/library"; import { currentLibrary } from "$lib/stores/library";
import { auth } from "$lib/stores/auth"; import { auth } from "$lib/stores/auth";
import SearchBar from "$lib/components/common/SearchBar.svelte"; import SearchBar from "$lib/components/common/SearchBar.svelte";
@@ -138,7 +138,7 @@
selectedGenre = null; selectedGenre = null;
genreItems = []; genreItems = [];
} else { } else {
navigateBack(config.backPath); navigateUp(config.backPath);
} }
} }
@@ -3,7 +3,7 @@
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
import { listen, type UnlistenFn } from "@tauri-apps/api/event"; import { listen, type UnlistenFn } from "@tauri-apps/api/event";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { navigateBack } from "$lib/utils/navigation"; import { navigateUp } from "$lib/utils/navigation";
import { currentLibrary } from "$lib/stores/library"; import { currentLibrary } from "$lib/stores/library";
import { auth } from "$lib/stores/auth"; import { auth } from "$lib/stores/auth";
import { shouldShowAudioMiniPlayer } from "$lib/stores/player"; import { shouldShowAudioMiniPlayer } from "$lib/stores/player";
@@ -176,7 +176,7 @@
} }
function goBack() { function goBack() {
navigateBack(config.backPath); navigateUp(config.backPath);
} }
const searchPlaceholder = $derived(config.searchPlaceholder || `Search ${config.title.toLowerCase()}...`); const searchPlaceholder = $derived(config.searchPlaceholder || `Search ${config.title.toLowerCase()}...`);
+20 -6
View File
@@ -139,7 +139,9 @@ function createAuthStore() {
update((s) => ({ ...s, isLoading: true, error: null })); update((s) => ({ ...s, isLoading: true, error: null }));
try { try {
// Check security status // Check security status — fire-and-forget. It only sets a warning banner,
// so it must not sit in front of session restore (and thus first paint).
void (async () => {
try { try {
const securityStatus = await commands.storageGetSecurityStatus(); const securityStatus = await commands.storageGetSecurityStatus();
console.log("[Auth] Security status:", securityStatus); console.log("[Auth] Security status:", securityStatus);
@@ -153,6 +155,7 @@ function createAuthStore() {
} catch (error) { } catch (error) {
console.warn("[Auth] Failed to get security status:", error); console.warn("[Auth] Failed to get security status:", error);
} }
})();
// Initialize auth manager and get session // Initialize auth manager and get session
console.log("[Auth] Initializing auth manager..."); console.log("[Auth] Initializing auth manager...");
@@ -162,14 +165,19 @@ function createAuthStore() {
if (session) { if (session) {
console.log("[Auth] Restoring session for user:", session.username, "on server:", session.serverUrl); console.log("[Auth] Restoring session for user:", session.username, "on server:", session.serverUrl);
// Create RepositoryClient for cache-first access // Create RepositoryClient for cache-first access. This IS required before
// we mark authenticated — the first screen (library overview) reads
// through it — so keep it awaited.
repository = new RepositoryClient(); repository = new RepositoryClient();
await repository.create(session.serverUrl, session.userId, session.accessToken, session.serverId); await repository.create(session.serverUrl, session.userId, session.accessToken, session.serverId);
// Configure Jellyfin client in Rust player for automatic playback reporting // Configure the Rust player for playback reporting. This is NOT needed to
const deviceId = await getDeviceId(); // render the first screen (it only matters once playback starts), so run
// it fire-and-forget instead of blocking first paint on two more IPC
// round-trips (getDeviceId + playerConfigureJellyfin).
void (async () => {
try { try {
console.log("[Auth] Configuring Rust player with restored session..."); const deviceId = await getDeviceId();
await commands.playerConfigureJellyfin( await commands.playerConfigureJellyfin(
session.serverUrl, session.serverUrl,
session.accessToken, session.accessToken,
@@ -180,6 +188,7 @@ function createAuthStore() {
} catch (error) { } catch (error) {
console.error("[Auth] Failed to configure Rust player:", error); console.error("[Auth] Failed to configure Rust player:", error);
} }
})();
// Set authenticated immediately (offline-first) // Set authenticated immediately (offline-first)
set({ set({
@@ -211,7 +220,11 @@ function createAuthStore() {
console.error("[Auth] Failed to start connectivity monitoring:", error); console.error("[Auth] Failed to start connectivity monitoring:", error);
}); });
// Start background session verification // Start background session verification — fire-and-forget. This is
// already asynchronous work (results arrive via the auth:* events wired
// above), so awaiting getDeviceId + authStartVerification here only
// delayed first paint by two IPC round-trips for no UI benefit.
void (async () => {
try { try {
const verifyDeviceId = await getDeviceId(); const verifyDeviceId = await getDeviceId();
await commands.authStartVerification(verifyDeviceId); await commands.authStartVerification(verifyDeviceId);
@@ -219,6 +232,7 @@ function createAuthStore() {
} catch (error) { } catch (error) {
console.error("[Auth] Failed to start verification:", error); console.error("[Auth] Failed to start verification:", error);
} }
})();
} else { } else {
// No stored session // No stored session
console.log("[Auth] No active session found"); console.log("[Auth] No active session found");
+70 -10
View File
@@ -1,20 +1,79 @@
import { describe, it, expect, vi, beforeEach } from "vitest"; import { describe, it, expect, vi, beforeEach } from "vitest";
import { navigateBack } from "./navigation";
const goto = vi.fn(); const goto = vi.fn();
// Capture the afterNavigate callback so tests can simulate navigations and thus
// drive the in-app depth counter that canGoBack/navigateBack rely on.
let afterNavigateCb: ((nav: { from: unknown; to: unknown; delta?: number }) => void) | null =
null;
vi.mock("$app/navigation", () => ({ vi.mock("$app/navigation", () => ({
goto: (...args: unknown[]) => goto(...args), goto: (...args: unknown[]) => goto(...args),
afterNavigate: (cb: (nav: any) => void) => {
afterNavigateCb = cb;
},
})); }));
describe("navigateBack", () => { import {
navigateUp,
navigateBack,
canGoBack,
registerNavigationTracking,
__resetNavigationDepthForTest,
} from "./navigation";
/** Simulate a SvelteKit navigation to move the depth counter. */
function nav(opts: { from?: boolean; delta?: number }) {
afterNavigateCb?.({
from: opts.from === false ? null : {},
to: {},
delta: opts.delta,
});
}
describe("navigation", () => {
beforeEach(() => { beforeEach(() => {
goto.mockClear(); goto.mockClear();
// registerNavigationTracking is idempotent; the first call in the suite wins
// and wires afterNavigateCb. Ensure it is registered, then reset depth so
// each case starts from the entry page (module state persists otherwise).
registerNavigationTracking();
__resetNavigationDepthForTest();
}); });
it("pops real history when there is in-app history to go back to", () => { describe("navigateUp", () => {
it("always goes to the given parent path, never touching history", () => {
const back = vi.fn();
vi.spyOn(history, "back").mockImplementation(back);
navigateUp("/library/music");
expect(goto).toHaveBeenCalledWith("/library/music");
expect(back).not.toHaveBeenCalled();
});
});
describe("navigateBack / canGoBack", () => {
it("falls back to the path when there is no in-app history yet", () => {
// Fresh session: only the initial load happened (from == null), so depth
// stays at 0 and there is nothing to pop.
nav({ from: false });
expect(canGoBack()).toBe(false);
const back = vi.fn();
vi.spyOn(history, "back").mockImplementation(back);
navigateBack("/library");
expect(goto).toHaveBeenCalledWith("/library");
expect(back).not.toHaveBeenCalled();
});
it("pops history after a real in-app forward navigation", () => {
nav({ from: false }); // initial load
nav({}); // navigated deeper within the app
expect(canGoBack()).toBe(true);
const back = vi.fn(); const back = vi.fn();
vi.spyOn(history, "back").mockImplementation(back); vi.spyOn(history, "back").mockImplementation(back);
vi.spyOn(history, "length", "get").mockReturnValue(3);
navigateBack("/library"); navigateBack("/library");
@@ -22,14 +81,15 @@ describe("navigateBack", () => {
expect(goto).not.toHaveBeenCalled(); expect(goto).not.toHaveBeenCalled();
}); });
it("falls back to the given path on a fresh deep-link (no history)", () => { it("does not go below zero when the user backs out to the entry page", () => {
const back = vi.fn(); nav({ from: false }); // load
vi.spyOn(history, "back").mockImplementation(back); nav({}); // forward → depth 1
vi.spyOn(history, "length", "get").mockReturnValue(1); nav({ delta: -1 }); // back → depth 0
nav({ delta: -1 }); // extra back (e.g. stale delta) must not underflow
expect(canGoBack()).toBe(false);
navigateBack("/library/music"); navigateBack("/library/music");
expect(goto).toHaveBeenCalledWith("/library/music"); expect(goto).toHaveBeenCalledWith("/library/music");
expect(back).not.toHaveBeenCalled(); });
}); });
}); });
+83 -21
View File
@@ -1,18 +1,90 @@
import { goto } from "$app/navigation"; import { goto, afterNavigate } from "$app/navigation";
/** /**
* Navigate "back" using real browser/Android history when possible, falling * App navigation has two distinct affordances (per the Android guidelines):
* back to an explicit path otherwise.
* *
* Hardcoded `goto(backPath)` always sends the user to a fixed screen, which * - **Up** — move to the current screen's *logical parent* in the app
* loses track of where they actually came from (e.g. reaching the genres list * hierarchy (e.g. `/library/music/albums` → `/library/music`). Deterministic,
* from different entry points). Preferring `history.back()` keeps the back * derived from the route, and never depends on how the user got here. This is
* affordance consistent with the platform back gesture and the browser/Android * what the in-app header arrows should do almost everywhere.
* hardware back button.
* *
* We only use history when there is somewhere to go back to *within the app*. * - **Back** — pop the *actual* history stack: return to wherever the user came
* On a fresh deep-link (history length 1, or an external referrer) we fall back * from, which may be a sibling branch (a detail page reached from search vs.
* to `fallbackPath` so the user never gets stranded or bounced out of the app. * from the library) or even outside the app. This is the hardware/gesture
* back button's job; use it in-app only where "return to origin" is genuinely
* better than Up (e.g. a detail page with many entry points).
*
* The old single `navigateBack` conflated the two: it called `history.back()`
* first and only fell back to a path. On resume-from-background the WebView can
* restore a history stack whose `length` is still > 1 but which cannot actually
* go back within the app — so `history.back()` no-ops and the user is trapped on
* the page. Splitting Up (pure `goto`) from Back (tracked in-app depth) removes
* that trap: Up can never get stuck, and Back only fires when we *know* there is
* an in-app entry to return to.
*/
// In-app navigation depth, maintained via the public `afterNavigate` hook rather
// than reading SvelteKit's internal history-state key. Starts at 0 (the entry
// page). Each forward in-app navigation increments it; a popstate (back/forward
// gesture) sets it to the delta-adjusted value. When it is > 0 we know a real
// in-app Back exists and won't strand the user — independent of the WebView's
// possibly-stale `history.length` after a background/restore.
let inAppDepth = 0;
let navHookRegistered = false;
/**
* Register the navigation-depth tracker. Call once from the root layout's
* component init (afterNavigate must run in a component context). Safe to call
* more than once — only the first registration takes effect.
*/
export function registerNavigationTracking(): void {
if (navHookRegistered) return;
navHookRegistered = true;
afterNavigate((nav) => {
// A popstate (hardware/gesture back or forward) carries a delta; apply it so
// depth tracks the true stack position. Programmatic goto/link navigations
// have no delta and move one step deeper.
const delta = nav.delta;
if (typeof delta === "number") {
inAppDepth = Math.max(0, inAppDepth + delta);
} else if (nav.from) {
// A real forward navigation from an existing page (not the initial load).
inAppDepth += 1;
}
});
}
/**
* Reset the tracked depth. Intended for tests only, so each case starts from a
* known baseline (module state persists across a test file otherwise).
*/
export function __resetNavigationDepthForTest(): void {
inAppDepth = 0;
}
/**
* True when there is at least one in-app history entry to pop. Unlike
* `history.length > 1`, this reflects navigations that happened *within this app
* session*, so a stale WebView stack after a background/restore can't fool it.
*/
export function canGoBack(): boolean {
return inAppDepth > 0;
}
/**
* **Up**: go to the given logical parent path. Always deterministic; never
* consults history, so it cannot trap the user. Prefer this for header arrows.
*/
export function navigateUp(parentPath: string): void {
goto(parentPath);
}
/**
* **Back**: return to the previous in-app page when there is one, otherwise fall
* back to `fallbackPath` (typically the logical parent) so the user is never
* stranded. Use only where returning to the exact origin is preferable to Up
* (e.g. a detail page reachable from multiple branches).
*/ */
export function navigateBack(fallbackPath: string): void { export function navigateBack(fallbackPath: string): void {
if (canGoBack()) { if (canGoBack()) {
@@ -21,13 +93,3 @@ export function navigateBack(fallbackPath: string): void {
goto(fallbackPath); goto(fallbackPath);
} }
} }
/**
* True when there is in-app history to pop. `history.length > 1` means the user
* navigated here from another page in this session rather than landing here
* directly (deep link, refresh, or first load).
*/
function canGoBack(): boolean {
if (typeof history === "undefined") return false;
return history.length > 1;
}
+7
View File
@@ -22,9 +22,16 @@
showGlobalMiniPlayer as computeShowGlobalMiniPlayer, showGlobalMiniPlayer as computeShowGlobalMiniPlayer,
routeOwnsLayout as computeRouteOwnsLayout, routeOwnsLayout as computeRouteOwnsLayout,
} from "$lib/utils/layoutShell"; } from "$lib/utils/layoutShell";
import { registerNavigationTracking } from "$lib/utils/navigation";
let { children } = $props(); let { children } = $props();
// Track in-app navigation depth so the header "back" affordance knows when a
// real in-app Back exists (vs. a stale WebView stack after a background /
// restore). Must run during component init — afterNavigate needs a component
// context, not the async onMount callback below.
registerNavigationTracking();
// Layout-shell visibility rules live in one pure, unit-tested module // Layout-shell visibility rules live in one pure, unit-tested module
// ($lib/utils/layoutShell) so they can't drift per route/platform. // ($lib/utils/layoutShell) so they can't drift per route/platform.
// //
+20 -3
View File
@@ -17,6 +17,23 @@
const isMusicLibrary = $derived($currentLibrary?.collectionType === "music"); const isMusicLibrary = $derived($currentLibrary?.collectionType === "music");
// Music/TV/Movies libraries have their own dedicated landing pages
// (/library/music, /library/tv, /library/movies). When `currentLibrary` is one
// of those, any inline "library content" view here is a STALE leftover from
// navigating into that page — showing it makes "up"/back from that page render
// the library's item list instead of the libraries overview. Treat those types
// as "no inline content" so this page always shows the overview for them,
// whether we arrived via the header Up affordance or the hardware back button.
// Live TV / channels / other types still render their content inline here.
const currentLibraryHasDedicatedPage = $derived(
$currentLibrary?.collectionType === "music" ||
$currentLibrary?.collectionType === "tvshows" ||
$currentLibrary?.collectionType === "movies"
);
const showInlineLibraryContent = $derived(
!!$currentLibrary && !currentLibraryHasDedicatedPage
);
// Filter out Playlist libraries - they belong in Music sub-library // Filter out Playlist libraries - they belong in Music sub-library
const visibleLibraries = $derived.by(() => { const visibleLibraries = $derived.by(() => {
return $libraries.filter(lib => lib.collectionType !== "playlists"); return $libraries.filter(lib => lib.collectionType !== "playlists");
@@ -176,8 +193,8 @@
onItemClick={handleItemClick} onItemClick={handleItemClick}
/> />
</div> </div>
{:else if $currentLibrary} {:else if showInlineLibraryContent}
<!-- Library content --> <!-- Library content (live TV / channels / other inline-rendered types) -->
<div class="space-y-6"> <div class="space-y-6">
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<button <button
@@ -189,7 +206,7 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg> </svg>
</button> </button>
<h1 class="text-2xl font-bold text-white">{$currentLibrary.name}</h1> <h1 class="text-2xl font-bold text-white">{$currentLibrary?.name}</h1>
</div> </div>
{#if isMusicLibrary} {#if isMusicLibrary}
+3 -3
View File
@@ -2,8 +2,8 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { navigateBack } from "$lib/utils/navigation"; import { navigateUp } from "$lib/utils/navigation";
import { currentLibrary } from "$lib/stores/library"; import { library, currentLibrary } from "$lib/stores/library";
import { movies } from "$lib/stores/movies"; import { movies } from "$lib/stores/movies";
import { isServerReachable } from "$lib/stores/connectivity"; import { isServerReachable } from "$lib/stores/connectivity";
import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload"; import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload";
@@ -86,7 +86,7 @@
<div class="flex items-center justify-between px-4"> <div class="flex items-center justify-between px-4">
<h1 class="text-3xl font-bold text-white">{$currentLibrary?.name ?? "Movies"}</h1> <h1 class="text-3xl font-bold text-white">{$currentLibrary?.name ?? "Movies"}</h1>
<button <button
onclick={() => navigateBack("/library")} onclick={() => { library.setCurrentLibrary(null); navigateUp("/library"); }}
class="p-2 rounded-lg hover:bg-white/10 transition-colors text-gray-400 hover:text-white" class="p-2 rounded-lg hover:bg-white/10 transition-colors text-gray-400 hover:text-white"
title="Back to libraries" title="Back to libraries"
aria-label="Back to libraries" aria-label="Back to libraries"
+3 -3
View File
@@ -2,9 +2,9 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { navigateBack } from "$lib/utils/navigation"; import { navigateUp } from "$lib/utils/navigation";
import { auth } from "$lib/stores/auth"; import { auth } from "$lib/stores/auth";
import { currentLibrary } from "$lib/stores/library"; import { library, currentLibrary } from "$lib/stores/library";
import { music } from "$lib/stores/music"; import { music } from "$lib/stores/music";
import { isServerReachable } from "$lib/stores/connectivity"; import { isServerReachable } from "$lib/stores/connectivity";
import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload"; import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload";
@@ -108,7 +108,7 @@
<div class="flex items-center justify-between px-4"> <div class="flex items-center justify-between px-4">
<h1 class="text-3xl font-bold text-white">Music</h1> <h1 class="text-3xl font-bold text-white">Music</h1>
<button <button
onclick={() => navigateBack("/library")} onclick={() => { library.setCurrentLibrary(null); navigateUp("/library"); }}
class="p-2 rounded-lg hover:bg-white/10 transition-colors text-gray-400 hover:text-white" class="p-2 rounded-lg hover:bg-white/10 transition-colors text-gray-400 hover:text-white"
title="Back to libraries" title="Back to libraries"
aria-label="Back to libraries" aria-label="Back to libraries"
+3 -3
View File
@@ -2,8 +2,8 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { navigateBack } from "$lib/utils/navigation"; import { navigateUp } from "$lib/utils/navigation";
import { currentLibrary } from "$lib/stores/library"; import { library, currentLibrary } from "$lib/stores/library";
import { tv } from "$lib/stores/tv"; import { tv } from "$lib/stores/tv";
import { isServerReachable } from "$lib/stores/connectivity"; import { isServerReachable } from "$lib/stores/connectivity";
import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload"; import { useServerReachabilityReload } from "$lib/composables/useServerReachabilityReload";
@@ -93,7 +93,7 @@
<div class="flex items-center justify-between px-4"> <div class="flex items-center justify-between px-4">
<h1 class="text-3xl font-bold text-white">{$currentLibrary?.name ?? "TV Shows"}</h1> <h1 class="text-3xl font-bold text-white">{$currentLibrary?.name ?? "TV Shows"}</h1>
<button <button
onclick={() => navigateBack("/library")} onclick={() => { library.setCurrentLibrary(null); navigateUp("/library"); }}
class="p-2 rounded-lg hover:bg-white/10 transition-colors text-gray-400 hover:text-white" class="p-2 rounded-lg hover:bg-white/10 transition-colors text-gray-400 hover:text-white"
title="Back to libraries" title="Back to libraries"
aria-label="Back to libraries" aria-label="Back to libraries"