Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b8a8f66e5 | ||
|
|
2e479d05b3 |
@@ -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
@@ -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,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
@@ -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");
|
||||||
|
|||||||
@@ -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
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user