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:
parent
e560543181
commit
544ea43a84
18
.dockerignore
Normal file
18
.dockerignore
Normal 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
|
||||
81
.gitea/workflows/build-and-test.yml
Normal file
81
.gitea/workflows/build-and-test.yml
Normal 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
156
BUILD-BUILDER-IMAGE.md
Normal 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
282
DOCKER.md
Normal 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
107
Dockerfile
Normal 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
68
Dockerfile.builder
Normal 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
62
docker-compose.yml
Normal 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
44
scripts/build-builder-image.sh
Executable 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}"
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user