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:
Duncan Tourolle 2026-01-27 16:04:57 +01:00
parent e560543181
commit 544ea43a84
14 changed files with 949 additions and 102 deletions

18
.dockerignore Normal file
View File

@ -0,0 +1,18 @@
node_modules
.git
.gitignore
.claude
.svelte-kit
build
dist
.env
.env.local
.vscode
.idea
target
*.apk
*.aab
*.log
coverage
src-tauri/gen
src-tauri/target

View File

@ -0,0 +1,81 @@
name: '🏗️ Build and Test JellyTau'
on:
push:
branches:
- master
paths-ignore:
- '**/*.md'
pull_request:
branches:
- master
paths-ignore:
- '**/*.md'
workflow_dispatch:
jobs:
build:
name: Build APK and Run Tests
runs-on: ubuntu-latest
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Cache Rust dependencies
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
src-tauri/target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Cache Node dependencies
uses: actions/cache@v3
with:
path: |
~/.bun/install/cache
node_modules
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
restore-keys: |
${{ runner.os }}-bun-
- name: Install dependencies
run: |
bun install
- name: Run frontend tests
run: bun test
- name: Run Rust tests
run: |
cd src-tauri
cargo test
cd ..
- name: Build frontend
run: bun run build
- name: Build Android APK
id: build
run: |
mkdir -p artifacts
bun run tauri android build --apk true
# Find the generated APK file
ARTIFACT=$(find src-tauri/gen/android/app/build/outputs/apk -name "*.apk" -type f -print -quit)
echo "artifact=${ARTIFACT}" >> $GITHUB_OUTPUT
echo "Found artifact: ${ARTIFACT}"
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: jellytau-apk
path: ${{ steps.build.outputs.artifact }}
retention-days: 30
if-no-files-found: error

156
BUILD-BUILDER-IMAGE.md Normal file
View File

@ -0,0 +1,156 @@
# Building and Pushing the JellyTau Builder Image
This document explains how to create and push the pre-built builder Docker image to your registry for use in Gitea Act CI/CD.
## Prerequisites
- Docker installed and running
- Access to your Docker registry (e.g., `gitea.tourolle.paris`)
- Docker registry credentials configured (`docker login`)
## Building the Builder Image
### Step 1: Build the Image Locally
```bash
# From the project root
docker build -f Dockerfile.builder -t jellytau-builder:latest .
```
This creates a local image with:
- All system dependencies
- Rust with Android targets
- Android SDK and NDK
- Node.js and Bun
- All build tools pre-installed
### Step 2: Tag for Your Registry
Replace `gitea.tourolle.paris/dtourolle` with your actual registry path:
```bash
docker tag jellytau-builder:latest gitea.tourolle.paris/dtourolle/jellytau-builder:latest
```
### Step 3: Login to Your Registry
If not already logged in:
```bash
docker login gitea.tourolle.paris
```
### Step 4: Push to Registry
```bash
docker push gitea.tourolle.paris/dtourolle/jellytau-builder:latest
```
## Complete One-Liner
```bash
docker build -f Dockerfile.builder -t jellytau-builder:latest . && \
docker tag jellytau-builder:latest gitea.tourolle.paris/dtourolle/jellytau-builder:latest && \
docker push gitea.tourolle.paris/dtourolle/jellytau-builder:latest
```
## Verifying the Build
Check that the image was pushed successfully:
```bash
# List images in your registry (depends on registry API support)
docker search gitea.tourolle.paris/dtourolle/jellytau-builder
# Or pull and test locally
docker pull gitea.tourolle.paris/dtourolle/jellytau-builder:latest
docker run -it gitea.tourolle.paris/dtourolle/jellytau-builder:latest bun --version
```
## Using in CI/CD
The workflow at `.gitea/workflows/build-and-test.yml` automatically uses:
```yaml
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:latest
```
Once pushed, your CI/CD pipeline will use this pre-built image instead of installing everything during the build, saving significant time.
## Updating the Builder Image
When dependencies change (new Rust version, Android SDK update, etc.):
1. Update `Dockerfile.builder` with the new configuration
2. Rebuild and push with a new tag:
```bash
docker build -f Dockerfile.builder -t jellytau-builder:v1.2.0 .
docker tag jellytau-builder:v1.2.0 gitea.tourolle.paris/dtourolle/jellytau-builder:v1.2.0
docker push gitea.tourolle.paris/dtourolle/jellytau-builder:v1.2.0
```
3. Update the workflow to use the new tag:
```yaml
container:
image: gitea.tourolle.paris/dtourolle/jellytau-builder:v1.2.0
```
## Image Contents
The builder image includes:
- **Base OS**: Ubuntu 24.04
- **Languages**:
- Rust (stable) with targets: aarch64-linux-android, armv7-linux-androideabi, x86_64-linux-android
- Node.js 20.x
- OpenJDK 17 (for Android)
- **Tools**:
- Bun package manager
- Android SDK 34
- Android NDK 27.0.11902837
- Build essentials (gcc, make, etc.)
- Git, curl, wget
- libssl, libclang development libraries
- **Pre-configured**:
- Rust toolchain components (rustfmt, clippy)
- Android SDK/NDK environment variables
- All paths optimized for building
## Build Time
First build takes ~15-20 minutes depending on internet speed (downloads Android SDK/NDK).
Subsequent builds are cached and take seconds.
## Storage
The built image is approximately **4-5 GB**. Ensure your registry has sufficient storage.
## Troubleshooting
### "Image not found" in CI
- Verify the image name matches exactly in the workflow
- Check that the image was successfully pushed: `docker push` output should show successful layers
- Ensure Gitea has access to your registry (check network/firewall)
### Build fails with "command not found"
- The image may not have finished pushing. Wait a few moments and retry the CI job.
- Check that all layers were pushed successfully in the push output.
### Registry authentication in CI
If your registry requires credentials in CI:
1. Create a deploy token in your registry
2. Add to Gitea secrets as `REGISTRY_USERNAME` and `REGISTRY_TOKEN`
3. Use in workflow:
```yaml
- name: Login to Registry
run: |
docker login gitea.tourolle.paris -u ${{ secrets.REGISTRY_USERNAME }} -p ${{ secrets.REGISTRY_TOKEN }}
```
## References
- [Docker Build Documentation](https://docs.docker.com/build/)
- [Docker Push Documentation](https://docs.docker.com/engine/reference/commandline/push/)
- [Dockerfile Reference](https://docs.docker.com/engine/reference/builder/)

282
DOCKER.md Normal file
View File

@ -0,0 +1,282 @@
# Docker & CI/CD Setup for JellyTau
This document explains how to use the Docker configuration and Gitea Act CI/CD pipeline for building and testing JellyTau.
## Overview
The setup includes:
- **Dockerfile.builder**: Pre-built image with all dependencies (push to your registry)
- **Dockerfile**: Multi-stage build for local testing and building
- **docker-compose.yml**: Orchestration for local development and testing
- **.gitea/workflows/build-and-test.yml**: Automated CI/CD pipeline using pre-built builder image
### Quick Start
**For CI/CD (Gitea Actions)**:
1. Build and push builder image (see [BUILD-BUILDER-IMAGE.md](BUILD-BUILDER-IMAGE.md))
2. Push to master branch - workflow runs automatically
3. Check Actions tab for results and APK artifacts
**For Local Testing**:
```bash
docker-compose run test # Run tests
docker-compose run android-build # Build APK
docker-compose run dev # Interactive shell
```
## Docker Usage
### Prerequisites
- Docker Engine 20.10+
- Docker Compose 2.0+ (if using docker-compose)
- At least 10GB free disk space (for Android SDK and build artifacts)
### Building the Docker Image
```bash
# Build the complete image
docker build -t jellytau:latest .
# Build specific target
docker build -t jellytau:test --target test .
docker build -t jellytau:android --target android-build .
```
### Using Docker Compose
#### Run Tests Only
```bash
docker-compose run test
```
This will:
1. Install all dependencies
2. Run frontend tests (Vitest)
3. Run Rust backend tests
4. Report results
#### Build Android APK
```bash
docker-compose run android-build
```
This will:
1. Run tests first (depends on test service)
2. If tests pass, build the Android APK
3. Output APK files to `src-tauri/gen/android/app/build/outputs/apk/`
#### Interactive Development
```bash
docker-compose run dev
```
This starts an interactive shell with all development tools available. From here you can:
```bash
bun install
bun run build
bun test
bun run tauri android build --apk true
```
#### Run All Services in Sequence
```bash
docker-compose up --abort-on-container-exit
```
### Extracting Build Artifacts
After a successful build, APK files are located in:
```
src-tauri/gen/android/app/build/outputs/apk/
```
Copy to your host machine:
```bash
docker cp jellytau-android-build:/app/src-tauri/gen/android/app/build/outputs/apk ./apk-output
```
## Gitea Act CI/CD Pipeline
The `.gitea/workflows/build-and-test.yml` workflow automates:
**Single Job**: Runs on every push to `master` and PRs
- Uses pre-built builder image (no setup time)
- Installs project dependencies
- Runs frontend tests (Vitest)
- Runs Rust backend tests
- Builds the frontend
- Builds the Android APK
- Uploads APK as artifact (30-day retention)
The workflow skips markdown files to avoid unnecessary builds.
### Workflow Triggers
The workflow runs on:
- Push to `master` or `main` branches
- Pull requests to `master` or `main` branches
- Can be extended with: `workflow_dispatch` for manual triggers
### Setting Up the Builder Image
Before using the CI/CD pipeline, you must build and push the builder image:
```bash
# Build the image
docker build -f Dockerfile.builder -t jellytau-builder:latest .
# Tag for your registry
docker tag jellytau-builder:latest gitea.tourolle.paris/dtourolle/jellytau-builder:latest
# Push to registry
docker push gitea.tourolle.paris/dtourolle/jellytau-builder:latest
```
See [BUILD-BUILDER-IMAGE.md](BUILD-BUILDER-IMAGE.md) for detailed instructions.
### Setting Up Gitea Act
1. **Ensure builder image is pushed** (see above)
2. **Push to Gitea repository**:
The workflow will automatically trigger on push to `master` or pull requests
3. **View workflow runs in Gitea UI**:
- Navigate to your repository
- Go to Actions tab
- Click on workflow runs to see logs
4. **Test locally** (optional):
```bash
# Install act if needed
curl https://gitea.com/actions/setup-act/releases/download/v0.25.0/act-0.25.0-linux-x86_64.tar.gz | tar xz
# Run locally (requires builder image to be available)
./act push --file .gitea/workflows/build-and-test.yml
```
### Customizing the Workflow
#### Modify Build Triggers
Edit `.gitea/workflows/build-and-test.yml` to change when builds run:
```yaml
on:
push:
branches:
- master
- develop # Add more branches
paths:
- 'src/**' # Only run if src/ changes
- 'src-tauri/**' # Only run if Rust code changes
```
#### Add Notifications
Add Slack, Discord, or email notifications on build completion:
```yaml
- name: Notify on success
if: success()
run: |
curl -X POST https://slack-webhook-url...
```
#### Customize APK Upload
Modify artifact retention or add to cloud storage:
```yaml
- name: Upload APK to S3
uses: actions/s3-sync@v1
with:
aws_access_key_id: ${{ secrets.AWS_ACCESS_KEY }}
aws_secret_access_key: ${{ secrets.AWS_SECRET_KEY }}
aws_bucket: my-apk-bucket
source_dir: src-tauri/gen/android/app/build/outputs/apk/
```
## Environment Setup in CI
### Secret Variables
To use secrets in the workflow, set them in Gitea:
1. Go to Repository Settings → Secrets
2. Add secrets like:
- `AWS_ACCESS_KEY` for S3 uploads
- `SLACK_WEBHOOK_URL` for notifications
- `GITHUB_TOKEN` for releases (pre-configured)
## Troubleshooting
### Out of Memory During Build
Android builds are memory-intensive. If you get OOM errors:
```bash
# Limit memory in docker-compose
services:
android-build:
deploy:
resources:
limits:
memory: 6G
```
Or increase Docker's memory allocation in Docker Desktop settings.
### Android SDK Download Timeout
If downloads timeout, increase timeout or download manually:
```bash
# In container, with longer timeout
timeout 600 sdkmanager --sdk_root=$ANDROID_HOME ...
```
### Rust Compilation Errors
Make sure Rust is updated:
```bash
rustup update
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
```
### Cache Issues
Clear Docker cache and rebuild:
```bash
docker-compose down -v # Remove volumes
docker system prune # Clean up dangling images
docker-compose up --build
```
## Performance Tips
1. **Cache Reuse**: Both Docker and Gitea Act cache dependencies across runs
2. **Parallel Steps**: The workflow runs frontend and Rust tests in series; consider parallelizing for faster CI
3. **Incremental Builds**: Rust and Node caches persist between runs
4. **Docker Buildkit**: Enable for faster builds:
```bash
DOCKER_BUILDKIT=1 docker build .
```
## Security Considerations
- Dockerfile uses `ubuntu:24.04` base image from official Docker Hub
- NDK is downloaded from official Google servers (verified via HTTPS)
- No credentials are stored in the Dockerfile
- Use Gitea Secrets for sensitive values (API keys, tokens, etc.)
- Lock dependency versions in `Cargo.toml` and `package.json`
## Next Steps
1. Test locally with `docker-compose up`
2. Push to your Gitea repository
3. Monitor workflow runs in the Actions tab
4. Configure secrets in repository settings for production builds
5. Set up artifact retention policies (currently 30 days)
## References
- [Gitea Actions Documentation](https://docs.gitea.io/en-us/actions/)
- [Docker Multi-stage Builds](https://docs.docker.com/build/building/multi-stage/)
- [Android Build Tools](https://developer.android.com/studio/command-line)
- [Tauri Android Guide](https://tauri.app/v1/guides/building/android)

107
Dockerfile Normal file
View File

@ -0,0 +1,107 @@
# Multi-stage build for JellyTau - Tauri Jellyfin client
FROM ubuntu:24.04 AS builder
ENV DEBIAN_FRONTEND=noninteractive \
ANDROID_HOME=/opt/android-sdk \
NDK_VERSION=27.0.11902837 \
SDK_VERSION=34 \
RUST_BACKTRACE=1
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
# Build essentials
build-essential \
curl \
wget \
git \
ca-certificates \
# Node.js (for Bun)
nodejs=20.* \
npm \
# Rust toolchain
rustc \
cargo \
# JDK for Android
openjdk-17-jdk-headless \
# Android build tools
android-sdk-platform-tools \
# Additional development tools
pkg-config \
libssl-dev \
libclang-dev \
llvm-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Bun
RUN curl -fsSL https://bun.sh/install | bash && \
ln -s /root/.bun/bin/bun /usr/local/bin/bun
# Setup Rust for Android targets
RUN rustup update && \
rustup target add aarch64-linux-android && \
rustup target add armv7-linux-androideabi && \
rustup target add x86_64-linux-android
# Setup Android SDK
RUN mkdir -p $ANDROID_HOME && \
mkdir -p /root/.android && \
echo '### User Sources for `android` cmd line tool ###' > /root/.android/repositories.cfg && \
echo 'count=0' >> /root/.android/repositories.cfg
# Download and setup Android Command Line Tools
RUN wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdline-tools.zip && \
unzip -q /tmp/cmdline-tools.zip -d $ANDROID_HOME && \
rm /tmp/cmdline-tools.zip && \
mkdir -p $ANDROID_HOME/cmdline-tools/latest && \
mv $ANDROID_HOME/cmdline-tools/* $ANDROID_HOME/cmdline-tools/latest/ 2>/dev/null || true
# Setup Android SDK components
RUN $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_HOME \
"platforms;android-$SDK_VERSION" \
"build-tools;34.0.0" \
"ndk;$NDK_VERSION" \
--channel=0 2>&1 | grep -v "Warning" || true
# Set NDK environment variable
ENV NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION
# Create working directory
WORKDIR /app
# Copy project files
COPY . .
# Install Node.js dependencies
RUN bun install
# Install Rust dependencies
RUN cd src-tauri && cargo fetch && cd ..
# Build stage - Tests
FROM builder AS test
WORKDIR /app
RUN echo "Running tests..." && \
bun run test && \
cd src-tauri && cargo test && cd .. && \
echo "All tests passed!"
# Build stage - APK
FROM builder AS android-build
WORKDIR /app
RUN cd src-tauri && cargo fetch && cd .. && \
echo "Building Android APK..." && \
bun run build && \
bun run tauri android build --apk true && \
echo "APK build complete!"
# Final output stage
FROM ubuntu:24.04 AS final
RUN apt-get update && apt-get install -y --no-install-recommends \
android-sdk-platform-tools \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=android-build /app/src-tauri/gen/android/app/build/outputs/apk /app/apk
VOLUME ["/app/apk"]
CMD ["/bin/bash", "-c", "echo 'APK files are available in /app/apk' && ls -lh /app/apk/"]

68
Dockerfile.builder Normal file
View File

@ -0,0 +1,68 @@
# JellyTau Builder Image
# Pre-built image with all dependencies for building and testing
# Push to your registry: docker build -f Dockerfile.builder -t gitea.tourolle.paris/dtourolle/jellytau-builder:latest .
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive \
ANDROID_HOME=/opt/android-sdk \
NDK_VERSION=27.0.11902837 \
SDK_VERSION=34 \
RUST_BACKTRACE=1 \
PATH="/root/.bun/bin:/root/.cargo/bin:$PATH"
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
curl \
wget \
git \
ca-certificates \
nodejs \
npm \
rustc \
cargo \
openjdk-17-jdk-headless \
pkg-config \
libssl-dev \
libclang-dev \
llvm-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Bun
RUN curl -fsSL https://bun.sh/install | bash && \
ln -s /root/.bun/bin/bun /usr/local/bin/bun
# Setup Rust for Android targets
RUN rustup update && \
rustup target add aarch64-linux-android && \
rustup target add armv7-linux-androideabi && \
rustup target add x86_64-linux-android && \
rustup component add rustfmt clippy
# Setup Android SDK
RUN mkdir -p $ANDROID_HOME && \
mkdir -p /root/.android && \
echo '### User Sources for `android` cmd line tool ###' > /root/.android/repositories.cfg && \
echo 'count=0' >> /root/.android/repositories.cfg
# Download and setup Android Command Line Tools
RUN wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdline-tools.zip && \
unzip -q /tmp/cmdline-tools.zip -d $ANDROID_HOME && \
rm /tmp/cmdline-tools.zip && \
mkdir -p $ANDROID_HOME/cmdline-tools/latest && \
mv $ANDROID_HOME/cmdline-tools/* $ANDROID_HOME/cmdline-tools/latest/ 2>/dev/null || true
# Install Android SDK components
RUN $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_HOME \
"platforms;android-$SDK_VERSION" \
"build-tools;34.0.0" \
"ndk;$NDK_VERSION" \
--channel=0 2>&1 | grep -v "Warning" || true
# Set NDK environment variable
ENV NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION
WORKDIR /app
ENTRYPOINT ["/bin/bash"]

62
docker-compose.yml Normal file
View File

@ -0,0 +1,62 @@
version: '3.8'
services:
# Test service - runs tests only
test:
build:
context: .
dockerfile: Dockerfile
target: test
container_name: jellytau-test
volumes:
- .:/app
environment:
- RUST_BACKTRACE=1
command: bash -c "bun test && cd src-tauri && cargo test && cd .. && echo 'All tests passed!'"
# Android build service - builds APK after tests pass
android-build:
build:
context: .
dockerfile: Dockerfile
target: android-build
container_name: jellytau-android-build
volumes:
- .:/app
- android-cache:/root/.cargo
- android-bun-cache:/root/.bun
environment:
- RUST_BACKTRACE=1
- ANDROID_HOME=/opt/android-sdk
depends_on:
- test
ports:
- "5172:5172" # In case you want to run dev server
# Development container - for interactive development
dev:
build:
context: .
dockerfile: Dockerfile
target: builder
container_name: jellytau-dev
volumes:
- .:/app
- cargo-cache:/root/.cargo
- bun-cache:/root/.bun
- node-modules:/app/node_modules
environment:
- RUST_BACKTRACE=1
- ANDROID_HOME=/opt/android-sdk
- NDK_HOME=/opt/android-sdk/ndk/27.0.11902837
working_dir: /app
stdin_open: true
tty: true
command: /bin/bash
volumes:
cargo-cache:
bun-cache:
android-cache:
android-bun-cache:
node-modules:

44
scripts/build-builder-image.sh Executable file
View File

@ -0,0 +1,44 @@
#!/bin/bash
# Build and push the JellyTau builder Docker image to your registry
set -e
# Configuration
REGISTRY_HOST="${REGISTRY_HOST:-gitea.tourolle.paris}"
REGISTRY_USER="${REGISTRY_USER:-dtourolle}"
IMAGE_NAME="jellytau-builder"
IMAGE_TAG="${1:-latest}"
FULL_IMAGE_NAME="${REGISTRY_HOST}/${REGISTRY_USER}/${IMAGE_NAME}:${IMAGE_TAG}"
echo "🐳 Building JellyTau Builder Image"
echo "=================================="
echo "Registry: $REGISTRY_HOST"
echo "User: $REGISTRY_USER"
echo "Image: $FULL_IMAGE_NAME"
echo ""
# Step 1: Build locally
echo "🔨 Building Docker image locally..."
docker build -f Dockerfile.builder -t ${IMAGE_NAME}:${IMAGE_TAG} .
# Step 2: Tag for registry
echo "🏷️ Tagging for registry..."
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${FULL_IMAGE_NAME}
# Step 3: Login to registry (if not already logged in)
echo "🔐 Checking registry authentication..."
if ! docker info | grep -q "Username"; then
echo "Not authenticated to Docker. Logging in to ${REGISTRY_HOST}..."
docker login ${REGISTRY_HOST}
fi
# Step 4: Push to registry
echo "📤 Pushing image to registry..."
docker push ${FULL_IMAGE_NAME}
echo ""
echo "✅ Successfully built and pushed: ${FULL_IMAGE_NAME}"
echo ""
echo "Update your workflow to use:"
echo " container:"
echo " image: ${FULL_IMAGE_NAME}"

View File

@ -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>

View File

@ -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}

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}

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}

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}

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>