Many improvemtns and fixes related to decoupling of svelte and rust on android.
🏗️ Build and Test JellyTau / Run Tests (push) Failing after 18s
🏗️ Build and Test JellyTau / Build Android APK (push) Has been skipped
Traceability Validation / Check Requirement Traces (push) Failing after 2s

This commit is contained in:
2026-02-28 19:50:47 +01:00
parent 07f3bf04ca
commit e8e37649fa
53 changed files with 2309 additions and 792 deletions
+1
View File
@@ -84,6 +84,7 @@
{:else}
<div class="h-screen overflow-y-auto p-4 pb-16 md:pb-4 {isAndroid && $currentMedia && $currentMedia.type !== 'Movie' && $currentMedia.type !== 'Episode' ? 'pb-40' : ''}">
<div class="space-y-8">
<!-- Hero Banner -->
{#if heroItems.length > 0}
<HeroBanner items={heroItems} />
+14 -4
View File
@@ -1,17 +1,22 @@
<script lang="ts">
import { onMount } from "svelte";
import { onMount, onDestroy, setContext } from "svelte";
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import { invoke } from "@tauri-apps/api/core";
import { platform } from "@tauri-apps/plugin-os";
import { auth, isAuthenticated, isLoading as isAuthLoading, currentUser } from "$lib/stores/auth";
import { library } from "$lib/stores/library";
import { currentMedia, isPlaying, playbackPosition, playbackDuration } from "$lib/stores/player";
import { currentMedia, isPlaying, playbackPosition, playbackDuration, shouldShowAudioMiniPlayer } from "$lib/stores/player";
import { useScrollGuard } from "$lib/composables/useScrollGuard";
import Search from "$lib/components/Search.svelte";
import MiniPlayer from "$lib/components/player/MiniPlayer.svelte";
import AudioPlayer from "$lib/components/player/AudioPlayer.svelte";
import SleepTimerModal from "$lib/components/player/SleepTimerModal.svelte";
// Scroll guard prevents accidental taps on library cards during/after scrolling (Android)
const scrollGuard = useScrollGuard(300);
setContext("scrollGuard", scrollGuard);
let { children } = $props();
let searchQuery = $state("");
@@ -48,6 +53,7 @@
return () => {
clearTimeout(timeoutId);
if (pollInterval) clearInterval(pollInterval);
scrollGuard.cleanup();
};
});
@@ -241,8 +247,12 @@
</div>
</header>
<!-- Main content (with padding for nav bar and mini player) -->
<main class="flex-1 overflow-y-auto p-4 pb-16 {$currentMedia && $currentMedia.type !== 'Movie' && $currentMedia.type !== 'Episode' ? (isAndroid ? 'pb-40' : 'pb-40 md:pb-24') : ''}">
<!-- Main content (with padding for bottom nav bar and mini player) -->
<main
class="flex-1 overflow-y-auto p-4"
style="padding-bottom: {$shouldShowAudioMiniPlayer ? (isAndroid ? '11rem' : '7rem') : '5rem'}; overscroll-behavior: contain"
onscroll={scrollGuard.onScroll}
>
{@render children()}
</main>
+13 -1
View File
@@ -1,13 +1,17 @@
<script lang="ts">
import { onMount } from "svelte";
import { onMount, getContext } from "svelte";
import { goto } from "$app/navigation";
import type { Library, MediaItem } from "$lib/api/types";
import { library, libraries, libraryItems, isLibraryLoading, currentLibrary, selectedGenres } from "$lib/stores/library";
import { isServerReachable } from "$lib/stores/connectivity";
import type { useScrollGuard } from "$lib/composables/useScrollGuard";
import LibraryGrid from "$lib/components/library/LibraryGrid.svelte";
import MediaCard from "$lib/components/library/MediaCard.svelte";
import GenreFilter from "$lib/components/library/GenreFilter.svelte";
// Scroll guard from layout - prevents accidental taps during scrolling (Android)
const scrollGuard = getContext<ReturnType<typeof useScrollGuard>>("scrollGuard");
let searchResults = $derived($library.searchResults);
let searchQuery = $derived($library.searchQuery);
@@ -48,6 +52,9 @@
});
async function handleLibraryClick(lib: Library) {
// Prevent accidental taps during scrolling (Android)
if (scrollGuard.isScrollActive()) return;
// Route to dedicated music library page
if (lib.collectionType === "music") {
library.setCurrentLibrary(lib);
@@ -70,6 +77,9 @@
}
function handleItemClick(item: MediaItem | Library) {
// Prevent accidental taps during scrolling (Android)
if (scrollGuard.isScrollActive()) return;
if ("type" in item) {
// It's a MediaItem
const mediaItem = item as MediaItem;
@@ -102,6 +112,8 @@
}
function goBackToLibraries() {
// Prevent accidental taps during scrolling (Android)
if (scrollGuard.isScrollActive()) return;
library.setCurrentLibrary(null);
}
</script>
+7
View File
@@ -25,6 +25,13 @@
connecting = true;
localError = null;
// Reject plain HTTP — all connections must use HTTPS
if (serverUrl.trim().toLowerCase().startsWith("http://")) {
localError = "HTTP connections are not allowed. Please use HTTPS (e.g., https://your-server.com).";
connecting = false;
return;
}
try {
const info = await auth.connectToServer(serverUrl);
serverName = info.name;
+62 -34
View File
@@ -8,7 +8,7 @@
import { library } from "$lib/stores/library";
import { queue, currentQueueItem } from "$lib/stores/queue";
import { downloads, type DownloadInfo } from "$lib/stores/downloads";
import { playbackPosition, playbackDuration } from "$lib/stores/player";
import { playbackPosition, playbackDuration, currentMedia as storeCurrentMedia } from "$lib/stores/player";
import { get } from "svelte/store";
import AudioPlayer from "$lib/components/player/AudioPlayer.svelte";
import VideoPlayer from "$lib/components/player/VideoPlayer.svelte";
@@ -126,6 +126,25 @@
return;
}
// If this track is already playing in the backend, just show the UI
// without restarting playback (e.g., when expanding from MiniPlayer)
const alreadyPlayingMedia = get(storeCurrentMedia);
if (alreadyPlayingMedia?.id === id && !startPosition) {
console.log("loadAndPlay: Track already playing, showing UI without restarting");
isVideo = item.type === "Movie" || item.type === "Episode";
isPlaying = true;
loading = false;
// Sync queue status
try {
const queueStatus = await invoke<{ hasNext: boolean; hasPrevious: boolean }>("player_get_queue");
hasNext = queueStatus.hasNext;
hasPrevious = queueStatus.hasPrevious;
} catch (e) {
// Ignore - queue status will update via polling
}
return;
}
// Determine if this is video content (Movie and Episode are video types)
isVideo = item.type === "Movie" || item.type === "Episode";
@@ -214,17 +233,20 @@
} else {
// Local audio playback via MPV backend
console.log("loadAndPlay: Using MPV backend for offline audio");
await invoke("player_play_item", {
item: {
id: item.id,
title: item.name,
artist: item.artists?.join(", ") || null,
album: item.albumName || null,
duration: item.runTimeTicks ? item.runTimeTicks / 10000000 : null,
artworkUrl: null, // Local file may not have artwork
mediaType: "audio",
streamUrl: localUrl,
jellyfinItemId: item.id,
// Use player_play_tracks - backend fetches all metadata from single ID
const repo = auth.getRepository();
const repositoryHandle = repo.getHandle();
await invoke("player_play_tracks", {
repositoryHandle,
request: {
trackIds: [item.id],
startIndex: 0,
shuffle: false,
context: {
type: "search",
searchQuery: "",
},
},
});
if (startPosition) {
@@ -334,17 +356,20 @@
} else {
// Fallback to single item playback
console.log("loadAndPlay: No audio tracks found in parent, falling back to single item");
await invoke("player_play_item", {
item: {
id: item.id,
title: item.name,
artist: item.artists?.join(", ") || null,
album: item.albumName || null,
duration: item.runTimeTicks ? item.runTimeTicks / 10000000 : null,
artworkUrl: repo.getImageUrl(item.id, "Primary", { maxWidth: 500 }),
mediaType: "audio",
streamUrl: playbackInfo.streamUrl,
jellyfinItemId: item.id,
// Use player_play_tracks - backend fetches all metadata from single ID
const repo = auth.getRepository();
const repositoryHandle = repo.getHandle();
await invoke("player_play_tracks", {
repositoryHandle,
request: {
trackIds: [item.id],
startIndex: 0,
shuffle: false,
context: {
type: "search",
searchQuery: "",
},
},
});
@@ -353,17 +378,20 @@
}
} else {
// No queue parameter - single item playback
await invoke("player_play_item", {
item: {
id: item.id,
title: item.name,
artist: item.artists?.join(", ") || null,
album: item.albumName || null,
duration: item.runTimeTicks ? item.runTimeTicks / 10000000 : null,
artworkUrl: repo.getImageUrl(item.id, "Primary", { maxWidth: 500 }),
mediaType: "audio",
streamUrl: playbackInfo.streamUrl,
jellyfinItemId: item.id,
// Use player_play_tracks - backend fetches all metadata from single ID
const repo = auth.getRepository();
const repositoryHandle = repo.getHandle();
await invoke("player_play_tracks", {
repositoryHandle,
request: {
trackIds: [item.id],
startIndex: 0,
shuffle: false,
context: {
type: "search",
searchQuery: "",
},
},
});
+1 -1
View File
@@ -59,7 +59,7 @@
{#if searchQuery.trim()}
<SearchResults
results={$library.searchResults}
loading={$library.isLoading}
loading={$library.loadingCount > 0}
onItemClick={handleItemClick}
/>
{:else}