Fix Android navigation and improve UI responsiveness

- Convert music category buttons from <button> to native <a> links for better Android compatibility
- Convert artist/album nested buttons in TrackList to <a> links to fix HTML validation issues
- Add event handlers with proper stopPropagation to maintain click behavior
- Increase library overview card sizes from medium to large (50% bigger)
- Increase thumbnail sizes in list view from 10x10 to 16x16
- Add console logging for debugging click events on mobile
- Remove preventDefault() handlers that were blocking Android touch events

These changes resolve navigation issues on Android devices where buttons weren't responding to taps. Native <a> links provide better cross-platform compatibility and allow SvelteKit to handle navigation more reliably.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-27 16:04:57 +01:00
co-authored by Claude Haiku 4.5
parent e560543181
commit 544ea43a84
14 changed files with 949 additions and 102 deletions
@@ -125,16 +125,19 @@
function handleItemClick(item: MediaItem) {
// Navigate to detail page for browseable items
goto(`/library/${item.id}`);
console.log('Item clicked:', item.id, item.name);
goto(`/library/${item.id}`).catch(err => {
console.error('Navigation failed:', err);
});
}
function handleTrackClick(track: MediaItem, _index: number) {
// For track lists, navigate to the track's album if available, otherwise detail page
if (track.albumId) {
goto(`/library/${track.albumId}`);
} else {
goto(`/library/${track.id}`);
}
console.log('Track clicked:', track.id, track.name);
const targetId = track.albumId || track.id;
goto(`/library/${targetId}`).catch(err => {
console.error('Navigation failed:', err);
});
}
</script>
@@ -21,7 +21,7 @@
const repo = auth.getRepository();
const tag = "primaryImageTag" in item ? item.primaryImageTag : ("imageTag" in item ? item.imageTag : undefined);
return repo.getImageUrl(item.id, "Primary", {
maxWidth: 80,
maxWidth: 120,
tag,
});
} catch {
@@ -84,8 +84,15 @@
<button
type="button"
onclick={() => onItemClick?.(item)}
class="w-full flex items-center gap-3 p-2 rounded-lg hover:bg-[var(--color-surface)] transition-colors group"
onclick={() => {
console.log('ListItem clicked:', item.id);
onItemClick?.(item);
}}
ontouchend={() => {
console.log('ListItem touched:', item.id);
onItemClick?.(item);
}}
class="w-full flex items-center gap-3 p-3 rounded-lg hover:bg-[var(--color-surface)] transition-colors group cursor-pointer active:scale-98"
>
<!-- Track number or index -->
<span class="text-gray-500 w-6 text-right text-sm flex-shrink-0">
@@ -93,7 +100,7 @@
</span>
<!-- Thumbnail -->
<div class="w-10 h-10 rounded bg-[var(--color-surface)] flex-shrink-0 overflow-hidden relative">
<div class="w-16 h-16 rounded-lg bg-[var(--color-surface)] flex-shrink-0 overflow-hidden relative">
{#if imageUrl}
<img
src={imageUrl}
+9 -2
View File
@@ -91,8 +91,15 @@
<button
type="button"
class="group/card flex flex-col text-left {sizeClasses[size]} flex-shrink-0 transition-transform duration-200 hover:scale-105"
{onclick}
class="group/card flex flex-col text-left {sizeClasses[size]} flex-shrink-0 transition-transform duration-200 hover:scale-105 cursor-pointer active:scale-95"
onclick={() => {
console.log('[MediaCard] click event - item:', item.id, item.name);
onclick?.();
}}
onpointerup={() => {
console.log('[MediaCard] pointer up event - item:', item.id, item.name);
onclick?.();
}}
>
<div class="relative {aspectRatio()} w-full rounded-lg overflow-hidden bg-[var(--color-surface)] shadow-md group-hover/card:shadow-2xl transition-shadow duration-200">
{#if imageUrl}
+45 -35
View File
@@ -142,15 +142,23 @@
}
}
function handleArtistClick(artistId: string, e: Event) {
async function handleArtistClick(artistId: string, e: Event) {
e.stopPropagation();
goto(`/library/${artistId}`);
try {
await goto(`/library/${artistId}`);
} catch (error) {
console.error("Navigation error:", error);
}
}
function handleAlbumClick(albumId: string | undefined, e: Event) {
async function handleAlbumClick(albumId: string | undefined, e: Event) {
if (!albumId) return;
e.stopPropagation();
goto(`/library/${albumId}`);
try {
await goto(`/library/${albumId}`);
} catch (error) {
console.error("Navigation error:", error);
}
}
async function addToQueue(track: MediaItem, position: "next" | "end", e: Event) {
@@ -214,6 +222,7 @@
<div class="w-full group hover:bg-[var(--color-surface-hover)] rounded-lg transition-colors relative {currentlyPlayingId === track.id ? 'bg-[var(--color-jellyfin)]/10 border-l-4 border-[var(--color-jellyfin)]' : ''}">
<!-- Desktop View -->
<button
type="button"
onclick={() => handleTrackClick(track, index)}
disabled={isPlayingTrack !== null}
class="hidden md:grid gap-4 px-4 py-3 items-center w-full text-left cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
@@ -258,13 +267,13 @@
<div class="text-gray-300 truncate flex flex-wrap items-center gap-1">
{#if track.artistItems && track.artistItems.length > 0}
{#each track.artistItems as artist, idx}
<button
type="button"
onclick={(e) => handleArtistClick(artist.id, e)}
class="text-[var(--color-jellyfin)] hover:underline truncate"
<a
href="/library/{artist.id}"
onclick={(e) => e.stopPropagation()}
class="text-[var(--color-jellyfin)] hover:underline truncate cursor-pointer block"
>
{artist.name}
</button>
</a>
{#if idx < track.artistItems.length - 1}
<span>,</span>
{/if}
@@ -279,13 +288,13 @@
{#if showAlbum}
<div class="text-gray-300 truncate">
{#if track.albumId}
<button
type="button"
onclick={(e) => handleAlbumClick(track.albumId, e)}
class="text-[var(--color-jellyfin)] hover:underline truncate"
<a
href="/library/{track.albumId}"
onclick={(e) => e.stopPropagation()}
class="text-[var(--color-jellyfin)] hover:underline truncate cursor-pointer"
>
{track.albumName || "-"}
</button>
</a>
{:else}
{track.albumName || "-"}
{/if}
@@ -333,9 +342,10 @@
<!-- Mobile View -->
<button
type="button"
onclick={() => handleTrackClick(track, index)}
disabled={isPlayingTrack !== null}
class="md:hidden flex items-center gap-3 px-4 py-3 w-full disabled:opacity-50 disabled:cursor-not-allowed"
class="md:hidden flex items-center gap-3 px-4 py-3 w-full disabled:opacity-50 disabled:cursor-not-allowed cursor-pointer"
>
<!-- Track Number -->
<div class="w-8 flex-shrink-0 text-center">
@@ -365,13 +375,13 @@
{#if showArtist && showAlbum}
{#if track.artistItems && track.artistItems.length > 0}
{#each track.artistItems as artist, idx}
<button
type="button"
onclick={(e) => handleArtistClick(artist.id, e)}
class="text-[var(--color-jellyfin)] hover:underline"
<a
href="/library/{artist.id}"
onclick={(e) => e.stopPropagation()}
class="text-[var(--color-jellyfin)] hover:underline cursor-pointer"
>
{artist.name}
</button>
</a>
{#if idx < track.artistItems.length - 1}
<span>,</span>
{/if}
@@ -381,26 +391,26 @@
{/if}
<span></span>
{#if track.albumId}
<button
type="button"
onclick={(e) => handleAlbumClick(track.albumId, e)}
class="text-[var(--color-jellyfin)] hover:underline"
<a
href="/library/{track.albumId}"
onclick={(e) => e.stopPropagation()}
class="text-[var(--color-jellyfin)] hover:underline cursor-pointer"
>
{track.albumName || "-"}
</button>
</a>
{:else}
{track.albumName || "-"}
{/if}
{:else if showArtist}
{#if track.artistItems && track.artistItems.length > 0}
{#each track.artistItems as artist, idx}
<button
type="button"
onclick={(e) => handleArtistClick(artist.id, e)}
class="text-[var(--color-jellyfin)] hover:underline"
<a
href="/library/{artist.id}"
onclick={(e) => e.stopPropagation()}
class="text-[var(--color-jellyfin)] hover:underline cursor-pointer"
>
{artist.name}
</button>
</a>
{#if idx < track.artistItems.length - 1}
<span>,</span>
{/if}
@@ -410,13 +420,13 @@
{/if}
{:else if showAlbum}
{#if track.albumId}
<button
type="button"
onclick={(e) => handleAlbumClick(track.albumId, e)}
class="text-[var(--color-jellyfin)] hover:underline"
<a
href="/library/{track.albumId}"
onclick={(e) => e.stopPropagation()}
class="text-[var(--color-jellyfin)] hover:underline cursor-pointer"
>
{track.albumName || "-"}
</button>
</a>
{:else}
{track.albumName || "-"}
{/if}
+48 -40
View File
@@ -43,17 +43,21 @@
});
async function handleLibraryClick(lib: Library) {
// Route to dedicated music library page
if (lib.collectionType === "music") {
library.setCurrentLibrary(lib);
goto("/library/music");
return;
}
try {
// Route to dedicated music library page
if (lib.collectionType === "music") {
library.setCurrentLibrary(lib);
await goto("/library/music");
return;
}
// For other library types, load items normally
library.setCurrentLibrary(lib);
library.clearGenres();
await library.loadItems(lib.id);
// For other library types, load items normally
library.setCurrentLibrary(lib);
library.clearGenres();
await library.loadItems(lib.id);
} catch (error) {
console.error("Navigation error:", error);
}
}
async function handleGenreFilterChange() {
@@ -64,35 +68,39 @@
}
}
function handleItemClick(item: MediaItem | Library) {
if ("type" in item) {
// It's a MediaItem
const mediaItem = item as MediaItem;
switch (mediaItem.type) {
case "Series":
case "Movie":
case "MusicAlbum":
case "MusicArtist":
case "Folder":
case "CollectionFolder":
case "Playlist":
case "Channel":
case "ChannelFolderItem":
// Navigate to detail view
goto(`/library/${mediaItem.id}`);
break;
case "Episode":
// Episodes play directly
goto(`/player/${mediaItem.id}`);
break;
default:
// For other items, try detail page first
goto(`/library/${mediaItem.id}`);
break;
async function handleItemClick(item: MediaItem | Library) {
try {
if ("type" in item) {
// It's a MediaItem
const mediaItem = item as MediaItem;
switch (mediaItem.type) {
case "Series":
case "Movie":
case "MusicAlbum":
case "MusicArtist":
case "Folder":
case "CollectionFolder":
case "Playlist":
case "Channel":
case "ChannelFolderItem":
// Navigate to detail view
await goto(`/library/${mediaItem.id}`);
break;
case "Episode":
// Episodes play directly
await goto(`/player/${mediaItem.id}`);
break;
default:
// For other items, try detail page first
await goto(`/library/${mediaItem.id}`);
break;
}
} else {
// It's a Library
await handleLibraryClick(item as Library);
}
} else {
// It's a Library
handleLibraryClick(item as Library);
} catch (error) {
console.error("Navigation error:", error);
}
}
@@ -180,11 +188,11 @@
<p>No libraries found</p>
</div>
{:else}
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{#each $libraries as lib (lib.id)}
<MediaCard
item={lib}
size="medium"
size="large"
onclick={() => handleLibraryClick(lib)}
/>
{/each}
+9 -15
View File
@@ -1,6 +1,4 @@
<script lang="ts">
import { goto } from "$app/navigation";
interface Category {
id: string;
name: string;
@@ -46,10 +44,6 @@
route: "/library/music/genres",
},
];
function handleCategoryClick(route: string) {
goto(route);
}
</script>
<div class="space-y-8">
@@ -59,27 +53,27 @@
<h1 class="text-3xl font-bold text-white">Music Library</h1>
<p class="text-gray-400 mt-1">Choose a category to browse</p>
</div>
<button
onclick={() => goto('/library')}
class="p-2 rounded-lg hover:bg-white/10 transition-colors text-gray-400 hover:text-white"
<a
href="/library"
class="p-2 rounded-lg hover:bg-white/10 transition-colors text-gray-400 hover:text-white inline-block"
title="Back to libraries"
>
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
</a>
</div>
<!-- Category Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
{#each categories as category (category.id)}
<button
onclick={() => handleCategoryClick(category.route)}
class="group relative bg-[var(--color-surface)] rounded-xl p-8 hover:bg-[var(--color-surface-hover)] transition-all duration-200 text-left overflow-hidden"
<a
href={category.route}
class="group relative bg-[var(--color-surface)] rounded-xl p-8 hover:bg-[var(--color-surface-hover)] transition-all duration-200 text-left overflow-hidden cursor-pointer active:scale-95 block no-underline"
>
<!-- Background gradient -->
<div
class="absolute inset-0 bg-gradient-to-br from-[var(--color-jellyfin)]/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity"
class="absolute inset-0 bg-gradient-to-br from-[var(--color-jellyfin)]/20 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
></div>
<!-- Content -->
@@ -107,7 +101,7 @@
</svg>
</div>
</div>
</button>
</a>
{/each}
</div>
</div>