Merge branch 'DonutWare:develop' into develop

This commit is contained in:
Jan 2025-06-22 18:25:26 +02:00 committed by GitHub
commit 2c51b9400d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
266 changed files with 12667 additions and 5649 deletions

2
.fvmrc
View file

@ -1,3 +1,3 @@
{
"flutter": "3.29.2"
"flutter": "3.32.1"
}

View file

@ -14,19 +14,87 @@ on:
types:
- opened
- reopened
schedule:
# Run nightly at midnight UTC, but only if there were changes to develop
- cron: "0 0 * * *"
workflow_dispatch:
inputs:
build_type:
description: "Build type (release, nightly, or auto)"
required: false
default: "auto"
type: choice
options:
- auto
- release
- nightly
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
NIGHTLY_TAG: nightly
jobs:
# Check if workflow should run based on trigger conditions
check-trigger:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.check.outputs.should_run }}
build_type: ${{ steps.check.outputs.build_type }}
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
- name: Check if should run and determine build type
id: check
run: |
# Determine build type based on trigger and input
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.build_type }}" != "auto" ]]; then
BUILD_TYPE="${{ github.event.inputs.build_type }}"
elif [[ "${{ startsWith(github.ref, 'refs/tags/v') }}" == "true" ]]; then
BUILD_TYPE="release"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
# Check if there were commits to develop in the last 24 hours
git checkout develop
COMMITS_LAST_24H=$(git log --since="24 hours ago" --oneline | wc -l)
if [[ $COMMITS_LAST_24H -gt 0 ]]; then
BUILD_TYPE="nightly"
else
echo "No commits to develop in the last 24 hours, skipping nightly build"
echo "should_run=false" >> $GITHUB_OUTPUT
exit 0
fi
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
BUILD_TYPE="nightly"
else
# For PRs and other events, build but don't release
BUILD_TYPE="development"
fi
echo "build_type=${BUILD_TYPE}" >> $GITHUB_OUTPUT
echo "should_run=true" >> $GITHUB_OUTPUT
echo "Build type determined: ${BUILD_TYPE}"
fetch-info:
needs: [check-trigger]
if: needs.check-trigger.outputs.should_run == 'true'
runs-on: ubuntu-latest
outputs:
version_name: ${{ steps.fetch.outputs.version_name }}
flutter_version: ${{ steps.fetch.outputs.flutter_version }}
nightly_version: ${{ steps.fetch.outputs.nightly_version }}
build_type: ${{ needs.check-trigger.outputs.build_type }}
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
- name: Fetch version name
- name: Fetch version name and create nightly version
id: fetch
run: |
# Extract version_name from pubspec.yaml
@ -36,29 +104,47 @@ jobs:
# Extract flutter_version from .fvmrc
FLUTTER_VERSION=$(jq -r '.flutter' .fvmrc)
echo "flutter_version=${FLUTTER_VERSION}" >> "$GITHUB_OUTPUT"
# Create nightly version if needed
if [[ "${{ needs.check-trigger.outputs.build_type }}" == "nightly" ]]; then
NIGHTLY_VERSION="${VERSION_NAME}-nightly"
echo "nightly_version=${NIGHTLY_VERSION}" >> "$GITHUB_OUTPUT"
echo "Nightly version: $NIGHTLY_VERSION"
fi
# Print versions for logging
echo "Base version: $VERSION_NAME"
echo "Flutter version: $FLUTTER_VERSION"
shell: bash
build-android:
needs: [fetch-info]
runs-on: ubuntu-latest
outputs:
artifact_suffix: ${{ env.ARTIFACT_SUFFIX }}
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1
- name: Determine Build Type
run: |
if [[ "${{ startsWith(github.ref, 'refs/tags/v') }}" == "true" ]]; then
if [[ "${{ needs.fetch-info.outputs.build_type }}" == "release" ]]; then
echo "BUILD_MODE=release" >> $GITHUB_ENV
echo "ARTIFACT_SUFFIX=release-signed" >> $GITHUB_ENV
echo "AAB_PATH=productionRelease" >> $GITHUB_ENV
elif [[ "${{ needs.fetch-info.outputs.build_type }}" == "nightly" ]]; then
echo "BUILD_MODE=profile" >> $GITHUB_ENV
echo "ARTIFACT_SUFFIX=nightly" >> $GITHUB_ENV
echo "AAB_PATH=productionProfile" >> $GITHUB_ENV
else
echo "BUILD_MODE=profile" >> $GITHUB_ENV
echo "ARTIFACT_SUFFIX=production" >> $GITHUB_ENV
echo "AAB_PATH=productionProfile" >> $GITHUB_ENV
fi
echo "ARTIFACT_SUFFIX=${ARTIFACT_SUFFIX}" >> $GITHUB_OUTPUT
- name: Decode Keystore for release
if: startsWith(github.ref, 'refs/tags/v')
if: needs.fetch-info.outputs.build_type == 'release'
env:
ENCODED_STRING: ${{ secrets.KEYSTORE_BASE_64 }}
RELEASE_KEYSTORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }}
@ -84,10 +170,10 @@ jobs:
check-latest: true
- name: Set up Flutter
uses: subosito/flutter-action@v2.16.0
uses: subosito/flutter-action@v2.19.0
with:
channel: "stable"
flutter-version: ${{needs.fetch-info.outputs.flutter-version}}
flutter-version: ${{needs.fetch-info.outputs.flutter_version}}
cache: true
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
@ -121,10 +207,10 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Flutter
uses: subosito/flutter-action@v2.16.0
uses: subosito/flutter-action@v2.19.0
with:
channel: "stable"
flutter-version: ${{needs.fetch-info.outputs.flutter-version}}
flutter-version: ${{needs.fetch-info.outputs.flutter_version}}
cache: true
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
@ -141,7 +227,7 @@ jobs:
path: windows/windows_setup.iss
options: /O+
env:
FLADDER_VERSION: ${{needs.fetch-info.outputs.version_name}}
FLADDER_VERSION: ${{ needs.fetch-info.outputs.version_name }}
- name: Archive Windows portable artifact
uses: actions/upload-artifact@v4.0.0
@ -164,10 +250,10 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Flutter
uses: subosito/flutter-action@v2.16.0
uses: subosito/flutter-action@v2.19.0
with:
channel: "stable"
flutter-version: ${{needs.fetch-info.outputs.flutter-version}}
flutter-version: ${{needs.fetch-info.outputs.flutter_version}}
cache: true
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
@ -184,7 +270,6 @@ jobs:
mkdir Payload
mv Runner.app Payload/
zip -r iOS.ipa Payload
- name: Archive iOS IPA artifact
uses: actions/upload-artifact@v4.0.0
with:
@ -200,10 +285,10 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Flutter
uses: subosito/flutter-action@v2.16.0
uses: subosito/flutter-action@v2.19.0
with:
channel: "stable"
flutter-version: ${{needs.fetch-info.outputs.flutter-version}}
flutter-version: ${{needs.fetch-info.outputs.flutter_version}}
cache: true
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
@ -214,8 +299,16 @@ jobs:
- name: Build macOS app
run: flutter build macos --flavor production --build-number=${{ github.run_number }}
- name: Create DMG file
run: hdiutil create -format UDZO -srcfolder build/macos/Build/Products/Release-production/fladder.app build/macos/Build/Products/Release-production/macOS.dmg
- name: Ensure correct app name casing
run: |
APP_DIR="build/macos/Build/Products/Release-production"
mv "$APP_DIR/fladder.app" "$APP_DIR/Fladder.app"
- name: Install create-dmg
run: brew install create-dmg
- name: Create DMG with custom background
run: ./scripts/create_dmg.sh
- name: Archive macOS artifact
uses: actions/upload-artifact@v4.0.0
@ -232,10 +325,10 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Flutter
uses: subosito/flutter-action@v2.16.0
uses: subosito/flutter-action@v2.19.0
with:
channel: "stable"
flutter-version: ${{needs.fetch-info.outputs.flutter-version}}
flutter-version: ${{needs.fetch-info.outputs.flutter_version}}
cache: true
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
@ -271,7 +364,8 @@ jobs:
- name: Build AppImage
run: |
sed -i -E 's/(version:\s*)latest/\1${{needs.fetch-info.outputs.version_name}}/' AppImageBuilder.yml
VERSION_TO_USE="${{ needs.fetch-info.outputs.version_name }}"
sed -i -E "s/(version:\\s*)latest/\\1${VERSION_TO_USE}/" AppImageBuilder.yml
wget -O appimage-builder-x86_64.AppImage https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
chmod +x appimage-builder-x86_64.AppImage
sudo mv appimage-builder-x86_64.AppImage /usr/local/bin/appimage-builder
@ -286,9 +380,8 @@ jobs:
Fladder_x86_64.AppImage.zsync
build-linux-flatpak:
name: "Flatpak"
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
if: needs.fetch-info.outputs.build_type == 'release' || needs.fetch-info.outputs.build_type == 'nightly'
needs: [fetch-info, build-linux]
container:
image: bilelmoussaoui/flatpak-github-actions:gnome-46
@ -325,10 +418,10 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Set up Flutter
uses: subosito/flutter-action@v2.16.0
uses: subosito/flutter-action@v2.19.0
with:
channel: "stable"
flutter-version: ${{needs.fetch-info.outputs.flutter-version}}
flutter-version: ${{needs.fetch-info.outputs.flutter_version}}
cache: true
cache-key: "flutter-:os:-:channel:-:version:-:arch:-:hash:"
cache-path: "${{ runner.tool_cache }}/flutter/:channel:-:version:-:arch:"
@ -347,10 +440,11 @@ jobs:
path: build/web
- name: Build Github pages web
if: startsWith(github.ref, 'refs/tags/v')
if: needs.fetch-info.outputs.build_type == 'release'
run: flutter build web --base-href /${{ github.event.repository.name }}/ --release --build-number=$GITHUB_RUN_NUMBER
- name: Archive web pages artifact
if: needs.fetch-info.outputs.build_type == 'release'
uses: actions/upload-artifact@v4.0.0
with:
name: fladder-web-pages
@ -368,8 +462,93 @@ jobs:
- build-linux-flatpak
- build-web
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
if: needs.fetch-info.outputs.build_type == 'release' || needs.fetch-info.outputs.build_type == 'nightly'
permissions:
contents: write
steps:
- name: Checkout repository
if: needs.fetch-info.outputs.build_type == 'nightly'
uses: actions/checkout@v4.1.1
with:
fetch-depth: 0
- name: Set version variables
id: version
run: |
if [[ "${{ needs.fetch-info.outputs.build_type }}" == "nightly" ]]; then
echo "version=${{ needs.fetch-info.outputs.nightly_version }}" >> $GITHUB_OUTPUT
else
echo "version=${{ needs.fetch-info.outputs.version_name }}" >> $GITHUB_OUTPUT
fi
- name: Delete existing nightly release
if: needs.fetch-info.outputs.build_type == 'nightly'
run: |
# Delete existing nightly release and tag with better error handling
echo "Checking for existing nightly release..."
if gh release view ${{ env.NIGHTLY_TAG }} >/dev/null 2>&1; then
echo "Deleting existing nightly release..."
gh release delete ${{ env.NIGHTLY_TAG }} --yes --cleanup-tag
else
echo "No existing nightly release found."
fi
# Clean up any orphaned tags
if git tag -l | grep -q "^${{ env.NIGHTLY_TAG }}$"; then
echo "Deleting orphaned nightly tag..."
git push origin --delete ${{ env.NIGHTLY_TAG }} || true
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate changelog
if: needs.fetch-info.outputs.build_type == 'nightly'
id: changelog
run: |
# Get the latest stable release tag for comparison
LATEST_STABLE_RELEASE=$(gh release list --limit 50 --exclude-drafts --json tagName,isPrerelease --jq '.[] | select(.isPrerelease == false) | .tagName' | head -1 || echo "")
# Create new changelog with comparison link
cat > changelog.md <<-EOF
# $(date -u "+%Y-%m-%d %H:%M:%S UTC")
This is an automated nightly build containing the latest changes from the develop branch.
**⚠️ Warning:** This is a development build and may contain bugs or incomplete features.
EOF
# Add comparison link if we have a latest stable release
if [ -n "$LATEST_STABLE_RELEASE" ]; then
cat >> changelog.md <<-EOF
## 📋 What's Changed
See all changes since the latest release: [Compare $LATEST_STABLE_RELEASE...HEAD](https://github.com/${{ github.repository }}/compare/$LATEST_STABLE_RELEASE...HEAD)
EOF
else
cat >> changelog.md <<-EOF
## 📋 What's Changed
See all changes: [View commits](https://github.com/${{ github.repository }}/commits/develop)
EOF
fi
# Add build metadata
cat >> changelog.md <<-EOF
---
📅 **Build Date:** $(date -u "+%Y-%m-%d %H:%M:%S UTC")
🔧 **Build Number:** ${{ github.run_number }}
📝 **Commit:** ${{ github.sha }}
🌿 **Branch:** develop
EOF
echo "changelog_file=changelog.md" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Download Artifacts Android
uses: actions/download-artifact@v4
with:
@ -378,8 +557,8 @@ jobs:
- name: Move Android
run: |
mv fladder-android/release-signed.apk Fladder-Android-${{needs.fetch-info.outputs.version_name}}.apk
mv fladder-android/release-signed.aab Fladder-Android-${{needs.fetch-info.outputs.version_name}}.aab
mv fladder-android/${{ needs.build-android.outputs.artifact_suffix }}.apk Fladder-Android-${{ steps.version.outputs.version }}.apk
mv fladder-android/${{ needs.build-android.outputs.artifact_suffix }}.aab Fladder-Android-${{ steps.version.outputs.version }}.aab
- name: Download Windows portable artifact
uses: actions/download-artifact@v4
@ -390,7 +569,7 @@ jobs:
- name: Compress Windows
run: |
cd fladder-windows-portable
zip -r ../Fladder-Windows-${{needs.fetch-info.outputs.version_name}}.zip .
zip -r ../Fladder-Windows-${{ steps.version.outputs.version }}.zip .
- name: Download Windows installer artifact
uses: actions/download-artifact@v4
@ -399,7 +578,7 @@ jobs:
path: fladder-windows-installer
- name: Rename Windows installer
run: mv fladder-windows-installer/fladder_setup.exe Fladder-Windows-${{needs.fetch-info.outputs.version_name}}-Setup.exe
run: mv fladder-windows-installer/fladder_setup.exe Fladder-Windows-${{ steps.version.outputs.version }}-Setup.exe
- name: Download Artifacts iOS
uses: actions/download-artifact@v4
@ -408,7 +587,7 @@ jobs:
path: fladder-iOS
- name: Move iOS
run: mv fladder-iOS/iOS.ipa Fladder-iOS-${{needs.fetch-info.outputs.version_name}}.ipa
run: mv fladder-iOS/iOS.ipa Fladder-iOS-${{ steps.version.outputs.version }}.ipa
- name: Download Artifacts macOS
uses: actions/download-artifact@v4
@ -417,7 +596,7 @@ jobs:
path: fladder-macOS
- name: Move macOS
run: mv fladder-macOS/macOS.dmg Fladder-macOS-${{needs.fetch-info.outputs.version_name}}.dmg
run: mv fladder-macOS/macOS.dmg Fladder-macOS-${{ steps.version.outputs.version }}.dmg
- name: Download Artifacts Linux
uses: actions/download-artifact@v4
@ -428,16 +607,18 @@ jobs:
- name: Compress Linux
run: |
cd fladder-linux
zip -r ../Fladder-Linux-${{needs.fetch-info.outputs.version_name}}.zip .
zip -r ../Fladder-Linux-${{ steps.version.outputs.version }}.zip .
- name: Download Artifacts Linux Flatpak
if: needs.fetch-info.outputs.build_type == 'release'
uses: actions/download-artifact@v4
with:
name: fladder-linux-flatpak
path: fladder-linux-flatpak
- name: Move Linux Flatpak
run: mv fladder-linux-flatpak/Fladder-Linux.flatpak Fladder-Linux-${{needs.fetch-info.outputs.version_name}}.flatpak
if: needs.fetch-info.outputs.build_type == 'release'
run: mv fladder-linux-flatpak/Fladder-Linux.flatpak Fladder-Linux-${{ steps.version.outputs.version }}.flatpak
- name: Download Artifacts Linux AppImage
uses: actions/download-artifact@v4
@ -445,11 +626,10 @@ jobs:
name: fladder-linux-appimage
path: fladder-linux-appimage
- name: Move linux AppImages
- name: Move Linux AppImages
run: |
mv fladder-linux-appimage/Fladder_x86_64.AppImage Fladder-Linux-${{needs.fetch-info.outputs.version_name}}.AppImage
mv fladder-linux-appimage/Fladder_x86_64.AppImage.zsync Fladder-Linux-${{needs.fetch-info.outputs.version_name}}.AppImage.zsync
mv fladder-linux-appimage/Fladder_x86_64.AppImage Fladder-Linux-${{ steps.version.outputs.version }}.AppImage
mv fladder-linux-appimage/Fladder_x86_64.AppImage.zsync Fladder-Linux-${{ steps.version.outputs.version }}.AppImage.zsync
- name: Download Artifacts Web
uses: actions/download-artifact@v4
with:
@ -459,32 +639,50 @@ jobs:
- name: Compress Web
run: |
cd fladder-web
zip -r ../Fladder-Web-${{needs.fetch-info.outputs.version_name}}.zip .
zip -r ../Fladder-Web-${{ steps.version.outputs.version }}.zip .
- name: Release
- name: Create Release
uses: softprops/action-gh-release@v2.2.2
with:
tag_name: ${{ needs.fetch-info.outputs.build_type == 'nightly' && env.NIGHTLY_TAG || github.ref_name }}
name: ${{ needs.fetch-info.outputs.build_type == 'nightly' && format('🌙 Nightly Build - {0}', steps.version.outputs.version) || '' }}
body_path: ${{ needs.fetch-info.outputs.build_type == 'nightly' && 'changelog.md' || '' }}
draft: ${{ needs.fetch-info.outputs.build_type == 'release' }}
prerelease: ${{ needs.fetch-info.outputs.build_type == 'nightly' }}
make_latest: ${{ needs.fetch-info.outputs.build_type == 'release' }}
generate_release_notes: ${{ needs.fetch-info.outputs.build_type == 'release' }}
fail_on_unmatched_files: true
files: |
Fladder-Android-${{ steps.version.outputs.version }}.apk
Fladder-Android-${{ steps.version.outputs.version }}.aab
Fladder-Windows-${{ steps.version.outputs.version }}-Setup.exe
Fladder-Windows-${{ steps.version.outputs.version }}.zip
Fladder-iOS-${{ steps.version.outputs.version }}.ipa
Fladder-macOS-${{ steps.version.outputs.version }}.dmg
Fladder-Web-${{ steps.version.outputs.version }}.zip
Fladder-Linux-${{ steps.version.outputs.version }}.zip
Fladder-Linux-${{ steps.version.outputs.version }}.AppImage
Fladder-Linux-${{ steps.version.outputs.version }}.AppImage.zsync
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Add Flatpak to release
if: needs.fetch-info.outputs.build_type == 'release'
uses: softprops/action-gh-release@v2
with:
draft: true
fail_on_unmatched_files: true
generate_release_notes: true
tag_name: ${{ github.ref_name }}
files: |
Fladder-Android-${{needs.fetch-info.outputs.version_name}}.apk
Fladder-Windows-${{needs.fetch-info.outputs.version_name}}-Setup.exe
Fladder-Windows-${{needs.fetch-info.outputs.version_name}}.zip
Fladder-iOS-${{needs.fetch-info.outputs.version_name}}.ipa
Fladder-macOS-${{needs.fetch-info.outputs.version_name}}.dmg
Fladder-Web-${{needs.fetch-info.outputs.version_name}}.zip
Fladder-Linux-${{needs.fetch-info.outputs.version_name}}.zip
Fladder-Linux-${{needs.fetch-info.outputs.version_name}}.flatpak
Fladder-Linux-${{needs.fetch-info.outputs.version_name}}.AppImage
Fladder-Linux-${{needs.fetch-info.outputs.version_name}}.AppImage.zsync
Fladder-Linux-${{ steps.version.outputs.version }}.flatpak
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
release_web:
name: Release Web
needs:
- fetch-info
- create_release
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
if: needs.fetch-info.outputs.build_type == 'release'
steps:
- name: Checkout repository
uses: actions/checkout@v4.1.1

5
.gitignore vendored
View file

@ -5,6 +5,8 @@
*.swp
.DS_Store
.atom/
.build/
.dmg_temp/
.buildlog/
.history
.svn/
@ -73,3 +75,6 @@ local.properties
# Inno Setup builds
Output
# Generated localizations
lib/l10n/generated

19
.vscode/launch.json vendored
View file

@ -10,7 +10,18 @@
"type": "dart",
"args": [
"--flavor",
"development"
"development",
]
},
{
"name": "Fladder Development HTPC (debug)",
"request": "launch",
"type": "dart",
"args": [
"--flavor",
"development",
"-a",
"--htpc",
]
},
{
@ -69,7 +80,8 @@
"deviceId": "chrome",
"args": [
"--web-port",
"9090"
"9090",
"--web-experimental-hot-reload"
],
},
{
@ -80,7 +92,8 @@
"flutterMode": "release",
"args": [
"--web-port",
"9090"
"9090",
"--web-experimental-hot-reload"
],
},
],

12
.vscode/settings.json vendored
View file

@ -7,7 +7,7 @@
"LTWH",
"outro"
],
"dart.flutterSdkPath": ".fvm/versions/3.29.2",
"dart.flutterSdkPath": ".fvm/versions/3.32.1",
"search.exclude": {
"**/.fvm": true
},
@ -15,5 +15,13 @@
"**/.fvm": true
},
"editor.detectIndentation": true,
"dart.lineLength": 120
"dart.lineLength": 120,
"dart.analysisExcludedFolders": [
"build",
".dart_tool",
".fvm",
"packages",
"lib/jellyfin",
"lib/l10n"
]
}

65
DEVELOPEMENT.md Normal file
View file

@ -0,0 +1,65 @@
# 🚀 Fladder Dev Setup
## 🔧 Requirements
Ensure the following tools are installed:
- [Flutter SDK](https://flutter.dev/docs/get-started/install) (latest stable)
- [Android Studio](https://developer.android.com/studio) (for Android development and emulators)
- [VS Code](https://code.visualstudio.com/) with:
- Flutter extension
- Dart extension
Verify your Flutter setup with:
```bash
flutter doctor
```
## 🚀 Quick Start
```bash
# Clone the repository
git clone https://github.com/DonutWare/Fladder.git
cd Fladder
# Install dependencies
flutter pub get
```
## 🐧 Linux Dependencies
If you're on **Linux**, install the `mpv` dependency:
```bash
sudo apt install libmpv-dev
```
## 🛠️ Running the App
1. **Connect a device** or launch an emulator.
2. In VS Code:
- Select the target device (bottom right corner).
- Press `F5` or go to **Run > Start Debugging**.
- If prompted, select **"Run Anyway"**.
## ⚙️ Code Generation
Generate build files (e.g., for `json_serializable`, `freezed`, etc.):
```bash
flutter pub run build_runner build
```
> Tip: Use `watch` for continuous builds during development:
```bash
flutter pub run build_runner watch
```
Update localization definitions:
```bash
flutter gen-l10n
```
## 🌐 Using a demo Server
You can use a fake server from Jellyfin.
https://demo.jellyfin.org/stable/web/

View file

@ -16,6 +16,7 @@ Platform-specific installation instructions can be found in this document.
- [iOS](#iosipados)
- [Sideloadly](#sideloadly)
- [Docker](#docker)
- [Web](#web)
## Windows
@ -140,3 +141,10 @@ Run `docker-compose up -d` to start the container. It will be available on `http
> [!TIP]
> We recommend changing the `BASE_URL` environment variable to the URL you use to access Jellyfin, as this will skip entering it when you load the web UI.
## Web
You can also manually copy the web .zip build to any static file server such as Nginx, Caddy, or Apache
> [!TIP]
> You can preconfigure Fladder by placing a config file in [assets/config/config.json](https://github.com/DonutWare/Fladder/blob/develop/config/config.json)

View file

@ -53,11 +53,14 @@
<details close>
<summary>Mobile</summary>
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Dashboard.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Details.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Details_2.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Favourites.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Library.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Library_Search.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Resume_Tab.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Sync.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Settings.png?raw=true" alt="Fladder" width="200">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Mobile/Player.png?raw=true" alt="Fladder" width="1280">
</details>
@ -65,8 +68,14 @@
<summary>Tablet</summary>
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Dashboard.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Details.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Details_2.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Favourites.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Library.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Library_Search.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Resume_Tab.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Sync.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Settings.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Sync.png?raw=true" alt="Fladder" width="1280">
<img src="https://github.com/DonutWare/Fladder/blob/develop/assets/marketing/screenshots/Tablet/Player.png?raw=true" alt="Fladder" width="1280">
</details>
Web/Desktop [try out the web build!](https://DonutWare.github.io/Fladder)

View file

@ -3,12 +3,15 @@ include: package:lints/recommended.yaml
analyzer:
exclude:
- build/**
- .dart_tool/**
- .fvm/**
- pubspec.yaml
- packages/**
- lib/jellyfin/**
- lib/**/**.g.dart
- lib/**/**.freezed.dart
- lib/**/**.mapped.dart
- packages/**
- pubspec.yaml
- lib/l10n/**
strong-mode:
implicit-casts: false
implicit-dynamic: false

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 732 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 731 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 895 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 996 KiB

After

Width:  |  Height:  |  Size: 1.8 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 930 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 4.3 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 3.3 MiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 244 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 187 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before After
Before After

View file

@ -27,6 +27,9 @@ targets:
- "**/**_page.dart"
freezed|freezed:
options:
copy_with: false
equal: false
tostring:
generate_for:
- "**/**.f.dart"
- "**/**.g.dart"
@ -34,7 +37,7 @@ targets:
options:
ignoreNull: true
discriminatorKey: type
generateMethods: [decode, encode, copy, stringify]
generateMethods: [copy]
chopper_generator:
options:
header: "//Generated jellyfin api code"

View file

@ -1,9 +1,18 @@
id: nl.jknaapen.fladder
runtime: org.gnome.Platform
runtime-version: '46'
runtime-version: '48'
sdk: org.gnome.Sdk
command: Fladder
add-extensions:
org.freedesktop.Platform.ffmpeg-full:
directory: lib/ffmpeg
version: '24.08'
add-ld-path: .
cleanup-commands:
- mkdir -p /app/lib/ffmpeg
finish-args:
# X11/Wayland access
- --share=ipc
@ -15,6 +24,8 @@ finish-args:
- --share=network
# File access for downloads/media
- --filesystem=home
# File access for downloads temporary directory
- --filesystem=/tmp
# Allow access to PipeWire socket for mpv
- --filesystem=xdg-run/pipewire-0:ro
# Hardware acceleration
@ -61,42 +72,12 @@ modules:
sources:
- type: git
url: https://github.com/FFmpeg/nv-codec-headers.git
tag: n12.2.72.0
commit: c69278340ab1d5559c7d7bf0edf615dc33ddbba7
tag: n13.0.19.0
commit: e844e5b26f46bb77479f063029595293aa8f812d
x-checker-data:
type: git
tag-pattern: ^n([\d.]+)$
- name: ffmpeg
config-opts:
- --enable-shared
- --disable-static
- --enable-gnutls
- --enable-pic
- --disable-doc
- --disable-programs
- --disable-encoders
- --disable-muxers
- --disable-devices
- --enable-vaapi
- --enable-cuvid
- --enable-libdav1d
- --enable-gpl
cleanup:
- /lib/pkgconfig
- /share
- /include
sources:
- type: git
url: https://github.com/FFmpeg/FFmpeg.git
tag: n7.1
commit: b08d7969c550a804a59511c7b83f2dd8cc0499b8
x-checker-data:
type: git
tag-pattern: ^n([\d.]+)$
- shared-modules/luajit/luajit.json
- name: libass
config-opts:
- --enable-shared
@ -113,7 +94,6 @@ modules:
x-checker-data:
type: git
tag-pattern: ^([\d.]+)$
- name: uchardet
buildsystem: cmake-ninja
config-opts:
@ -134,15 +114,12 @@ modules:
url: https://www.freedesktop.org/software/uchardet/releases/
version-pattern: uchardet-(\d+\.\d+\.\d+)\.tar\.xz
url-template: https://www.freedesktop.org/software/uchardet/releases/uchardet-$version.tar.xz
- name: libplacebo
buildsystem: meson
config-opts:
- -Dvulkan=enabled
- -Dshaderc=enabled
cleanup:
- /include
- /lib/pkgconfig
- --libdir=lib
sources:
- type: git
url: https://code.videolan.org/videolan/libplacebo.git
@ -151,49 +128,6 @@ modules:
x-checker-data:
type: git
tag-pattern: ^v([\d.]+)$
modules:
- name: shaderc
buildsystem: cmake-ninja
builddir: true
config-opts:
- -DSHADERC_SKIP_COPYRIGHT_CHECK=ON
- -DSHADERC_SKIP_EXAMPLES=ON
- -DSHADERC_SKIP_TESTS=ON
- -DSPIRV_SKIP_EXECUTABLES=ON
- -DENABLE_GLSLANG_BINARIES=OFF
- -DCMAKE_BUILD_TYPE=Release
cleanup:
- /bin
- /include
- /lib/*.a
- /lib/cmake
- /lib/pkgconfig
sources:
- type: git
url: https://github.com/google/shaderc.git
tag: v2024.2
commit: 3ac03b8ad85a8e328a6182cddee8d05810bd5a2c
x-checker-data:
type: git
tag-pattern: ^v([\d.]+)$
- type: git
url: https://github.com/KhronosGroup/SPIRV-Tools.git
tag: v2023.2
commit: 44d72a9b36702f093dd20815561a56778b2d181e
dest: third_party/spirv-tools
- type: git
url: https://github.com/KhronosGroup/SPIRV-Headers.git
tag: sdk-1.3.250.1
commit: 268a061764ee69f09a477a695bf6a11ffe311b8d
dest: third_party/spirv-headers
- type: git
url: https://github.com/KhronosGroup/glslang.git
tag: 14.3.0
commit: fa9c3deb49e035a8abcabe366f26aac010f6cbfb
dest: third_party/glslang
x-checker-data:
type: git
tag-pattern: ^([\d.]+)$
- name: zenity
buildsystem: meson
@ -215,4 +149,4 @@ modules:
- install -Dm644 icons/fladder_icon.svg /app/share/icons/hicolor/scalable/apps/${FLATPAK_ID}.svg
sources:
- type: dir
path: ..
path: ..

View file

@ -2,3 +2,5 @@ arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
nullable-getter: false
synthetic-package: false
output-dir: lib/l10n/generated

View file

@ -1284,8 +1284,62 @@
}
}
},
"settingsPlayerBufferSizeTitle": "حجم ذاكرة التخزين المؤقت للفيديو",
"settingsPlayerBufferSizeTitle": "حجم التخزين المؤقت للفيديو",
"@settingsPlayerBufferSizeTitle": {},
"settingsPlayerBufferSizeDesc": "قم بتهيئة حجم ذاكرة التخزين المؤقت لتشغيل الفيديو، مما يحدد كمية البيانات التي يتم تحميلها في الذاكرة المؤقتة.",
"@settingsPlayerBufferSizeDesc": {}
"@settingsPlayerBufferSizeDesc": {},
"maxConcurrentDownloadsTitle": "الحد الأقصى للتنزيلات المتزامنة",
"@maxConcurrentDownloadsTitle": {},
"maxConcurrentDownloadsDesc": "يحدد العدد الأقصى للتنزيلات التي يمكن أن تتم في نفس الوقت. اضبطه على 0 لتعطيل الحد.",
"@maxConcurrentDownloadsDesc": {},
"playbackTrackSelection": "اختيار مسار التشغيل",
"@playbackTrackSelection": {},
"rememberSubtitleSelections": "تذكر اختيارات الترجمة بناءً على العنصر السابق",
"@rememberSubtitleSelections": {},
"rememberAudioSelections": "تذكر اختيارات الصوت بناءً على العنصر السابق",
"@rememberAudioSelections": {},
"rememberAudioSelectionsDesc": "حاول ضبط مسار الصوت ليكون الأقرب تطابقاً مع الفيديو الأخير.",
"@rememberAudioSelectionsDesc": {},
"rememberSubtitleSelectionsDesc": "حاول ضبط مسار الترجمة ليكون الأقرب تطابقاً مع الفيديو الأخير.",
"@rememberSubtitleSelectionsDesc": {},
"exitFladderTitle": "خروج فلادر",
"@exitFladderTitle": {},
"similarToLikedItem": "مشابه للعنصر المفضل",
"@similarToLikedItem": {},
"hasActorFromRecentlyPlayed": "يحتوي على ممثل من الأفلام المشغولة مؤخراً",
"@hasActorFromRecentlyPlayed": {},
"hasLikedActor": "يحتوي على ممثل مفضل",
"@hasLikedActor": {},
"hasDirectorFromRecentlyPlayed": "يحتوي على مخرج من الأفلام المشغولة مؤخراً",
"@hasDirectorFromRecentlyPlayed": {},
"hasLikedDirector": "يحتوي على مخرج مفضل",
"@hasLikedDirector": {},
"playbackTypeDirect": "مباشر",
"@playbackTypeDirect": {},
"latestReleases": "أحدث الإصدارات",
"@latestReleases": {},
"autoCheckForUpdates": "التحقق التلقائي من التحديثات",
"@autoCheckForUpdates": {},
"newUpdateFoundOnGithub": "تم اكتشاف تحديث جديد على Github",
"@newUpdateFoundOnGithub": {},
"similarToRecentlyPlayed": "مشابه لما تم تشغيله مؤخراً",
"@similarToRecentlyPlayed": {},
"playbackTypeOffline": "في وضع عدم الاتصال",
"@playbackTypeOffline": {},
"latest": "الأحدث",
"@latest": {},
"playbackTypeTranscode": "تحويل الصيغة",
"@playbackTypeTranscode": {},
"newReleaseFoundTitle": "تحديث {newRelease} متاح الآن!",
"@newReleaseFoundTitle": {
"placeholders": {
"newRelease": {
"type": "String"
}
}
},
"recommended": "الموصى به",
"@recommended": {},
"playbackType": "نوع التشغيل",
"@playbackType": {}
}

1345
lib/l10n/app_cs.arb Normal file

File diff suppressed because it is too large Load diff

View file

@ -1290,5 +1290,69 @@
"example": "1"
}
}
}
},
"settingsPlayerBufferSizeTitle": "Videopuffergröße",
"@settingsPlayerBufferSizeTitle": {},
"settingsPlayerBufferSizeDesc": "Konfigurieren Sie die Puffergröße für die Videowiedergabe und legen Sie fest, wie viele Daten in den Cache geladen werden.",
"@settingsPlayerBufferSizeDesc": {},
"exitFladderTitle": "Fladder Beenden",
"@exitFladderTitle": {},
"maxConcurrentDownloadsTitle": "Maximale Anzahl gleichzeitiger Downloads",
"@maxConcurrentDownloadsTitle": {},
"maxConcurrentDownloadsDesc": "Legt die maximale Anzahl gleichzeitig laufender Downloads fest. Setzen Sie den Wert auf 0, um die Begrenzung zu deaktivieren.",
"@maxConcurrentDownloadsDesc": {},
"rememberSubtitleSelections": "Untertitelspur basierend auf vorherigem Element einstellen",
"@rememberSubtitleSelections": {},
"rememberAudioSelections": "Audiospur basierend auf vorherigem Element einstellen",
"@rememberAudioSelections": {},
"similarToRecentlyPlayed": "Ähnlich wie kürzlich gespielt",
"@similarToRecentlyPlayed": {},
"similarToLikedItem": "Ähnlich wie der gewünschte Artikel",
"@similarToLikedItem": {},
"hasActorFromRecentlyPlayed": "Hat Schauspieler aus vor kurzem gespielt",
"@hasActorFromRecentlyPlayed": {},
"hasLikedActor": "Hat Schauspieler gemocht",
"@hasLikedActor": {},
"latest": "Letzte",
"@latest": {},
"recommended": "Empfohlen",
"@recommended": {},
"hasLikedDirector": "Hat Regisseur gemocht",
"@hasLikedDirector": {},
"hasDirectorFromRecentlyPlayed": "Hat Regisseur von vor kurzem gespielt",
"@hasDirectorFromRecentlyPlayed": {},
"playbackType": "Wiedergabetyp",
"@playbackType": {},
"playbackTypeDirect": "Direkt",
"@playbackTypeDirect": {},
"playbackTypeTranscode": "Transkodieren",
"@playbackTypeTranscode": {},
"playbackTypeOffline": "Offline",
"@playbackTypeOffline": {},
"latestReleases": "Aktuelle Veröffentlichungen",
"@latestReleases": {},
"newReleaseFoundTitle": "Update {newRelease} verfügbar!",
"@newReleaseFoundTitle": {
"placeholders": {
"newRelease": {
"type": "String"
}
}
},
"autoCheckForUpdates": "Regelmäßig nach Updates suchen",
"@autoCheckForUpdates": {},
"newUpdateFoundOnGithub": "Habe ein neues Update auf Github gefunden",
"@newUpdateFoundOnGithub": {},
"rememberSubtitleSelectionsDesc": "Versuchen Sie, die Untertitelspur so einzustellen, dass sie dem letzten Video am nächsten kommt.",
"@rememberSubtitleSelectionsDesc": {},
"rememberAudioSelectionsDesc": "Versuchen Sie, die Audiospur so einzustellen, dass sie dem letzten Video am nächsten kommt.",
"@rememberAudioSelectionsDesc": {},
"playbackTrackSelection": "Auswahl der Wiedergabespur",
"@playbackTrackSelection": {},
"enableBackgroundPostersDesc": "Zeigen Sie zufällige Poster in den entsprechenden Bildschirmen",
"@enableBackgroundPostersDesc": {},
"settingsEnableOsMediaControlsDesc": "Ermöglicht die Wiedergabesteuerung mithilfe von Medientasten und zeigt die aktuell wiedergegebenen Medien im Betriebssystem an",
"@settingsEnableOsMediaControlsDesc": {},
"enableBackgroundPostersTitle": "Hintergrundbilder aktivieren",
"@enableBackgroundPostersTitle": {}
}

View file

@ -721,6 +721,7 @@
"@settingsContinue": {},
"settingsEnableOsMediaControls": "Enable OS media controls",
"@settingsEnableOsMediaControls": {},
"settingsEnableOsMediaControlsDesc": "Allow for playback control using media-keys and show current playing media in OS",
"settingsHomeBannerDescription": "Display as a slideshow, carousel, or hide the banner",
"@settingsHomeBannerDescription": {},
"settingsHomeBannerTitle": "Home banner",
@ -1189,6 +1190,7 @@
"segmentActionAskToSkip": "Ask to skip",
"segmentActionSkip": "Skip",
"loading": "Loading",
"exitFladderTitle": "Exit Fladder",
"castAndCrew": "Cast & Crew",
"guestActor": "{count, plural, other{Guest Actors} one{Guest Actor}}",
"@guestActor": {
@ -1199,5 +1201,42 @@
"example": "1"
}
}
}
},
"maxConcurrentDownloadsTitle": "Max concurrent downloads",
"maxConcurrentDownloadsDesc": "Sets the maximum number of downloads that can run at the same time. Set to 0 to disable the limit.",
"playbackTrackSelection": "Playback track selection",
"@playbackTrackSelection": {},
"rememberSubtitleSelections": "Set subtitle track based on previous item",
"@rememberSubtitleSelections": {},
"rememberAudioSelections": "Set audio track based on previous item",
"@rememberAudioSelections": {},
"rememberSubtitleSelectionsDesc": "Try to set the subtitle track to the closest match to the last video.",
"@rememberSubtitleSelectionsDesc": {},
"rememberAudioSelectionsDesc": "Try to set the audio track to the closest match to the last video.",
"@rememberAudioSelectionsDesc": {},
"similarToRecentlyPlayed": "Similar to recently played",
"similarToLikedItem": "Similar to liked item",
"hasDirectorFromRecentlyPlayed": "Has director from recently played",
"hasActorFromRecentlyPlayed": "Has actor from recently played",
"hasLikedDirector": "Has liked director",
"hasLikedActor": "Has liked actor",
"latest": "Latest",
"recommended": "Recommended",
"playbackType": "Playback type",
"playbackTypeDirect": "Direct",
"playbackTypeTranscode": "Transcode",
"playbackTypeOffline": "Offline",
"latestReleases": "Latest releases",
"autoCheckForUpdates": "Periodically check for updates",
"newReleaseFoundTitle": "Update {newRelease} available!",
"@newReleaseFoundTitle": {
"placeholders": {
"newRelease": {
"type": "String"
}
}
},
"newUpdateFoundOnGithub": "Found a new update on Github",
"enableBackgroundPostersTitle": "Enable background posters",
"enableBackgroundPostersDesc": "Show random posters in applicable screens"
}

View file

@ -37,5 +37,33 @@
"appLockPasscode": "Codice",
"@appLockPasscode": {},
"audio": "Audio",
"@audio": {}
"@audio": {},
"amoledBlack": "Amoled nero",
"@amoledBlack": {},
"backgroundBlur": "Sfocatura dello sfondo",
"@backgroundBlur": {},
"backgroundOpacity": "Opacità dello sfondo",
"@backgroundOpacity": {},
"biometricsFailedCheckAgain": "Errore biometrico. Controlla le impostazioni e riprova.",
"@biometricsFailedCheckAgain": {},
"bold": "Grassetto",
"@bold": {},
"cancel": "Cancellare",
"@cancel": {},
"change": "Modifica",
"@change": {},
"clear": "Leggere",
"@clear": {},
"clearAllSettings": "Cancella tutte le impostazioni",
"@clearAllSettings": {},
"clearAllSettingsQuestion": "Cancellare tutte le impostazioni?",
"@clearAllSettingsQuestion": {},
"clearChanges": "Cancella modifiche",
"@clearChanges": {},
"clearSelection": "Cancella selezione",
"@clearSelection": {},
"close": "Vicina",
"@close": {},
"code": "Codice",
"@code": {}
}

1082
lib/l10n/app_mr.arb Normal file

File diff suppressed because it is too large Load diff

1265
lib/l10n/app_mt.arb Normal file

File diff suppressed because it is too large Load diff

View file

@ -1284,5 +1284,63 @@
}
},
"castAndCrew": "Cast en crew",
"@castAndCrew": {}
"@castAndCrew": {},
"settingsPlayerBufferSizeTitle": "Video buffer grootte",
"@settingsPlayerBufferSizeTitle": {},
"settingsPlayerBufferSizeDesc": "Configureer de buffergrootte voor het afspelen van video's en bepaal de hoeveelheid vooraf geladen data.",
"@settingsPlayerBufferSizeDesc": {},
"maxConcurrentDownloadsTitle": "Maximum gelijktijdige downloads",
"@maxConcurrentDownloadsTitle": {},
"maxConcurrentDownloadsDesc": "Stelt het maximumaantal downloads in dat tegelijkertijd kan worden uitgevoerd. Stel in op 0 om de limiet uit te schakelen.",
"@maxConcurrentDownloadsDesc": {},
"rememberSubtitleSelections": "Ondertitelspoor instellen op basis van vorig item",
"@rememberSubtitleSelections": {},
"rememberSubtitleSelectionsDesc": "Probeer het ondertitelspoor in te stellen op de best overeenkomende optie van de vorige video.",
"@rememberSubtitleSelectionsDesc": {},
"playbackTrackSelection": "Selectie van afspeeltrack",
"@playbackTrackSelection": {},
"rememberAudioSelections": "Audiotrack instellen op basis van vorig item",
"@rememberAudioSelections": {},
"rememberAudioSelectionsDesc": "Probeer de audiotrack in te stellen op de best overeenkomende optie van de vorige video.",
"@rememberAudioSelectionsDesc": {},
"hasLikedActor": "Heeft dezelfde favorite acteur",
"@hasLikedActor": {},
"hasDirectorFromRecentlyPlayed": "Heeft regisseur van recent afgespeeld",
"@hasDirectorFromRecentlyPlayed": {},
"similarToLikedItem": "Vergelijkbaar met favoriet",
"@similarToLikedItem": {},
"hasActorFromRecentlyPlayed": "Heeft acteur van recent afgespeeld",
"@hasActorFromRecentlyPlayed": {},
"hasLikedDirector": "Heeft dezelfde favoriete regisseur",
"@hasLikedDirector": {},
"recommended": "Aanbevolen",
"@recommended": {},
"similarToRecentlyPlayed": "Vergelijkbaar met recent afgespeeld",
"@similarToRecentlyPlayed": {},
"latest": "Laatste",
"@latest": {},
"exitFladderTitle": "Fladder sluiten",
"@exitFladderTitle": {},
"playbackTypeDirect": "Direct",
"@playbackTypeDirect": {},
"playbackTypeOffline": "Offline",
"@playbackTypeOffline": {},
"playbackType": "Afspeel type",
"@playbackType": {},
"playbackTypeTranscode": "Transcoderen",
"@playbackTypeTranscode": {},
"newUpdateFoundOnGithub": "Nieuwe update gevonden op Github",
"@newUpdateFoundOnGithub": {},
"autoCheckForUpdates": "Regelmatig controleren op updates",
"@autoCheckForUpdates": {},
"newReleaseFoundTitle": "Update {newRelease} beschikbaar!",
"@newReleaseFoundTitle": {
"placeholders": {
"newRelease": {
"type": "String"
}
}
},
"latestReleases": "Nieuwste releases",
"@latestReleases": {}
}

View file

@ -1153,5 +1153,37 @@
"episodeUnaired": "Niewyemitowany",
"@episodeUnaired": {},
"episodeMissing": "Brakujący",
"@episodeMissing": {}
"@episodeMissing": {},
"settingsPlayerBufferSizeDesc": "Konfiguruje rozmiar bufora dla odtwarzania wideo, określając jak wiele danych jest załadowanych w pamięci cache.",
"@settingsPlayerBufferSizeDesc": {},
"settingsPlayerBufferSizeTitle": "Wielkość bufora wideo",
"@settingsPlayerBufferSizeTitle": {},
"refreshPopupContentMetadata": "Metadane są odświeżane na podstawie ustawień i usług internetowych włączonych w panelu administracyjnym.",
"@refreshPopupContentMetadata": {},
"libraryPageSizeDesc": "Ustawia ilość elementów do załadowania na stronie. 0 wyłącza stronicowanie.",
"@libraryPageSizeDesc": {},
"rating": "{count, plural, other{Ocen} many{Ocen} few{Oceny} one{Ocena}}",
"@rating": {
"description": "rating",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"pathEditDesc": "Ta lokalizacja jest ustawiana dla wszystkich użytkowników, żadne dane zsynchronizowane dotychczas nie będą już dostępne, ale pozostaną na urządzeniu.",
"@pathEditDesc": {},
"syncRemoveDataDesc": "Usunąć zsynchronizowane dane? Ta operacja jest nieodwracalna w związku z czym konieczna będzie ponowna synchronizacja.",
"@syncRemoveDataDesc": {},
"writer": "{count, plural, other{Scenarzytów} one{Scenarzysta}}",
"@writer": {
"description": "writer",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
}
}

View file

@ -1246,7 +1246,7 @@
"@layoutModeDual": {},
"copiedToClipboard": "Copiado para a área de transferência",
"@copiedToClipboard": {},
"internetStreamingQualityDesc": "Qualidade máxima de streaming pela internet (celular)",
"internetStreamingQualityDesc": "Qualidade máxima de streaming por dados móveis (celular)",
"@internetStreamingQualityDesc": {},
"homeStreamingQualityTitle": "Qualidade na rede local",
"@homeStreamingQualityTitle": {},
@ -1289,5 +1289,67 @@
"example": "1"
}
}
}
},
"hasLikedDirector": "Tem diretor favoritado",
"@hasLikedDirector": {},
"latest": "Mais recente",
"@latest": {},
"recommended": "Recomendado",
"@recommended": {},
"playbackType": "Tipo de reprodução",
"@playbackType": {},
"playbackTypeDirect": "Direta",
"@playbackTypeDirect": {},
"latestReleases": "Lançamentos mais recentes",
"@latestReleases": {},
"newReleaseFoundTitle": "Atualização {newRelease} disponível!",
"@newReleaseFoundTitle": {
"placeholders": {
"newRelease": {
"type": "String"
}
}
},
"newUpdateFoundOnGithub": "Nova atualização encontrada no Github",
"@newUpdateFoundOnGithub": {},
"enableBackgroundPostersDesc": "Mostra imagens de fundo aleatórias em telas aplicáveis",
"@enableBackgroundPostersDesc": {},
"hasActorFromRecentlyPlayed": "Tem ator de algo recentemente assistido",
"@hasActorFromRecentlyPlayed": {},
"enableBackgroundPostersTitle": "Ativar imagens de fundo",
"@enableBackgroundPostersTitle": {},
"settingsPlayerBufferSizeDesc": "Configure o tamanho do buffer para reprodução de vídeo, determinando a quantidade de dados que é carregada no cache.",
"@settingsPlayerBufferSizeDesc": {},
"playbackTypeOffline": "Offline",
"@playbackTypeOffline": {},
"autoCheckForUpdates": "Verificar periodicamente se há atualizações",
"@autoCheckForUpdates": {},
"hasLikedActor": "Tem ator favoritado",
"@hasLikedActor": {},
"exitFladderTitle": "Sair do Fladder",
"@exitFladderTitle": {},
"maxConcurrentDownloadsTitle": "Máximo de downloads simultâneos",
"@maxConcurrentDownloadsTitle": {},
"maxConcurrentDownloadsDesc": "Define o número máximo de downloads que podem ser executados ao mesmo tempo. Defina como 0 para desativar o limite.",
"@maxConcurrentDownloadsDesc": {},
"playbackTrackSelection": "Seleção de faixa",
"@playbackTrackSelection": {},
"rememberSubtitleSelections": "Definir a legenda com base no item anterior",
"@rememberSubtitleSelections": {},
"rememberAudioSelections": "Definir o áudio com base no item anterior",
"@rememberAudioSelections": {},
"rememberSubtitleSelectionsDesc": "Tentar definir a faixa de áudio com a correspondência mais próxima do último vídeo.",
"@rememberSubtitleSelectionsDesc": {},
"rememberAudioSelectionsDesc": "Tentar definir a legenda com a correspondência mais próxima do último vídeo.",
"@rememberAudioSelectionsDesc": {},
"similarToRecentlyPlayed": "Semelhante aos recentemente assistidos",
"@similarToRecentlyPlayed": {},
"similarToLikedItem": "Semelhante a itens curtidos",
"@similarToLikedItem": {},
"hasDirectorFromRecentlyPlayed": "Tem diretor de algo recentemente assistido",
"@hasDirectorFromRecentlyPlayed": {},
"settingsEnableOsMediaControlsDesc": "Permitir o controle da reprodução usando as teclas de mídia e mostrar a mídia atual em reprodução no OS",
"@settingsEnableOsMediaControlsDesc": {},
"settingsPlayerBufferSizeTitle": "Tamanho do buffer de vídeo",
"@settingsPlayerBufferSizeTitle": {}
}

129
lib/l10n/app_ro.arb Normal file
View file

@ -0,0 +1,129 @@
{
"code": "Cod",
"@code": {},
"chapter": "{count, plural, other{Capitole} one{Capitol}}",
"@chapter": {
"description": "chapter",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"continuePage": "Continuă - pagina {page}",
"@continuePage": {
"description": "Continue - page 1",
"placeholders": {
"page": {
"type": "int"
}
}
},
"nativeName": "Română",
"@nativeName": {},
"accept": "Acceptă",
"@accept": {},
"addAsFavorite": "Adaugă la favorite",
"@addAsFavorite": {},
"addToPlaylist": "Adaugă în lista de redare",
"@addToPlaylist": {},
"advanced": "Avansat",
"@advanced": {},
"all": "Toate",
"@all": {},
"amoledBlack": "Negru Amoled",
"@amoledBlack": {},
"appLockAutoLogin": "Conectare automată",
"@appLockAutoLogin": {},
"appLockPasscode": "Pinul de acces",
"@appLockPasscode": {},
"backgroundOpacity": "Opacitatea fundalului",
"@backgroundOpacity": {},
"bold": "Îngroșat",
"@bold": {},
"cancel": "Anulați",
"@cancel": {},
"change": "Schimbă",
"@change": {},
"clear": "",
"@clear": {},
"combined": "Combinat",
"@combined": {},
"controls": "Controale",
"@controls": {},
"dashboardContinueListening": "Continua Ascultarea",
"@dashboardContinueListening": {},
"dashboardContinueWatching": "Continua Vizionare",
"@dashboardContinueWatching": {},
"dashboardRecentlyAdded": "Adăugat recent în {name}",
"@dashboardRecentlyAdded": {
"description": "Recently added on home screen",
"placeholders": {
"name": {
"type": "String"
}
}
},
"dateAdded": "Data adăugării",
"@dateAdded": {},
"datePlayed": "Data redării",
"@datePlayed": {},
"days": "Zile",
"@days": {},
"about": "Despre",
"@about": {},
"close": "Închide",
"@close": {},
"dashboardContinueReading": "Continua Cititul",
"@dashboardContinueReading": {},
"autoPlay": "Redare automată",
"@autoPlay": {},
"active": "Activ",
"@active": {},
"appLockBiometrics": "Biometrice",
"@appLockBiometrics": {},
"actor": "{count, plural, other{Actori} one{Actor}}",
"@actor": {
"description": "actor",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"appLockTitle": "Selectează modul de conectare pentru {userName}",
"@appLockTitle": {
"description": "Pop-up to pick a login method",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"dateLastContentAdded": "Data ultimului conținut adăugat",
"@dateLastContentAdded": {},
"biometricsFailedCheckAgain": "Biometrice eșuate. Verificați setările și încercați din nou.",
"@biometricsFailedCheckAgain": {},
"communityRating": "Evaluarea comunității",
"@communityRating": {},
"dashboardContinue": "Continua",
"@dashboardContinue": {},
"dashboard": "Bord",
"@dashboard": {},
"delete": "Șterge",
"@delete": {},
"color": "Culoare",
"@color": {},
"addToCollection": "Adaugă la colecție",
"@addToCollection": {},
"audio": "Audio",
"@audio": {},
"backgroundBlur": "Blur de fundal",
"@backgroundBlur": {},
"collectionFolder": "Dosar de colectare",
"@collectionFolder": {},
"nextUp": "Următorul",
"@nextUp": {}
}

1351
lib/l10n/app_ru.arb Normal file

File diff suppressed because it is too large Load diff

137
lib/l10n/app_sk.arb Normal file
View file

@ -0,0 +1,137 @@
{
"active": "Aktívne",
"@active": {},
"amoledBlack": "Amoled čierna",
"@amoledBlack": {},
"actor": "{count, plural, other{Herci} one{Herec/Herečka}}",
"@actor": {
"description": "actor",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
},
"addAsFavorite": "Pridať ako obľúbené",
"@addAsFavorite": {},
"addToCollection": "Pridať do kolekcie",
"@addToCollection": {},
"addToPlaylist": "Pridať do playlistu",
"@addToPlaylist": {},
"advanced": "Pokročilé",
"@advanced": {},
"all": "Všetko",
"@all": {},
"appLockAutoLogin": "Automatické prihlásenie",
"@appLockAutoLogin": {},
"appLockBiometrics": "Biometrika",
"@appLockBiometrics": {},
"appLockPasscode": "Prístupový kód",
"@appLockPasscode": {},
"accept": "Prijať",
"@accept": {},
"about": "O appke",
"@about": {},
"appLockTitle": "Nastavenie metódy prihlásenia pre {userName}",
"@appLockTitle": {
"description": "Pop-up to pick a login method",
"placeholders": {
"userName": {
"type": "String"
}
}
},
"ascending": "Vzostupne",
"@ascending": {},
"audio": "Zvuk",
"@audio": {},
"autoPlay": "Automatické prehrávanie",
"@autoPlay": {},
"backgroundBlur": "Rozmazanie pozadia",
"@backgroundBlur": {},
"backgroundOpacity": "Priehľadnosť pozadia",
"@backgroundOpacity": {},
"biometricsFailedCheckAgain": "Biometria zlyhala. Skontrolujte nastavenia a skúste to znova.",
"@biometricsFailedCheckAgain": {},
"bold": "Hrubý",
"@bold": {},
"cancel": "Zrušiť",
"@cancel": {},
"change": "Zmeniť",
"@change": {},
"clearAllSettings": "Vyčistiť všetky nastavenia",
"@clearAllSettings": {},
"clearAllSettingsQuestion": "Vynulovať všetky nastavenia?",
"@clearAllSettingsQuestion": {},
"clearChanges": "Vynulovanie zmien",
"@clearChanges": {},
"clearSelection": "Vynulovať výber",
"@clearSelection": {},
"close": "Zatvoriť",
"@close": {},
"code": "Kód",
"@code": {},
"collectionFolder": "Zložka kolekcie",
"@collectionFolder": {},
"color": "Farba",
"@color": {},
"combined": "Kombinované",
"@combined": {},
"communityRating": "Hodnotenie komunity",
"@communityRating": {},
"continuePage": "Pokračovať - strana {page}",
"@continuePage": {
"description": "Continue - page 1",
"placeholders": {
"page": {
"type": "int"
}
}
},
"controls": "Ovládanie",
"@controls": {},
"dashboard": "Ovládací panel",
"@dashboard": {},
"dashboardContinue": "Pokračovať",
"@dashboardContinue": {},
"dashboardContinueListening": "Pokračovať v počúvaní",
"@dashboardContinueListening": {},
"dashboardContinueReading": "Pokračovať v čítaní",
"@dashboardContinueReading": {},
"dashboardContinueWatching": "Pokračovať v sledovaní",
"@dashboardContinueWatching": {},
"nextUp": "Ďalej",
"@nextUp": {},
"dashboardRecentlyAdded": "Nedávno pridané v {name}",
"@dashboardRecentlyAdded": {
"description": "Recently added on home screen",
"placeholders": {
"name": {
"type": "String"
}
}
},
"dateAdded": "Dátum pridania",
"@dateAdded": {},
"dateLastContentAdded": "Dátum posledného pridania obsahu",
"@dateLastContentAdded": {},
"datePlayed": "Dátum prehrania",
"@datePlayed": {},
"days": "Dni",
"@days": {},
"delete": "Odstrániť",
"@delete": {},
"clear": "Vyčistiť",
"@clear": {},
"chapter": "{count, plural, other{Chapters} one{Chapter}}",
"@chapter": {
"description": "chapter",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
}
}

View file

@ -76,7 +76,7 @@
"@mediaSegmentRecap": {},
"addAsFavorite": "பிடித்ததாகச் சேர்க்கவும்",
"@addAsFavorite": {},
"actor": "{count, plural, other{கூத்தர்கள்} one {கூத்தர்}}",
"actor": "{count, plural, other{நடிகர்கள்} one {நடிகர்}}",
"@actor": {
"description": "actor",
"placeholders": {
@ -94,7 +94,7 @@
"@advanced": {},
"all": "அனைத்தும்",
"@all": {},
"appLockAutoLogin": "ஆட்டோ உள்நுழைவு",
"appLockAutoLogin": "தானாக உள்நுழை",
"@appLockAutoLogin": {},
"appLockBiometrics": "பயோமெட்ரிக்ச்",
"@appLockBiometrics": {},
@ -111,7 +111,7 @@
},
"ascending": "ஏறுதல்",
"@ascending": {},
"audio": "ஒலி தேர்வு",
"audio": "ஆடியோ",
"@audio": {},
"autoPlay": "தானாக விளையாடும்",
"@autoPlay": {},
@ -1209,5 +1209,83 @@
"errorLogs": "பிழை பதிவுகள்",
"@errorLogs": {},
"external": "வெளிப்புற",
"@external": {}
"@external": {},
"settingsLayoutSizesTitle": "தளவமைப்பு அளவுகள்",
"@settingsLayoutSizesTitle": {},
"settingsLayoutSizesDesc": "சாளர அளவின் அடிப்படையில் பயன்பாடு எந்த தளவமைப்பு அளவுகளைப் பயன்படுத்தலாம் என்பதைத் தேர்வுசெய்க",
"@settingsLayoutSizesDesc": {},
"settingsLayoutModesTitle": "தளவமைப்பு முறைகள்",
"@settingsLayoutModesTitle": {},
"tablet": "டேப்லெட்",
"@tablet": {},
"mediaSegmentActions": "ஊடக பிரிவு செயல்கள்",
"@mediaSegmentActions": {},
"segmentActionNone": "எதுவுமில்லை",
"@segmentActionNone": {},
"segmentActionAskToSkip": "தவிர்க்கச் சொல்லுங்கள்",
"@segmentActionAskToSkip": {},
"segmentActionSkip": "தவிர்",
"@segmentActionSkip": {},
"castAndCrew": "நடிகர்கள் & குழுவினர்",
"@castAndCrew": {},
"downloadFile": "பதிவிறக்கம் {type}",
"@downloadFile": {
"placeholders": {
"type": {
"type": "String"
}
}
},
"copyStreamUrl": "ச்ட்ரீம் முகவரி ஐ நகலெடுக்கவும்",
"@copyStreamUrl": {},
"copiedToClipboard": "இடைநிலைப்பலகைக்கு நகலெடுக்கப்பட்டது",
"@copiedToClipboard": {},
"internetStreamingQualityTitle": "இணைய தகுதி",
"@internetStreamingQualityTitle": {},
"internetStreamingQualityDesc": "இணையத்தில் அதிகபட்ச ச்ட்ரீமிங் தகுதி (மொபைல்)",
"@internetStreamingQualityDesc": {},
"homeStreamingQualityTitle": "வீட்டின் தகுதி",
"@homeStreamingQualityTitle": {},
"homeStreamingQualityDesc": "வீட்டு நெட்வொர்க்குடன் இணைக்கப்படும்போது அதிகபட்ச ச்ட்ரீமிங் தகுதி",
"@homeStreamingQualityDesc": {},
"qualityOptionsTitle": "தரமான விருப்பங்கள்",
"@qualityOptionsTitle": {},
"loading": "ஏற்றுகிறது",
"@loading": {},
"episodeAvailable": "கிடைக்கிறது",
"@episodeAvailable": {},
"episodeUnaired": "UNIRED",
"@episodeUnaired": {},
"episodeMissing": "இல்லை",
"@episodeMissing": {},
"settingsPlayerBufferSizeTitle": "வீடியோ இடையக அளவு",
"@settingsPlayerBufferSizeTitle": {},
"settingsPlayerBufferSizeDesc": "வீடியோ பிளேபேக்கிற்கான இடையக அளவை உள்ளமைக்கவும், தற்காலிக சேமிப்பில் எவ்வளவு தரவு ஏற்றப்படுகிறது என்பதை தீர்மானிக்கவும்.",
"@settingsPlayerBufferSizeDesc": {},
"settingsLayoutModesDesc": "பயன்பாடு ஒற்றை அல்லது இரட்டை-பேனல் தளவமைப்புகளைப் பயன்படுத்தலாமா என்பதைக் கட்டுப்படுத்தவும்",
"@settingsLayoutModesDesc": {},
"phone": "தொலைபேசி",
"@phone": {},
"desktop": "டெச்க்டாப்",
"@desktop": {},
"layoutModeSingle": "ஒற்றை",
"@layoutModeSingle": {},
"layoutModeDual": "இருமம்",
"@layoutModeDual": {},
"qualityOptionsOriginal": "அசல்",
"@qualityOptionsOriginal": {},
"qualityOptionsAuto": "தானி",
"@qualityOptionsAuto": {},
"version": "பதிப்பு",
"@version": {},
"guestActor": "{count, plural, other{கௌரவ நடிகர்கள்} one{கௌரவ நடிகர்}}",
"@guestActor": {
"description": "Guest actors",
"placeholders": {
"count": {
"type": "int",
"example": "1"
}
}
}
}

View file

@ -1295,5 +1295,65 @@
"settingsPlayerBufferSizeTitle": "Розмір буфера відео",
"@settingsPlayerBufferSizeTitle": {},
"settingsPlayerBufferSizeDesc": "Налаштуйте розмір буфера для відтворення відео, визначивши, скільки даних буде завантажено в кеш.",
"@settingsPlayerBufferSizeDesc": {}
"@settingsPlayerBufferSizeDesc": {},
"maxConcurrentDownloadsTitle": "Максимум одночасних завантажень",
"@maxConcurrentDownloadsTitle": {},
"maxConcurrentDownloadsDesc": "Встановлює максимальну кількість завантажень, які можуть виконуватися одночасно. Встановіть значення 0, щоб вимкнути обмеження.",
"@maxConcurrentDownloadsDesc": {},
"playbackTrackSelection": "Вибір доріжки відтворення",
"@playbackTrackSelection": {},
"rememberSubtitleSelectionsDesc": "Спробувати встановити доріжку субтитрів, яка найкраще відповідає останньому відео.",
"@rememberSubtitleSelectionsDesc": {},
"rememberAudioSelections": "Встановити аудіодоріжку на основі попереднього елемента",
"@rememberAudioSelections": {},
"rememberAudioSelectionsDesc": "Спробувати встановити аудіодоріжку, яка найкраще відповідає останньому відео.",
"@rememberAudioSelectionsDesc": {},
"rememberSubtitleSelections": "Встановити доріжку субтитрів на основі попереднього елемента",
"@rememberSubtitleSelections": {},
"similarToRecentlyPlayed": "Схоже на нещодавно відтворене",
"@similarToRecentlyPlayed": {},
"similarToLikedItem": "Схоже на вподобаний елемент",
"@similarToLikedItem": {},
"hasActorFromRecentlyPlayed": "Має актора з нещодавно відтвореного",
"@hasActorFromRecentlyPlayed": {},
"latest": "Останнє",
"@latest": {},
"hasLikedDirector": "Має вподобаного режисера",
"@hasLikedDirector": {},
"hasLikedActor": "Має вподобаного актора",
"@hasLikedActor": {},
"recommended": "Рекомендовано",
"@recommended": {},
"hasDirectorFromRecentlyPlayed": "Має режисера з нещодавно відтвореного",
"@hasDirectorFromRecentlyPlayed": {},
"exitFladderTitle": "Вийти з Fladder",
"@exitFladderTitle": {},
"playbackTypeDirect": "Пряме",
"@playbackTypeDirect": {},
"playbackType": "Тип відтворення",
"@playbackType": {},
"playbackTypeTranscode": "Перекодування",
"@playbackTypeTranscode": {},
"playbackTypeOffline": "Офлайн",
"@playbackTypeOffline": {},
"latestReleases": "Останні релізи",
"@latestReleases": {},
"autoCheckForUpdates": "Періодично перевіряти наявність оновлень",
"@autoCheckForUpdates": {},
"newReleaseFoundTitle": "Доступне оновлення {newRelease}!",
"@newReleaseFoundTitle": {
"placeholders": {
"newRelease": {
"type": "String"
}
}
},
"newUpdateFoundOnGithub": "Знайдено нове оновлення на Github",
"@newUpdateFoundOnGithub": {},
"enableBackgroundPostersTitle": "Увімкнути фонові постери",
"@enableBackgroundPostersTitle": {},
"settingsEnableOsMediaControlsDesc": "Дозволяє керувати відтворенням за допомогою медіа-клавіш та показувати поточний відтворюваний медіафайл в ОС",
"@settingsEnableOsMediaControlsDesc": {},
"enableBackgroundPostersDesc": "Показувати випадкові постери на відповідних екранах",
"@enableBackgroundPostersDesc": {}
}

View file

@ -892,23 +892,23 @@
"@video": {},
"videoScaling": "视频缩放",
"@videoScaling": {},
"videoScalingContain": "包含",
"videoScalingContain": "适应窗口",
"@videoScalingContain": {},
"videoScalingCover": "覆盖",
"videoScalingCover": "覆盖填充",
"@videoScalingCover": {},
"videoScalingFill": "填充",
"videoScalingFill": "拉伸填充",
"@videoScalingFill": {},
"videoScalingFillScreenDesc": "填充导航栏和状态栏",
"videoScalingFillScreenDesc": "延伸至导航栏和状态栏区域",
"@videoScalingFillScreenDesc": {},
"videoScalingFillScreenNotif": "全屏覆盖视频适应,在水平旋转中",
"videoScalingFillScreenNotif": "全屏模式将覆盖视频适配设置",
"@videoScalingFillScreenNotif": {},
"videoScalingFillScreenTitle": "填满屏幕",
"videoScalingFillScreenTitle": "全屏填充",
"@videoScalingFillScreenTitle": {},
"videoScalingFitHeight": "适应高度",
"@videoScalingFitHeight": {},
"videoScalingFitWidth": "适应宽度",
"@videoScalingFitWidth": {},
"videoScalingScaleDown": "缩小",
"videoScalingScaleDown": "自适应缩小",
"@videoScalingScaleDown": {},
"viewPhotos": "查看照片",
"@viewPhotos": {},
@ -946,13 +946,13 @@
}
}
},
"syncStatusComplete": "完成",
"syncStatusComplete": "同步完成",
"@syncStatusComplete": {},
"syncNoFolderSetup": "未设置同步文件夹",
"@syncNoFolderSetup": {},
"syncStatusNotFound": "未找到",
"syncStatusNotFound": "资源未找到",
"@syncStatusNotFound": {},
"syncStatusPaused": "已暂停",
"syncStatusPaused": "同步已暂停",
"@syncStatusPaused": {},
"syncOverlayDeleting": "正在删除已同步的项目",
"@syncOverlayDeleting": {},
@ -964,7 +964,7 @@
}
}
},
"syncStatusCanceled": "已取消",
"syncStatusCanceled": "用户已取消",
"@syncStatusCanceled": {},
"settingsHomeBannerInformationDesc": "主页横幅中显示的信息",
"@settingsHomeBannerInformationDesc": {},
@ -972,21 +972,21 @@
"@settingsHomeBannerInformationTitle": {},
"syncStatusEnqueued": "已添加到队列",
"@syncStatusEnqueued": {},
"syncStatusRunning": "运行中",
"syncStatusRunning": "同步中",
"@syncStatusRunning": {},
"syncStatusFailed": "失败",
"syncStatusFailed": "同步失败",
"@syncStatusFailed": {},
"syncStatusWaitingToRetry": "等待重试",
"syncStatusWaitingToRetry": "等待自动重试",
"@syncStatusWaitingToRetry": {},
"syncStatusSynced": "已同步",
"syncStatusSynced": "已完成同步",
"@syncStatusSynced": {},
"syncStatusPartially": "部分",
"syncStatusPartially": "部分同步成功",
"@syncStatusPartially": {},
"syncOverlaySyncing": "正在同步项目详情",
"@syncOverlaySyncing": {},
"syncSelectDownloadsFolder": "选择下载文件夹",
"@syncSelectDownloadsFolder": {},
"syncRemoveUnableToDeleteItem": "无法移除已同步的项目,出了点问题",
"syncRemoveUnableToDeleteItem": "出现了一些问题,导致无法移除已同步的项目",
"@syncRemoveUnableToDeleteItem": {},
"syncAddItemForSyncing": "已添加 {item} 进行同步",
"@syncAddItemForSyncing": {
@ -1086,9 +1086,9 @@
"@settingsAutoNextDesc": {},
"autoNextOffStaticDesc": "播放时间还剩 30 秒时显示接下来预览窗",
"@autoNextOffStaticDesc": {},
"autoNextOffSmartTitle": "自动的",
"autoNextOffSmartTitle": "智能模式",
"@autoNextOffSmartTitle": {},
"autoNextOffStaticTitle": "静态的",
"autoNextOffStaticTitle": "固定模式",
"@autoNextOffStaticTitle": {},
"playbackRate": "播放速率",
"@playbackRate": {},
@ -1286,5 +1286,65 @@
"settingsPlayerBufferSizeDesc": "配置视频播放的缓冲区大小,确定加载到缓存中的数据量。",
"@settingsPlayerBufferSizeDesc": {},
"settingsPlayerBufferSizeTitle": "视频缓冲区大小",
"@settingsPlayerBufferSizeTitle": {}
"@settingsPlayerBufferSizeTitle": {},
"maxConcurrentDownloadsTitle": "最大并发下载",
"@maxConcurrentDownloadsTitle": {},
"maxConcurrentDownloadsDesc": "设置可同时运行的最大下载数量。设置为 0 则禁用限制。",
"@maxConcurrentDownloadsDesc": {},
"playbackTrackSelection": "播放轨道选择",
"@playbackTrackSelection": {},
"rememberAudioSelections": "根据上一项设置音轨",
"@rememberAudioSelections": {},
"rememberAudioSelectionsDesc": "尝试将音轨设置为与上一个视频最接近的匹配。",
"@rememberAudioSelectionsDesc": {},
"rememberSubtitleSelections": "根据上一项设置字幕轨",
"@rememberSubtitleSelections": {},
"rememberSubtitleSelectionsDesc": "尝试将字幕轨设置为与上一个视频最接近的匹配。",
"@rememberSubtitleSelectionsDesc": {},
"similarToLikedItem": "类似的喜欢项目",
"@similarToLikedItem": {},
"exitFladderTitle": "退出 Fladder",
"@exitFladderTitle": {},
"similarToRecentlyPlayed": "类似的最近播放",
"@similarToRecentlyPlayed": {},
"hasActorFromRecentlyPlayed": "最近播出的演员",
"@hasActorFromRecentlyPlayed": {},
"hasLikedDirector": "喜欢的导演",
"@hasLikedDirector": {},
"hasLikedActor": "喜欢的演员",
"@hasLikedActor": {},
"latest": "最新的",
"@latest": {},
"recommended": "建议",
"@recommended": {},
"hasDirectorFromRecentlyPlayed": "最近播出的导演",
"@hasDirectorFromRecentlyPlayed": {},
"playbackType": "播放类型",
"@playbackType": {},
"playbackTypeDirect": "直接播放",
"@playbackTypeDirect": {},
"playbackTypeTranscode": "转码",
"@playbackTypeTranscode": {},
"playbackTypeOffline": "离线",
"@playbackTypeOffline": {},
"latestReleases": "最新版本",
"@latestReleases": {},
"autoCheckForUpdates": "定期检查更新",
"@autoCheckForUpdates": {},
"newReleaseFoundTitle": "更新 {newRelease} 可用!",
"@newReleaseFoundTitle": {
"placeholders": {
"newRelease": {
"type": "String"
}
}
},
"newUpdateFoundOnGithub": "在 Github 上发现新的更新",
"@newUpdateFoundOnGithub": {},
"settingsEnableOsMediaControlsDesc": "允许使用媒体键控制播放并在操作系统中显示当前正在播放的媒体",
"@settingsEnableOsMediaControlsDesc": {},
"enableBackgroundPostersTitle": "启用背景海报",
"@enableBackgroundPostersTitle": {},
"enableBackgroundPostersDesc": "在适用的屏幕上显示随机海报",
"@enableBackgroundPostersDesc": {}
}

View file

@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:package_info_plus/package_info_plus.dart';
@ -18,9 +17,11 @@ import 'package:smtc_windows/smtc_windows.dart' if (dart.library.html) 'package:
import 'package:universal_html/html.dart' as html;
import 'package:window_manager/window_manager.dart';
import 'package:fladder/l10n/generated/app_localizations.dart';
import 'package:fladder/models/account_model.dart';
import 'package:fladder/models/settings/home_settings_model.dart';
import 'package:fladder/models/settings/arguments_model.dart';
import 'package:fladder/models/syncing/i_synced_item.dart';
import 'package:fladder/providers/arguments_provider.dart';
import 'package:fladder/providers/crash_log_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/shared_provider.dart';
@ -31,7 +32,7 @@ import 'package:fladder/routes/auto_router.dart';
import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/login/lock_screen.dart';
import 'package:fladder/theme.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/application_info.dart';
import 'package:fladder/util/fladder_config.dart';
import 'package:fladder/util/localization_helper.dart';
@ -52,7 +53,7 @@ Future<Map<String, dynamic>> loadConfig() async {
return jsonDecode(configString);
}
void main() async {
void main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
final crashProvider = CrashLogNotifier();
@ -96,6 +97,7 @@ void main() async {
sharedPreferencesProvider.overrideWith((ref) => sharedPreferences),
applicationInfoProvider.overrideWith((ref) => applicationInfo),
crashLogProvider.overrideWith((ref) => crashProvider),
argumentsStateProvider.overrideWith((ref) => ArgumentsModel.fromArguments(args)),
syncProvider.overrideWith((ref) => SyncNotifier(
ref,
!kIsWeb
@ -108,13 +110,7 @@ void main() async {
))
],
child: AdaptiveLayoutBuilder(
fallBack: ViewSize.tablet,
layoutPoints: [
LayoutPoints(start: 0, end: 599, type: ViewSize.phone),
LayoutPoints(start: 600, end: 1919, type: ViewSize.tablet),
LayoutPoints(start: 1920, end: 3180, type: ViewSize.desktop),
],
child: const Main(),
child: (context) => const Main(),
),
),
);
@ -241,6 +237,10 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
final startupArguments = ref.read(argumentsStateProvider);
if (startupArguments.htpcMode && !(await windowManager.isFullScreen())) {
await windowManager.setFullScreen(true);
}
});
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []);
@ -288,13 +288,15 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
builder: (context, child) => Localizations.override(
context: context,
locale: AppLocalizations.supportedLocales.firstWhere(
(element) => element.languageCode == language.languageCode,
orElse: () => const Locale('en', "GB"),
),
child: LocalizationContextWrapper(child: ScaffoldMessenger(child: child ?? Container())),
locale: language,
localeResolutionCallback: (locale, supportedLocales) {
if (locale == null || !supportedLocales.contains(locale)) {
return const Locale('en');
}
return locale;
},
builder: (context, child) => LocalizationContextWrapper(
child: ScaffoldMessenger(child: child ?? Container()),
),
debugShowCheckedModeBanner: false,
darkTheme: darkTheme.copyWith(
@ -304,6 +306,7 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
colorScheme: darkTheme.colorScheme.copyWith(
surface: amoledOverwrite,
surfaceContainerHighest: amoledOverwrite,
surfaceContainerLow: amoledOverwrite,
),
),
themeMode: themeMode,

View file

@ -4,19 +4,19 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/credentials_model.dart';
import 'package:fladder/models/library_filters_model.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
part 'account_model.freezed.dart';
part 'account_model.g.dart';
@freezed
@Freezed(copyWith: true)
class AccountModel with _$AccountModel {
const AccountModel._();
@ -34,6 +34,7 @@ class AccountModel with _$AccountModel {
@Default([]) List<LibraryFiltersModel> savedFilters,
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false) ServerConfiguration? serverConfiguration,
@JsonKey(includeFromJson: false, includeToJson: false) UserConfiguration? userConfiguration,
}) = _AccountModel;
factory AccountModel.fromJson(Map<String, dynamic> json) => _$AccountModelFromJson(json);

View file

@ -37,6 +37,9 @@ mixin _$AccountModel {
@JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? get serverConfiguration =>
throw _privateConstructorUsedError;
@JsonKey(includeFromJson: false, includeToJson: false)
UserConfiguration? get userConfiguration =>
throw _privateConstructorUsedError;
/// Serializes this AccountModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@ -68,7 +71,9 @@ abstract class $AccountModelCopyWith<$Res> {
List<LibraryFiltersModel> savedFilters,
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? serverConfiguration});
ServerConfiguration? serverConfiguration,
@JsonKey(includeFromJson: false, includeToJson: false)
UserConfiguration? userConfiguration});
}
/// @nodoc
@ -99,6 +104,7 @@ class _$AccountModelCopyWithImpl<$Res, $Val extends AccountModel>
Object? savedFilters = null,
Object? policy = freezed,
Object? serverConfiguration = freezed,
Object? userConfiguration = freezed,
}) {
return _then(_value.copyWith(
name: null == name
@ -153,6 +159,10 @@ class _$AccountModelCopyWithImpl<$Res, $Val extends AccountModel>
? _value.serverConfiguration
: serverConfiguration // ignore: cast_nullable_to_non_nullable
as ServerConfiguration?,
userConfiguration: freezed == userConfiguration
? _value.userConfiguration
: userConfiguration // ignore: cast_nullable_to_non_nullable
as UserConfiguration?,
) as $Val);
}
}
@ -179,7 +189,9 @@ abstract class _$$AccountModelImplCopyWith<$Res>
List<LibraryFiltersModel> savedFilters,
@JsonKey(includeFromJson: false, includeToJson: false) UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? serverConfiguration});
ServerConfiguration? serverConfiguration,
@JsonKey(includeFromJson: false, includeToJson: false)
UserConfiguration? userConfiguration});
}
/// @nodoc
@ -208,6 +220,7 @@ class __$$AccountModelImplCopyWithImpl<$Res>
Object? savedFilters = null,
Object? policy = freezed,
Object? serverConfiguration = freezed,
Object? userConfiguration = freezed,
}) {
return _then(_$AccountModelImpl(
name: null == name
@ -262,6 +275,10 @@ class __$$AccountModelImplCopyWithImpl<$Res>
? _value.serverConfiguration
: serverConfiguration // ignore: cast_nullable_to_non_nullable
as ServerConfiguration?,
userConfiguration: freezed == userConfiguration
? _value.userConfiguration
: userConfiguration // ignore: cast_nullable_to_non_nullable
as UserConfiguration?,
));
}
}
@ -283,7 +300,9 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
final List<LibraryFiltersModel> savedFilters = const [],
@JsonKey(includeFromJson: false, includeToJson: false) this.policy,
@JsonKey(includeFromJson: false, includeToJson: false)
this.serverConfiguration})
this.serverConfiguration,
@JsonKey(includeFromJson: false, includeToJson: false)
this.userConfiguration})
: _latestItemsExcludes = latestItemsExcludes,
_searchQueryHistory = searchQueryHistory,
_savedFilters = savedFilters,
@ -346,10 +365,13 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
@override
@JsonKey(includeFromJson: false, includeToJson: false)
final ServerConfiguration? serverConfiguration;
@override
@JsonKey(includeFromJson: false, includeToJson: false)
final UserConfiguration? userConfiguration;
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, savedFilters: $savedFilters, policy: $policy, serverConfiguration: $serverConfiguration)';
return 'AccountModel(name: $name, id: $id, avatar: $avatar, lastUsed: $lastUsed, authMethod: $authMethod, localPin: $localPin, credentials: $credentials, latestItemsExcludes: $latestItemsExcludes, searchQueryHistory: $searchQueryHistory, quickConnectState: $quickConnectState, savedFilters: $savedFilters, policy: $policy, serverConfiguration: $serverConfiguration, userConfiguration: $userConfiguration)';
}
@override
@ -369,56 +391,10 @@ class _$AccountModelImpl extends _AccountModel with DiagnosticableTreeMixin {
..add(DiagnosticsProperty('quickConnectState', quickConnectState))
..add(DiagnosticsProperty('savedFilters', savedFilters))
..add(DiagnosticsProperty('policy', policy))
..add(DiagnosticsProperty('serverConfiguration', serverConfiguration));
..add(DiagnosticsProperty('serverConfiguration', serverConfiguration))
..add(DiagnosticsProperty('userConfiguration', userConfiguration));
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$AccountModelImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.id, id) || other.id == id) &&
(identical(other.avatar, avatar) || other.avatar == avatar) &&
(identical(other.lastUsed, lastUsed) ||
other.lastUsed == lastUsed) &&
(identical(other.authMethod, authMethod) ||
other.authMethod == authMethod) &&
(identical(other.localPin, localPin) ||
other.localPin == localPin) &&
(identical(other.credentials, credentials) ||
other.credentials == credentials) &&
const DeepCollectionEquality()
.equals(other._latestItemsExcludes, _latestItemsExcludes) &&
const DeepCollectionEquality()
.equals(other._searchQueryHistory, _searchQueryHistory) &&
(identical(other.quickConnectState, quickConnectState) ||
other.quickConnectState == quickConnectState) &&
const DeepCollectionEquality()
.equals(other._savedFilters, _savedFilters) &&
(identical(other.policy, policy) || other.policy == policy) &&
(identical(other.serverConfiguration, serverConfiguration) ||
other.serverConfiguration == serverConfiguration));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
name,
id,
avatar,
lastUsed,
authMethod,
localPin,
credentials,
const DeepCollectionEquality().hash(_latestItemsExcludes),
const DeepCollectionEquality().hash(_searchQueryHistory),
quickConnectState,
const DeepCollectionEquality().hash(_savedFilters),
policy,
serverConfiguration);
/// Create a copy of AccountModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -451,7 +427,9 @@ abstract class _AccountModel extends AccountModel {
@JsonKey(includeFromJson: false, includeToJson: false)
final UserPolicy? policy,
@JsonKey(includeFromJson: false, includeToJson: false)
final ServerConfiguration? serverConfiguration}) = _$AccountModelImpl;
final ServerConfiguration? serverConfiguration,
@JsonKey(includeFromJson: false, includeToJson: false)
final UserConfiguration? userConfiguration}) = _$AccountModelImpl;
const _AccountModel._() : super._();
factory _AccountModel.fromJson(Map<String, dynamic> json) =
@ -485,6 +463,9 @@ abstract class _AccountModel extends AccountModel {
@override
@JsonKey(includeFromJson: false, includeToJson: false)
ServerConfiguration? get serverConfiguration;
@override
@JsonKey(includeFromJson: false, includeToJson: false)
UserConfiguration? get userConfiguration;
/// Create a copy of AccountModel
/// with the given fields replaced by the non-null parameter values.

View file

@ -108,34 +108,11 @@ class BoxSetModelMapper extends SubClassMapperBase<BoxSetModel> {
@override
final Function instantiate = _instantiate;
static BoxSetModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<BoxSetModel>(map);
}
static BoxSetModel fromJson(String json) {
return ensureInitialized().decodeJson<BoxSetModel>(json);
}
}
mixin BoxSetModelMappable {
String toJson() {
return BoxSetModelMapper.ensureInitialized()
.encodeJson<BoxSetModel>(this as BoxSetModel);
}
Map<String, dynamic> toMap() {
return BoxSetModelMapper.ensureInitialized()
.encodeMap<BoxSetModel>(this as BoxSetModel);
}
BoxSetModelCopyWith<BoxSetModel, BoxSetModel, BoxSetModel> get copyWith =>
_BoxSetModelCopyWithImpl(this as BoxSetModel, $identity, $identity);
@override
String toString() {
return BoxSetModelMapper.ensureInitialized()
.stringifyValue(this as BoxSetModel);
}
}
extension BoxSetModelValueCopy<$R, $Out>

View file

@ -17,6 +17,8 @@ extension CollectionTypeExtension on CollectionType {
Set<FladderItemType> get itemKinds {
switch (this) {
case CollectionType.music:
return {FladderItemType.musicAlbum};
case CollectionType.movies:
return {FladderItemType.movie};
case CollectionType.tvshows:
@ -30,6 +32,8 @@ extension CollectionTypeExtension on CollectionType {
IconData getIconType(bool outlined) {
switch (this) {
case CollectionType.music:
return outlined ? IconsaxPlusLinear.music_square : IconsaxPlusBold.music_square;
case CollectionType.movies:
return outlined ? IconsaxPlusLinear.video_horizontal : IconsaxPlusBold.video_horizontal;
case CollectionType.tvshows:
@ -48,4 +52,16 @@ extension CollectionTypeExtension on CollectionType {
return IconsaxPlusLinear.information;
}
}
double? get aspectRatio => switch (this) {
CollectionType.music ||
CollectionType.homevideos ||
CollectionType.boxsets ||
CollectionType.photos ||
CollectionType.livetv ||
CollectionType.playlists =>
0.8,
CollectionType.folders => 1.3,
_ => null,
};
}

View file

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
@ -304,6 +304,15 @@ enum FladderItemType {
const FladderItemType({required this.icon, required this.selectedicon});
double get aspectRatio => switch (this) {
FladderItemType.video => 0.8,
FladderItemType.photo => 0.8,
FladderItemType.photoAlbum => 0.8,
FladderItemType.musicAlbum => 0.8,
FladderItemType.baseType => 0.8,
_ => 0.55,
};
static Set<FladderItemType> get playable => {
FladderItemType.series,
FladderItemType.episode,
@ -317,27 +326,25 @@ enum FladderItemType {
FladderItemType.video,
};
String label(BuildContext context) {
return switch (this) {
FladderItemType.baseType => context.localized.mediaTypeBase,
FladderItemType.audio => context.localized.audio,
FladderItemType.collectionFolder => context.localized.collectionFolder,
FladderItemType.musicAlbum => context.localized.musicAlbum,
FladderItemType.musicVideo => context.localized.video,
FladderItemType.video => context.localized.video,
FladderItemType.movie => context.localized.mediaTypeMovie,
FladderItemType.series => context.localized.mediaTypeSeries,
FladderItemType.season => context.localized.mediaTypeSeason,
FladderItemType.episode => context.localized.mediaTypeEpisode,
FladderItemType.photo => context.localized.mediaTypePhoto,
FladderItemType.person => context.localized.mediaTypePerson,
FladderItemType.photoAlbum => context.localized.mediaTypePhotoAlbum,
FladderItemType.folder => context.localized.mediaTypeFolder,
FladderItemType.boxset => context.localized.mediaTypeBoxset,
FladderItemType.playlist => context.localized.mediaTypePlaylist,
FladderItemType.book => context.localized.mediaTypeBook,
};
}
String label(BuildContext context) => switch (this) {
FladderItemType.baseType => context.localized.mediaTypeBase,
FladderItemType.audio => context.localized.audio,
FladderItemType.collectionFolder => context.localized.collectionFolder,
FladderItemType.musicAlbum => context.localized.musicAlbum,
FladderItemType.musicVideo => context.localized.video,
FladderItemType.video => context.localized.video,
FladderItemType.movie => context.localized.mediaTypeMovie,
FladderItemType.series => context.localized.mediaTypeSeries,
FladderItemType.season => context.localized.mediaTypeSeason,
FladderItemType.episode => context.localized.mediaTypeEpisode,
FladderItemType.photo => context.localized.mediaTypePhoto,
FladderItemType.person => context.localized.mediaTypePerson,
FladderItemType.photoAlbum => context.localized.mediaTypePhotoAlbum,
FladderItemType.folder => context.localized.mediaTypeFolder,
FladderItemType.boxset => context.localized.mediaTypeBoxset,
FladderItemType.playlist => context.localized.mediaTypePlaylist,
FladderItemType.book => context.localized.mediaTypeBook,
};
BaseItemKind get dtoKind => switch (this) {
FladderItemType.baseType => BaseItemKind.userrootfolder,

View file

@ -93,35 +93,12 @@ class ItemBaseModelMapper extends ClassMapperBase<ItemBaseModel> {
@override
final Function instantiate = _instantiate;
static ItemBaseModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<ItemBaseModel>(map);
}
static ItemBaseModel fromJson(String json) {
return ensureInitialized().decodeJson<ItemBaseModel>(json);
}
}
mixin ItemBaseModelMappable {
String toJson() {
return ItemBaseModelMapper.ensureInitialized()
.encodeJson<ItemBaseModel>(this as ItemBaseModel);
}
Map<String, dynamic> toMap() {
return ItemBaseModelMapper.ensureInitialized()
.encodeMap<ItemBaseModel>(this as ItemBaseModel);
}
ItemBaseModelCopyWith<ItemBaseModel, ItemBaseModel, ItemBaseModel>
get copyWith => _ItemBaseModelCopyWithImpl(
this as ItemBaseModel, $identity, $identity);
@override
String toString() {
return ItemBaseModelMapper.ensureInitialized()
.stringifyValue(this as ItemBaseModel);
}
}
extension ItemBaseModelValueCopy<$R, $Out>

View file

@ -199,17 +199,34 @@ extension EpisodeListExtensions on List<EpisodeModel> {
}
EpisodeModel? get nextUp {
final lastProgress =
lastIndexWhere((element) => element.userData.progress != 0 && element.status == EpisodeStatus.available);
final lastPlayed =
lastIndexWhere((element) => element.userData.played && element.status == EpisodeStatus.available);
final episodes = where((e) => e.season > 0 && e.status == EpisodeStatus.available).toList();
if (episodes.isEmpty) return null;
if (lastProgress == -1 && lastPlayed == -1) {
return firstWhereOrNull((element) => element.status == EpisodeStatus.available);
} else {
return getRange(lastProgress > lastPlayed ? lastProgress : lastPlayed + 1, length)
.firstWhereOrNull((element) => element.status == EpisodeStatus.available);
final lastProgressIndex = episodes.lastIndexWhere((e) => e.userData.progress != 0);
final lastPlayedIndex = episodes.lastIndexWhere((e) => e.userData.played);
final lastWatchedIndex = [lastProgressIndex, lastPlayedIndex].reduce((a, b) => a > b ? a : b);
if (lastWatchedIndex >= 0) {
final current = episodes[lastWatchedIndex];
if (!current.userData.played && current.userData.progress != 0) {
return current;
}
final nextIndex = lastWatchedIndex + 1;
if (nextIndex < episodes.length) {
final next = episodes[nextIndex];
if (!next.userData.played && next.userData.progress != 0) {
return next;
}
final nextUnplayed = episodes.sublist(nextIndex).firstWhereOrNull(
(e) => e.status == EpisodeStatus.available && !e.userData.played,
);
if (nextUnplayed != null) return nextUnplayed;
}
}
return episodes.firstOrNull;
}
bool get allPlayed {

View file

@ -141,34 +141,11 @@ class EpisodeModelMapper extends SubClassMapperBase<EpisodeModel> {
@override
final Function instantiate = _instantiate;
static EpisodeModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<EpisodeModel>(map);
}
static EpisodeModel fromJson(String json) {
return ensureInitialized().decodeJson<EpisodeModel>(json);
}
}
mixin EpisodeModelMappable {
String toJson() {
return EpisodeModelMapper.ensureInitialized()
.encodeJson<EpisodeModel>(this as EpisodeModel);
}
Map<String, dynamic> toMap() {
return EpisodeModelMapper.ensureInitialized()
.encodeMap<EpisodeModel>(this as EpisodeModel);
}
EpisodeModelCopyWith<EpisodeModel, EpisodeModel, EpisodeModel> get copyWith =>
_EpisodeModelCopyWithImpl(this as EpisodeModel, $identity, $identity);
@override
String toString() {
return EpisodeModelMapper.ensureInitialized()
.stringifyValue(this as EpisodeModel);
}
}
extension EpisodeModelValueCopy<$R, $Out>

View file

@ -108,34 +108,11 @@ class FolderModelMapper extends SubClassMapperBase<FolderModel> {
@override
final Function instantiate = _instantiate;
static FolderModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<FolderModel>(map);
}
static FolderModel fromJson(String json) {
return ensureInitialized().decodeJson<FolderModel>(json);
}
}
mixin FolderModelMappable {
String toJson() {
return FolderModelMapper.ensureInitialized()
.encodeJson<FolderModel>(this as FolderModel);
}
Map<String, dynamic> toMap() {
return FolderModelMapper.ensureInitialized()
.encodeMap<FolderModel>(this as FolderModel);
}
FolderModelCopyWith<FolderModel, FolderModel, FolderModel> get copyWith =>
_FolderModelCopyWithImpl(this as FolderModel, $identity, $identity);
@override
String toString() {
return FolderModelMapper.ensureInitialized()
.stringifyValue(this as FolderModel);
}
}
extension FolderModelValueCopy<$R, $Out>

View file

@ -18,92 +18,6 @@ final _privateConstructorUsedError = UnsupportedError(
mixin _$ItemPropertiesModel {
bool get canDelete => throw _privateConstructorUsedError;
bool get canDownload => throw _privateConstructorUsedError;
/// Create a copy of ItemPropertiesModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ItemPropertiesModelCopyWith<ItemPropertiesModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ItemPropertiesModelCopyWith<$Res> {
factory $ItemPropertiesModelCopyWith(
ItemPropertiesModel value, $Res Function(ItemPropertiesModel) then) =
_$ItemPropertiesModelCopyWithImpl<$Res, ItemPropertiesModel>;
@useResult
$Res call({bool canDelete, bool canDownload});
}
/// @nodoc
class _$ItemPropertiesModelCopyWithImpl<$Res, $Val extends ItemPropertiesModel>
implements $ItemPropertiesModelCopyWith<$Res> {
_$ItemPropertiesModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ItemPropertiesModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? canDelete = null,
Object? canDownload = null,
}) {
return _then(_value.copyWith(
canDelete: null == canDelete
? _value.canDelete
: canDelete // ignore: cast_nullable_to_non_nullable
as bool,
canDownload: null == canDownload
? _value.canDownload
: canDownload // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$ItemPropertiesModelImplCopyWith<$Res>
implements $ItemPropertiesModelCopyWith<$Res> {
factory _$$ItemPropertiesModelImplCopyWith(_$ItemPropertiesModelImpl value,
$Res Function(_$ItemPropertiesModelImpl) then) =
__$$ItemPropertiesModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool canDelete, bool canDownload});
}
/// @nodoc
class __$$ItemPropertiesModelImplCopyWithImpl<$Res>
extends _$ItemPropertiesModelCopyWithImpl<$Res, _$ItemPropertiesModelImpl>
implements _$$ItemPropertiesModelImplCopyWith<$Res> {
__$$ItemPropertiesModelImplCopyWithImpl(_$ItemPropertiesModelImpl _value,
$Res Function(_$ItemPropertiesModelImpl) _then)
: super(_value, _then);
/// Create a copy of ItemPropertiesModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? canDelete = null,
Object? canDownload = null,
}) {
return _then(_$ItemPropertiesModelImpl(
canDelete: null == canDelete
? _value.canDelete
: canDelete // ignore: cast_nullable_to_non_nullable
as bool,
canDownload: null == canDownload
? _value.canDownload
: canDownload // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
@ -122,29 +36,6 @@ class _$ItemPropertiesModelImpl extends _ItemPropertiesModel {
String toString() {
return 'ItemPropertiesModel._internal(canDelete: $canDelete, canDownload: $canDownload)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ItemPropertiesModelImpl &&
(identical(other.canDelete, canDelete) ||
other.canDelete == canDelete) &&
(identical(other.canDownload, canDownload) ||
other.canDownload == canDownload));
}
@override
int get hashCode => Object.hash(runtimeType, canDelete, canDownload);
/// Create a copy of ItemPropertiesModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ItemPropertiesModelImplCopyWith<_$ItemPropertiesModelImpl> get copyWith =>
__$$ItemPropertiesModelImplCopyWithImpl<_$ItemPropertiesModelImpl>(
this, _$identity);
}
abstract class _ItemPropertiesModel extends ItemPropertiesModel {
@ -157,11 +48,4 @@ abstract class _ItemPropertiesModel extends ItemPropertiesModel {
bool get canDelete;
@override
bool get canDownload;
/// Create a copy of ItemPropertiesModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ItemPropertiesModelImplCopyWith<_$ItemPropertiesModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -14,7 +14,7 @@ import 'package:fladder/models/items/images_models.dart';
part 'item_shared_models.mapper.dart';
@MappableClass()
@MappableClass(generateMethods: GenerateMethods.encode | GenerateMethods.decode | GenerateMethods.copy)
class UserData with UserDataMappable {
final bool isFavourite;
final int playCount;

View file

@ -92,10 +92,6 @@ mixin UserDataMappable {
UserDataCopyWith<UserData, UserData, UserData> get copyWith =>
_UserDataCopyWithImpl(this as UserData, $identity, $identity);
@override
String toString() {
return UserDataMapper.ensureInitialized().stringifyValue(this as UserData);
}
}
extension UserDataValueCopy<$R, $Out> on ObjectCopyWith<$R, UserData, $Out> {

View file

@ -112,35 +112,12 @@ class ItemStreamModelMapper extends SubClassMapperBase<ItemStreamModel> {
@override
final Function instantiate = _instantiate;
static ItemStreamModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<ItemStreamModel>(map);
}
static ItemStreamModel fromJson(String json) {
return ensureInitialized().decodeJson<ItemStreamModel>(json);
}
}
mixin ItemStreamModelMappable {
String toJson() {
return ItemStreamModelMapper.ensureInitialized()
.encodeJson<ItemStreamModel>(this as ItemStreamModel);
}
Map<String, dynamic> toMap() {
return ItemStreamModelMapper.ensureInitialized()
.encodeMap<ItemStreamModel>(this as ItemStreamModel);
}
ItemStreamModelCopyWith<ItemStreamModel, ItemStreamModel, ItemStreamModel>
get copyWith => _ItemStreamModelCopyWithImpl(
this as ItemStreamModel, $identity, $identity);
@override
String toString() {
return ItemStreamModelMapper.ensureInitialized()
.stringifyValue(this as ItemStreamModel);
}
}
extension ItemStreamModelValueCopy<$R, $Out>

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:collection/collection.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
import 'package:fladder/util/localization_helper.dart';
@ -39,7 +40,19 @@ class MediaSegment with _$MediaSegment {
bool inRange(Duration position) => (position.compareTo(start) >= 0 && position.compareTo(end) <= 0);
bool forceShow(Duration position) => (position - start).inSeconds < (end - start).inSeconds * 0.20;
SegmentVisibility visibility(Duration position, {bool force = false}) {
if (force) return SegmentVisibility.visible;
var difference = (position - start);
if (difference > const Duration(minutes: 1, seconds: 30)) return SegmentVisibility.hidden;
Duration clamp = ((end - start) * 0.20).clamp(Duration.zero, const Duration(minutes: 1));
return difference < clamp ? SegmentVisibility.visible : SegmentVisibility.partially;
}
}
enum SegmentVisibility {
hidden,
partially,
visible;
}
const Map<MediaSegmentType, SegmentSkip> defaultSegmentSkipValues = {

View file

@ -24,82 +24,6 @@ mixin _$MediaSegmentsModel {
/// Serializes this MediaSegmentsModel to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of MediaSegmentsModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$MediaSegmentsModelCopyWith<MediaSegmentsModel> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MediaSegmentsModelCopyWith<$Res> {
factory $MediaSegmentsModelCopyWith(
MediaSegmentsModel value, $Res Function(MediaSegmentsModel) then) =
_$MediaSegmentsModelCopyWithImpl<$Res, MediaSegmentsModel>;
@useResult
$Res call({List<MediaSegment> segments});
}
/// @nodoc
class _$MediaSegmentsModelCopyWithImpl<$Res, $Val extends MediaSegmentsModel>
implements $MediaSegmentsModelCopyWith<$Res> {
_$MediaSegmentsModelCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of MediaSegmentsModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? segments = null,
}) {
return _then(_value.copyWith(
segments: null == segments
? _value.segments
: segments // ignore: cast_nullable_to_non_nullable
as List<MediaSegment>,
) as $Val);
}
}
/// @nodoc
abstract class _$$MediaSegmentsModelImplCopyWith<$Res>
implements $MediaSegmentsModelCopyWith<$Res> {
factory _$$MediaSegmentsModelImplCopyWith(_$MediaSegmentsModelImpl value,
$Res Function(_$MediaSegmentsModelImpl) then) =
__$$MediaSegmentsModelImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({List<MediaSegment> segments});
}
/// @nodoc
class __$$MediaSegmentsModelImplCopyWithImpl<$Res>
extends _$MediaSegmentsModelCopyWithImpl<$Res, _$MediaSegmentsModelImpl>
implements _$$MediaSegmentsModelImplCopyWith<$Res> {
__$$MediaSegmentsModelImplCopyWithImpl(_$MediaSegmentsModelImpl _value,
$Res Function(_$MediaSegmentsModelImpl) _then)
: super(_value, _then);
/// Create a copy of MediaSegmentsModel
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? segments = null,
}) {
return _then(_$MediaSegmentsModelImpl(
segments: null == segments
? _value._segments
: segments // ignore: cast_nullable_to_non_nullable
as List<MediaSegment>,
));
}
}
/// @nodoc
@ -126,28 +50,6 @@ class _$MediaSegmentsModelImpl extends _MediaSegmentsModel {
return 'MediaSegmentsModel(segments: $segments)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$MediaSegmentsModelImpl &&
const DeepCollectionEquality().equals(other._segments, _segments));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(_segments));
/// Create a copy of MediaSegmentsModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$MediaSegmentsModelImplCopyWith<_$MediaSegmentsModelImpl> get copyWith =>
__$$MediaSegmentsModelImplCopyWithImpl<_$MediaSegmentsModelImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$MediaSegmentsModelImplToJson(
@ -166,13 +68,6 @@ abstract class _MediaSegmentsModel extends MediaSegmentsModel {
@override
List<MediaSegment> get segments;
/// Create a copy of MediaSegmentsModel
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$MediaSegmentsModelImplCopyWith<_$MediaSegmentsModelImpl> get copyWith =>
throw _privateConstructorUsedError;
}
MediaSegment _$MediaSegmentFromJson(Map<String, dynamic> json) {
@ -187,102 +82,6 @@ mixin _$MediaSegment {
/// Serializes this MediaSegment to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of MediaSegment
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$MediaSegmentCopyWith<MediaSegment> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $MediaSegmentCopyWith<$Res> {
factory $MediaSegmentCopyWith(
MediaSegment value, $Res Function(MediaSegment) then) =
_$MediaSegmentCopyWithImpl<$Res, MediaSegment>;
@useResult
$Res call({MediaSegmentType type, Duration start, Duration end});
}
/// @nodoc
class _$MediaSegmentCopyWithImpl<$Res, $Val extends MediaSegment>
implements $MediaSegmentCopyWith<$Res> {
_$MediaSegmentCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of MediaSegment
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? type = null,
Object? start = null,
Object? end = null,
}) {
return _then(_value.copyWith(
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as MediaSegmentType,
start: null == start
? _value.start
: start // ignore: cast_nullable_to_non_nullable
as Duration,
end: null == end
? _value.end
: end // ignore: cast_nullable_to_non_nullable
as Duration,
) as $Val);
}
}
/// @nodoc
abstract class _$$MediaSegmentImplCopyWith<$Res>
implements $MediaSegmentCopyWith<$Res> {
factory _$$MediaSegmentImplCopyWith(
_$MediaSegmentImpl value, $Res Function(_$MediaSegmentImpl) then) =
__$$MediaSegmentImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({MediaSegmentType type, Duration start, Duration end});
}
/// @nodoc
class __$$MediaSegmentImplCopyWithImpl<$Res>
extends _$MediaSegmentCopyWithImpl<$Res, _$MediaSegmentImpl>
implements _$$MediaSegmentImplCopyWith<$Res> {
__$$MediaSegmentImplCopyWithImpl(
_$MediaSegmentImpl _value, $Res Function(_$MediaSegmentImpl) _then)
: super(_value, _then);
/// Create a copy of MediaSegment
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? type = null,
Object? start = null,
Object? end = null,
}) {
return _then(_$MediaSegmentImpl(
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as MediaSegmentType,
start: null == start
? _value.start
: start // ignore: cast_nullable_to_non_nullable
as Duration,
end: null == end
? _value.end
: end // ignore: cast_nullable_to_non_nullable
as Duration,
));
}
}
/// @nodoc
@ -307,28 +106,6 @@ class _$MediaSegmentImpl extends _MediaSegment {
return 'MediaSegment(type: $type, start: $start, end: $end)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$MediaSegmentImpl &&
(identical(other.type, type) || other.type == type) &&
(identical(other.start, start) || other.start == start) &&
(identical(other.end, end) || other.end == end));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, type, start, end);
/// Create a copy of MediaSegment
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$MediaSegmentImplCopyWith<_$MediaSegmentImpl> get copyWith =>
__$$MediaSegmentImplCopyWithImpl<_$MediaSegmentImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$MediaSegmentImplToJson(
@ -353,11 +130,4 @@ abstract class _MediaSegment extends MediaSegment {
Duration get start;
@override
Duration get end;
/// Create a copy of MediaSegment
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$MediaSegmentImplCopyWith<_$MediaSegmentImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -175,6 +175,20 @@ class StreamModel {
});
}
class AudioAndSubStreamModel extends StreamModel {
final String language;
final String displayTitle;
AudioAndSubStreamModel({
required this.displayTitle,
required super.name,
required super.codec,
required super.isDefault,
required super.isExternal,
required super.index,
required this.language,
});
}
class VersionStreamModel {
final String name;
final int index;
@ -250,19 +264,17 @@ extension SortByExternalExtension<T extends StreamModel> on Iterable<T> {
}
}
class AudioStreamModel extends StreamModel {
final String displayTitle;
final String language;
class AudioStreamModel extends AudioAndSubStreamModel {
final String channelLayout;
AudioStreamModel({
required this.displayTitle,
required super.displayTitle,
required super.name,
required super.codec,
required super.isDefault,
required super.isExternal,
required super.index,
required this.language,
required super.language,
required this.channelLayout,
});
@ -292,8 +304,8 @@ class AudioStreamModel extends StreamModel {
AudioStreamModel.no({
super.name = 'Off',
this.displayTitle = 'Off',
this.language = '',
super.displayTitle = 'Off',
super.language = '',
super.codec = '',
this.channelLayout = '',
super.isDefault = false,
@ -302,19 +314,17 @@ class AudioStreamModel extends StreamModel {
});
}
class SubStreamModel extends StreamModel {
class SubStreamModel extends AudioAndSubStreamModel {
String id;
String title;
String displayTitle;
String language;
String? url;
bool supportsExternalStream;
SubStreamModel({
required super.name,
required this.id,
required this.title,
required this.displayTitle,
required this.language,
required super.displayTitle,
required super.language,
this.url,
required super.codec,
required super.isDefault,
@ -327,8 +337,8 @@ class SubStreamModel extends StreamModel {
super.name = 'Off',
this.id = 'Off',
this.title = 'Off',
this.displayTitle = 'Off',
this.language = '',
super.displayTitle = 'Off',
super.language = '',
this.url = '',
super.codec = '',
super.isDefault = false,

View file

@ -147,34 +147,11 @@ class MovieModelMapper extends SubClassMapperBase<MovieModel> {
@override
final Function instantiate = _instantiate;
static MovieModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<MovieModel>(map);
}
static MovieModel fromJson(String json) {
return ensureInitialized().decodeJson<MovieModel>(json);
}
}
mixin MovieModelMappable {
String toJson() {
return MovieModelMapper.ensureInitialized()
.encodeJson<MovieModel>(this as MovieModel);
}
Map<String, dynamic> toMap() {
return MovieModelMapper.ensureInitialized()
.encodeMap<MovieModel>(this as MovieModel);
}
MovieModelCopyWith<MovieModel, MovieModel, MovieModel> get copyWith =>
_MovieModelCopyWithImpl(this as MovieModel, $identity, $identity);
@override
String toString() {
return MovieModelMapper.ensureInitialized()
.stringifyValue(this as MovieModel);
}
}
extension MovieModelValueCopy<$R, $Out>

View file

@ -1,3 +1,4 @@
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
@ -6,8 +7,6 @@ import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'overview_model.mapper.dart';
@MappableClass()
@ -76,7 +75,4 @@ class OverviewModel with OverviewModelMappable {
people: Person.peopleFromDto(item.people ?? [], ref),
);
}
factory OverviewModel.fromMap(Map<String, dynamic> map) => OverviewModelMapper.fromMap(map);
factory OverviewModel.fromJson(String json) => OverviewModelMapper.fromJson(json);
}

View file

@ -114,35 +114,12 @@ class OverviewModelMapper extends ClassMapperBase<OverviewModel> {
@override
final Function instantiate = _instantiate;
static OverviewModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<OverviewModel>(map);
}
static OverviewModel fromJson(String json) {
return ensureInitialized().decodeJson<OverviewModel>(json);
}
}
mixin OverviewModelMappable {
String toJson() {
return OverviewModelMapper.ensureInitialized()
.encodeJson<OverviewModel>(this as OverviewModel);
}
Map<String, dynamic> toMap() {
return OverviewModelMapper.ensureInitialized()
.encodeMap<OverviewModel>(this as OverviewModel);
}
OverviewModelCopyWith<OverviewModel, OverviewModel, OverviewModel>
get copyWith => _OverviewModelCopyWithImpl(
this as OverviewModel, $identity, $identity);
@override
String toString() {
return OverviewModelMapper.ensureInitialized()
.stringifyValue(this as OverviewModel);
}
}
extension OverviewModelValueCopy<$R, $Out>

View file

@ -124,34 +124,11 @@ class PersonModelMapper extends SubClassMapperBase<PersonModel> {
@override
final Function instantiate = _instantiate;
static PersonModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<PersonModel>(map);
}
static PersonModel fromJson(String json) {
return ensureInitialized().decodeJson<PersonModel>(json);
}
}
mixin PersonModelMappable {
String toJson() {
return PersonModelMapper.ensureInitialized()
.encodeJson<PersonModel>(this as PersonModel);
}
Map<String, dynamic> toMap() {
return PersonModelMapper.ensureInitialized()
.encodeMap<PersonModel>(this as PersonModel);
}
PersonModelCopyWith<PersonModel, PersonModel, PersonModel> get copyWith =>
_PersonModelCopyWithImpl(this as PersonModel, $identity, $identity);
@override
String toString() {
return PersonModelMapper.ensureInitialized()
.stringifyValue(this as PersonModel);
}
}
extension PersonModelValueCopy<$R, $Out>

View file

@ -108,35 +108,12 @@ class PhotoAlbumModelMapper extends SubClassMapperBase<PhotoAlbumModel> {
@override
final Function instantiate = _instantiate;
static PhotoAlbumModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<PhotoAlbumModel>(map);
}
static PhotoAlbumModel fromJson(String json) {
return ensureInitialized().decodeJson<PhotoAlbumModel>(json);
}
}
mixin PhotoAlbumModelMappable {
String toJson() {
return PhotoAlbumModelMapper.ensureInitialized()
.encodeJson<PhotoAlbumModel>(this as PhotoAlbumModel);
}
Map<String, dynamic> toMap() {
return PhotoAlbumModelMapper.ensureInitialized()
.encodeMap<PhotoAlbumModel>(this as PhotoAlbumModel);
}
PhotoAlbumModelCopyWith<PhotoAlbumModel, PhotoAlbumModel, PhotoAlbumModel>
get copyWith => _PhotoAlbumModelCopyWithImpl(
this as PhotoAlbumModel, $identity, $identity);
@override
String toString() {
return PhotoAlbumModelMapper.ensureInitialized()
.stringifyValue(this as PhotoAlbumModel);
}
}
extension PhotoAlbumModelValueCopy<$R, $Out>
@ -359,34 +336,11 @@ class PhotoModelMapper extends SubClassMapperBase<PhotoModel> {
@override
final Function instantiate = _instantiate;
static PhotoModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<PhotoModel>(map);
}
static PhotoModel fromJson(String json) {
return ensureInitialized().decodeJson<PhotoModel>(json);
}
}
mixin PhotoModelMappable {
String toJson() {
return PhotoModelMapper.ensureInitialized()
.encodeJson<PhotoModel>(this as PhotoModel);
}
Map<String, dynamic> toMap() {
return PhotoModelMapper.ensureInitialized()
.encodeMap<PhotoModel>(this as PhotoModel);
}
PhotoModelCopyWith<PhotoModel, PhotoModel, PhotoModel> get copyWith =>
_PhotoModelCopyWithImpl(this as PhotoModel, $identity, $identity);
@override
String toString() {
return PhotoModelMapper.ensureInitialized()
.stringifyValue(this as PhotoModel);
}
}
extension PhotoModelValueCopy<$R, $Out>

View file

@ -137,34 +137,11 @@ class SeasonModelMapper extends SubClassMapperBase<SeasonModel> {
@override
final Function instantiate = _instantiate;
static SeasonModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<SeasonModel>(map);
}
static SeasonModel fromJson(String json) {
return ensureInitialized().decodeJson<SeasonModel>(json);
}
}
mixin SeasonModelMappable {
String toJson() {
return SeasonModelMapper.ensureInitialized()
.encodeJson<SeasonModel>(this as SeasonModel);
}
Map<String, dynamic> toMap() {
return SeasonModelMapper.ensureInitialized()
.encodeMap<SeasonModel>(this as SeasonModel);
}
SeasonModelCopyWith<SeasonModel, SeasonModel, SeasonModel> get copyWith =>
_SeasonModelCopyWithImpl(this as SeasonModel, $identity, $identity);
@override
String toString() {
return SeasonModelMapper.ensureInitialized()
.stringifyValue(this as SeasonModel);
}
}
extension SeasonModelValueCopy<$R, $Out>

View file

@ -1,5 +1,6 @@
import 'package:fladder/screens/details_screens/series_detail_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto;
@ -9,8 +10,7 @@ import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/overview_model.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:fladder/screens/details_screens/series_detail_screen.dart';
part 'series_model.mapper.dart';

View file

@ -135,34 +135,11 @@ class SeriesModelMapper extends SubClassMapperBase<SeriesModel> {
@override
final Function instantiate = _instantiate;
static SeriesModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<SeriesModel>(map);
}
static SeriesModel fromJson(String json) {
return ensureInitialized().decodeJson<SeriesModel>(json);
}
}
mixin SeriesModelMappable {
String toJson() {
return SeriesModelMapper.ensureInitialized()
.encodeJson<SeriesModel>(this as SeriesModel);
}
Map<String, dynamic> toMap() {
return SeriesModelMapper.ensureInitialized()
.encodeMap<SeriesModel>(this as SeriesModel);
}
SeriesModelCopyWith<SeriesModel, SeriesModel, SeriesModel> get copyWith =>
_SeriesModelCopyWithImpl(this as SeriesModel, $identity, $identity);
@override
String toString() {
return SeriesModelMapper.ensureInitialized()
.stringifyValue(this as SeriesModel);
}
}
extension SeriesModelValueCopy<$R, $Out>

View file

@ -5,7 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'trick_play_model.freezed.dart';
part 'trick_play_model.g.dart';
@freezed
@Freezed(copyWith: true)
class TrickPlayModel with _$TrickPlayModel {
factory TrickPlayModel({
required int width,

View file

@ -225,36 +225,6 @@ class _$TrickPlayModelImpl extends _TrickPlayModel {
return 'TrickPlayModel(width: $width, height: $height, tileWidth: $tileWidth, tileHeight: $tileHeight, thumbnailCount: $thumbnailCount, interval: $interval, images: $images)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$TrickPlayModelImpl &&
(identical(other.width, width) || other.width == width) &&
(identical(other.height, height) || other.height == height) &&
(identical(other.tileWidth, tileWidth) ||
other.tileWidth == tileWidth) &&
(identical(other.tileHeight, tileHeight) ||
other.tileHeight == tileHeight) &&
(identical(other.thumbnailCount, thumbnailCount) ||
other.thumbnailCount == thumbnailCount) &&
(identical(other.interval, interval) ||
other.interval == interval) &&
const DeepCollectionEquality().equals(other._images, _images));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
width,
height,
tileWidth,
tileHeight,
thumbnailCount,
interval,
const DeepCollectionEquality().hash(_images));
/// Create a copy of TrickPlayModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)

View file

@ -14,7 +14,7 @@ import 'package:fladder/util/map_bool_helper.dart';
part 'library_filters_model.freezed.dart';
part 'library_filters_model.g.dart';
@freezed
@Freezed(copyWith: true)
class LibraryFiltersModel with _$LibraryFiltersModel {
const LibraryFiltersModel._();
@ -40,11 +40,16 @@ class LibraryFiltersModel with _$LibraryFiltersModel {
factory LibraryFiltersModel.fromJson(Map<String, dynamic> json) => _$LibraryFiltersModelFromJson(json);
factory LibraryFiltersModel.fromLibrarySearch(String name, LibrarySearchModel searchModel) {
factory LibraryFiltersModel.fromLibrarySearch(
String name,
LibrarySearchModel searchModel, {
bool? isFavourite,
String? id,
}) {
return LibraryFiltersModel._internal(
id: Xid().toString(),
id: id ?? Xid().toString(),
name: name,
isFavourite: false,
isFavourite: isFavourite ?? false,
ids: searchModel.views.included.map((e) => e.id).toList(),
genres: searchModel.genres,
filters: searchModel.filters,

View file

@ -436,59 +436,6 @@ class _$LibraryFiltersModelImpl extends _LibraryFiltersModel {
return 'LibraryFiltersModel._internal(id: $id, name: $name, isFavourite: $isFavourite, ids: $ids, genres: $genres, filters: $filters, studios: $studios, tags: $tags, years: $years, officialRatings: $officialRatings, types: $types, sortingOption: $sortingOption, sortOrder: $sortOrder, favourites: $favourites, hideEmptyShows: $hideEmptyShows, recursive: $recursive, groupBy: $groupBy)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$LibraryFiltersModelImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.isFavourite, isFavourite) ||
other.isFavourite == isFavourite) &&
const DeepCollectionEquality().equals(other._ids, _ids) &&
const DeepCollectionEquality().equals(other._genres, _genres) &&
const DeepCollectionEquality().equals(other._filters, _filters) &&
const DeepCollectionEquality().equals(other._studios, _studios) &&
const DeepCollectionEquality().equals(other._tags, _tags) &&
const DeepCollectionEquality().equals(other._years, _years) &&
const DeepCollectionEquality()
.equals(other._officialRatings, _officialRatings) &&
const DeepCollectionEquality().equals(other._types, _types) &&
(identical(other.sortingOption, sortingOption) ||
other.sortingOption == sortingOption) &&
(identical(other.sortOrder, sortOrder) ||
other.sortOrder == sortOrder) &&
(identical(other.favourites, favourites) ||
other.favourites == favourites) &&
(identical(other.hideEmptyShows, hideEmptyShows) ||
other.hideEmptyShows == hideEmptyShows) &&
(identical(other.recursive, recursive) ||
other.recursive == recursive) &&
(identical(other.groupBy, groupBy) || other.groupBy == groupBy));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
name,
isFavourite,
const DeepCollectionEquality().hash(_ids),
const DeepCollectionEquality().hash(_genres),
const DeepCollectionEquality().hash(_filters),
const DeepCollectionEquality().hash(_studios),
const DeepCollectionEquality().hash(_tags),
const DeepCollectionEquality().hash(_years),
const DeepCollectionEquality().hash(_officialRatings),
const DeepCollectionEquality().hash(_types),
sortingOption,
sortOrder,
favourites,
hideEmptyShows,
recursive,
groupBy);
/// Create a copy of LibraryFiltersModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)

View file

@ -93,7 +93,7 @@ const _$FladderItemTypeEnumMap = {
};
const _$SortingOptionsEnumMap = {
SortingOptions.name: 'name',
SortingOptions.sortName: 'sortName',
SortingOptions.communityRating: 'communityRating',
SortingOptions.parentalRating: 'parentalRating',
SortingOptions.dateAdded: 'dateAdded',

View file

@ -77,7 +77,7 @@ class LibrarySearchModel with LibrarySearchModelMappable {
FladderItemType.video: true,
},
this.favourites = false,
this.sortingOption = SortingOptions.name,
this.sortingOption = SortingOptions.sortName,
this.sortOrder = SortingOrder.ascending,
this.hideEmptyShows = true,
this.recursive = false,

View file

@ -94,7 +94,7 @@ class LibrarySearchModelMapper extends ClassMapperBase<LibrarySearchModel> {
v.sortingOption;
static const Field<LibrarySearchModel, SortingOptions> _f$sortingOption =
Field('sortingOption', _$sortingOption,
opt: true, def: SortingOptions.name);
opt: true, def: SortingOptions.sortName);
static SortingOrder _$sortOrder(LibrarySearchModel v) => v.sortOrder;
static const Field<LibrarySearchModel, SortingOrder> _f$sortOrder =
Field('sortOrder', _$sortOrder, opt: true, def: SortingOrder.ascending);
@ -177,36 +177,13 @@ class LibrarySearchModelMapper extends ClassMapperBase<LibrarySearchModel> {
@override
final Function instantiate = _instantiate;
static LibrarySearchModel fromMap(Map<String, dynamic> map) {
return ensureInitialized().decodeMap<LibrarySearchModel>(map);
}
static LibrarySearchModel fromJson(String json) {
return ensureInitialized().decodeJson<LibrarySearchModel>(json);
}
}
mixin LibrarySearchModelMappable {
String toJson() {
return LibrarySearchModelMapper.ensureInitialized()
.encodeJson<LibrarySearchModel>(this as LibrarySearchModel);
}
Map<String, dynamic> toMap() {
return LibrarySearchModelMapper.ensureInitialized()
.encodeMap<LibrarySearchModel>(this as LibrarySearchModel);
}
LibrarySearchModelCopyWith<LibrarySearchModel, LibrarySearchModel,
LibrarySearchModel>
get copyWith => _LibrarySearchModelCopyWithImpl(
this as LibrarySearchModel, $identity, $identity);
@override
String toString() {
return LibrarySearchModelMapper.ensureInitialized()
.stringifyValue(this as LibrarySearchModel);
}
}
extension LibrarySearchModelValueCopy<$R, $Out>

View file

@ -1,14 +1,13 @@
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:flutter/material.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/util/localization_helper.dart';
enum SortingOptions {
name([ItemSortBy.name]),
sortName([ItemSortBy.sortname]),
communityRating([ItemSortBy.communityrating]),
// criticsRating([ItemSortBy.criticrating]),
parentalRating([ItemSortBy.officialrating]),
dateAdded([ItemSortBy.datecreated]),
dateLastContentAdded([ItemSortBy.datelastcontentadded]),
@ -23,10 +22,10 @@ enum SortingOptions {
const SortingOptions(this.value);
final List<ItemSortBy> value;
List<ItemSortBy> get toSortBy => [...value, ItemSortBy.name];
List<ItemSortBy> get toSortBy => [...value, ItemSortBy.sortname];
String label(BuildContext context) => switch (this) {
name => context.localized.name,
sortName => context.localized.name,
communityRating => context.localized.communityRating,
parentalRating => context.localized.parentalRating,
dateAdded => context.localized.dateAdded,

View file

@ -1,5 +1,7 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:chopper/chopper.dart';
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -16,6 +18,7 @@ import 'package:fladder/models/items/series_model.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/models/playback/direct_playback_model.dart';
import 'package:fladder/models/playback/offline_playback_model.dart';
import 'package:fladder/models/playback/playback_options_dialogue.dart';
import 'package:fladder/models/playback/transcode_playback_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/models/video_stream_model.dart';
@ -31,6 +34,7 @@ import 'package:fladder/util/bitrate_helper.dart';
import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/list_extensions.dart';
import 'package:fladder/util/map_bool_helper.dart';
import 'package:fladder/util/streams_selection.dart';
import 'package:fladder/wrappers/media_control_wrapper.dart';
class Media {
@ -48,10 +52,10 @@ extension PlaybackModelExtension on PlaybackModel? {
AudioStreamModel? get defaultAudioStream =>
this?.audioStreams?.firstWhereOrNull((element) => element.index == this?.mediaStreams?.defaultAudioStreamIndex);
String? get label => switch (this) {
DirectPlaybackModel _ => PlaybackType.directStream.name,
TranscodePlaybackModel _ => PlaybackType.transcode.name,
OfflinePlaybackModel _ => PlaybackType.offline.name,
String? label(BuildContext context) => switch (this) {
DirectPlaybackModel _ => PlaybackType.directStream.name(context),
TranscodePlaybackModel _ => PlaybackType.transcode.name(context),
OfflinePlaybackModel _ => PlaybackType.offline.name(context),
_ => null
};
}
@ -117,13 +121,14 @@ class PlaybackModelHelper {
ref.read(videoPlayerProvider).pause();
ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(buffering: true));
final currentModel = ref.read(playBackModel);
final newModel = (await createServerPlaybackModel(
newItem,
final newModel = (await createPlaybackModel(
null,
newItem,
oldModel: currentModel,
)) ??
await createOfflinePlaybackModel(
await _createOfflinePlaybackModel(
newItem,
null,
ref.read(syncProvider.notifier).getSyncedItem(newItem),
oldModel: currentModel,
);
@ -132,8 +137,9 @@ class PlaybackModelHelper {
return newModel;
}
Future<OfflinePlaybackModel?> createOfflinePlaybackModel(
Future<OfflinePlaybackModel?> _createOfflinePlaybackModel(
ItemBaseModel item,
MediaStreamsModel? streamModel,
SyncedItem? syncedItem, {
PlaybackModel? oldModel,
}) async {
@ -156,53 +162,121 @@ class PlaybackModelHelper {
);
}
Future<EpisodeModel?> getNextUpEpisode(String itemId) async {
final response = await api.showsNextUpGet(parentId: itemId, fields: [ItemFields.overview]);
final episode = response.body?.items?.firstOrNull;
if (episode == null) {
return null;
Future<PlaybackModel?> createPlaybackModel(
BuildContext? context,
ItemBaseModel? item, {
PlaybackModel? oldModel,
List<ItemBaseModel>? libraryQueue,
bool showPlaybackOptions = false,
Duration? startPosition,
}) async {
if (item == null) return null;
final userId = ref.read(userProvider)?.id;
if (userId?.isEmpty == true) return null;
final queue = oldModel?.queue ?? libraryQueue ?? await collectQueue(item);
final firstItemToPlay = switch (item) {
SeriesModel _ || SeasonModel _ => (queue.whereType<EpisodeModel>().toList().nextUp),
_ => item,
};
if (firstItemToPlay == null) return null;
final fullItem = (await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id)).body;
if (fullItem == null) return null;
SyncedItem? syncedItem = ref.read(syncProvider.notifier).getSyncedItem(fullItem);
final firstItemIsSynced = syncedItem != null && syncedItem.status == SyncStatus.complete;
final options = {
PlaybackType.directStream,
PlaybackType.transcode,
if (firstItemIsSynced) PlaybackType.offline,
};
if ((showPlaybackOptions || firstItemIsSynced) && context != null) {
final playbackType = await showPlaybackTypeSelection(
context: context,
options: options,
);
if (!context.mounted) return null;
return switch (playbackType) {
PlaybackType.directStream || PlaybackType.transcode => await _createServerPlaybackModel(
fullItem,
item.streamModel,
playbackType,
oldModel: oldModel,
libraryQueue: queue,
startPosition: startPosition,
),
PlaybackType.offline => await _createOfflinePlaybackModel(
fullItem,
item.streamModel,
syncedItem,
),
null => null
};
} else {
return EpisodeModel.fromBaseDto(episode, ref);
return (await _createServerPlaybackModel(
fullItem,
item.streamModel,
PlaybackType.directStream,
startPosition: startPosition,
oldModel: oldModel,
libraryQueue: queue,
)) ??
await _createOfflinePlaybackModel(
fullItem,
item.streamModel,
syncedItem,
);
}
}
Future<PlaybackModel?> createServerPlaybackModel(
ItemBaseModel? item,
Future<PlaybackModel?> _createServerPlaybackModel(
ItemBaseModel item,
MediaStreamsModel? streamModel,
PlaybackType? type, {
PlaybackModel? oldModel,
List<ItemBaseModel>? libraryQueue,
required List<ItemBaseModel> libraryQueue,
Duration? startPosition,
}) async {
try {
if (item == null) return null;
final userId = ref.read(userProvider)?.id;
if (userId?.isEmpty == true) return null;
final queue = oldModel?.queue ?? libraryQueue ?? await collectQueue(item);
final firstItemToPlay = switch (item) {
SeriesModel _ || SeasonModel _ => (await getNextUpEpisode(item.id) ?? queue.first),
_ => item,
};
final fullItem = await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id);
final newStreamModel = streamModel ?? item.streamModel;
Map<Bitrate, bool> qualityOptions = getVideoQualityOptions(
VideoQualitySettings(
maxBitRate: ref.read(videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate)),
videoBitRate: firstItemToPlay.streamModel?.videoStreams.firstOrNull?.bitRate ?? 0,
videoCodec: firstItemToPlay.streamModel?.videoStreams.firstOrNull?.codec,
videoBitRate: newStreamModel?.videoStreams.firstOrNull?.bitRate ?? 0,
videoCodec: newStreamModel?.videoStreams.firstOrNull?.codec,
),
);
final streamModel = firstItemToPlay.streamModel;
final audioStreamIndex = selectAudioStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
oldModel?.mediaStreams?.currentAudioStream,
newStreamModel?.audioStreams,
newStreamModel?.defaultAudioStreamIndex);
final subStreamIndex = selectSubStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
oldModel?.mediaStreams?.currentSubStream,
newStreamModel?.subStreams,
newStreamModel?.defaultSubStreamIndex);
final Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
itemId: firstItemToPlay.id,
itemId: item.id,
body: PlaybackInfoDto(
startTimeTicks: startPosition?.toRuntimeTicks,
audioStreamIndex: streamModel?.defaultAudioStreamIndex,
subtitleStreamIndex: streamModel?.defaultSubStreamIndex,
audioStreamIndex: audioStreamIndex,
subtitleStreamIndex: subStreamIndex,
enableTranscoding: true,
autoOpenLiveStream: true,
deviceProfile: ref.read(videoProfileProvider),
@ -210,7 +284,7 @@ class PlaybackModelHelper {
enableDirectPlay: type != PlaybackType.transcode,
enableDirectStream: type != PlaybackType.transcode,
maxStreamingBitrate: qualityOptions.enabledFirst.keys.firstOrNull?.bitRate,
mediaSourceId: streamModel?.currentVersionStream?.id,
mediaSourceId: newStreamModel?.currentVersionStream?.id,
),
);
@ -218,18 +292,18 @@ class PlaybackModelHelper {
if (playbackInfo == null) return null;
final mediaSource = playbackInfo.mediaSources?[streamModel?.versionStreamIndex ?? 0];
final mediaSource = playbackInfo.mediaSources?[newStreamModel?.versionStreamIndex ?? 0];
if (mediaSource == null) return null;
final mediaStreamsWithUrls = MediaStreamsModel.fromMediaStreamsList(playbackInfo.mediaSources, ref).copyWith(
defaultAudioStreamIndex: streamModel?.defaultAudioStreamIndex,
defaultSubStreamIndex: streamModel?.defaultSubStreamIndex,
defaultAudioStreamIndex: audioStreamIndex,
defaultSubStreamIndex: subStreamIndex,
);
final mediaSegments = await api.mediaSegmentsGet(id: item.id);
final trickPlay = (await api.getTrickPlay(item: fullItem.body, ref: ref))?.body;
final chapters = fullItem.body?.overview.chapters ?? [];
final trickPlay = (await api.getTrickPlay(item: item, ref: ref))?.body;
final chapters = item.overview.chapters ?? [];
final mediaPath = isValidVideoUrl(mediaSource.path ?? "");
@ -252,8 +326,8 @@ class PlaybackModelHelper {
final playbackUrl = joinAll([ref.read(userProvider)!.server, "Videos", mediaSource.id!, "stream?$params"]);
return DirectPlaybackModel(
item: fullItem.body ?? item,
queue: queue,
item: item,
queue: libraryQueue,
mediaSegments: mediaSegments?.body,
chapters: chapters,
playbackInfo: playbackInfo,
@ -264,8 +338,8 @@ class PlaybackModelHelper {
);
} else if ((mediaSource.supportsTranscoding ?? false) && mediaSource.transcodingUrl != null) {
return TranscodePlaybackModel(
item: fullItem.body ?? item,
queue: queue,
item: item,
queue: libraryQueue,
mediaSegments: mediaSegments?.body,
chapters: chapters,
trickPlay: trickPlay,
@ -328,8 +402,16 @@ class PlaybackModelHelper {
final currentPosition = ref.read(mediaPlaybackProvider.select((value) => value.position));
final audioIndex = playbackModel.mediaStreams?.defaultAudioStreamIndex;
final subIndex = playbackModel.mediaStreams?.defaultSubStreamIndex;
final audioIndex = selectAudioStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberAudioSelections ?? true)),
playbackModel.mediaStreams?.currentAudioStream,
playbackModel.audioStreams,
playbackModel.mediaStreams?.defaultAudioStreamIndex);
final subIndex = selectSubStream(
ref.read(userProvider.select((value) => value?.userConfiguration?.rememberSubtitleSelections ?? true)),
playbackModel.mediaStreams?.currentSubStream,
playbackModel.subStreams,
playbackModel.mediaStreams?.defaultSubStreamIndex);
Response<PlaybackInfoResponse> response = await api.itemsItemIdPlaybackInfoPost(
itemId: item.id,

View file

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:fladder/models/video_stream_model.dart';
import 'package:fladder/screens/shared/adaptive_dialog.dart';
import 'package:fladder/util/localization_helper.dart';
Future<PlaybackType?> showPlaybackTypeSelection({
required BuildContext context,
required Set<PlaybackType> options,
}) async {
PlaybackType? playbackType;
await showDialogAdaptive(
context: context,
builder: (context) {
return PlaybackDialogue(
options: options,
onClose: (type) {
playbackType = type;
Navigator.of(context).pop();
},
);
},
);
return playbackType;
}
class PlaybackDialogue extends StatelessWidget {
final Set<PlaybackType> options;
final Function(PlaybackType type) onClose;
const PlaybackDialogue({required this.options, required this.onClose, super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16).add(const EdgeInsets.only(top: 16, bottom: 8)),
child: Text(
context.localized.playbackType,
style: Theme.of(context).textTheme.titleLarge,
),
),
const Divider(),
...options.map((type) => ListTile(
title: Text(type.name(context)),
leading: Icon(type.icon),
onTap: () {
onClose(type);
},
))
],
);
}
}

View file

@ -1,20 +1,66 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/util/localization_helper.dart';
sealed class NameSwitch {
const NameSwitch();
String label(BuildContext context);
}
class NextUp extends NameSwitch {
const NextUp();
@override
String label(BuildContext context) => context.localized.nextUp;
}
class Latest extends NameSwitch {
const Latest();
@override
String label(BuildContext context) => context.localized.latest;
}
class Other extends NameSwitch {
final String customLabel;
const Other(this.customLabel);
@override
String label(BuildContext context) => customLabel;
}
extension RecommendationTypeExtenstion on RecommendationType {
String label(BuildContext context) => switch (this) {
RecommendationType.similartorecentlyplayed => context.localized.similarToRecentlyPlayed,
RecommendationType.similartolikeditem => context.localized.similarToLikedItem,
RecommendationType.hasdirectorfromrecentlyplayed => context.localized.hasDirectorFromRecentlyPlayed,
RecommendationType.hasactorfromrecentlyplayed => context.localized.hasActorFromRecentlyPlayed,
RecommendationType.haslikeddirector => context.localized.hasLikedDirector,
RecommendationType.haslikedactor => context.localized.hasLikedActor,
_ => "",
};
}
class RecommendedModel {
final String name;
final NameSwitch name;
final List<ItemBaseModel> posters;
final String type;
final RecommendationType? type;
RecommendedModel({
required this.name,
required this.posters,
required this.type,
this.type,
});
RecommendedModel copyWith({
String? name,
NameSwitch? name,
List<ItemBaseModel>? posters,
String? type,
RecommendationType? type,
}) {
return RecommendedModel(
name: name ?? this.name,
@ -22,4 +68,12 @@ class RecommendedModel {
type: type ?? this.type,
);
}
factory RecommendedModel.fromBaseDto(RecommendationDto e, Ref ref) {
return RecommendedModel(
name: Other(e.baselineItemName ?? ""),
posters: e.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [],
type: e.recommendationType,
);
}
}

View file

@ -0,0 +1,19 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'arguments_model.freezed.dart';
@freezed
class ArgumentsModel with _$ArgumentsModel {
const ArgumentsModel._();
factory ArgumentsModel({
@Default(false) bool htpcMode,
}) = _ArgumentsModel;
factory ArgumentsModel.fromArguments(List<String> arguments) {
arguments = arguments.map((e) => e.trim()).toList();
return ArgumentsModel(
htpcMode: arguments.contains('--htpc'),
);
}
}

View file

@ -0,0 +1,43 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'arguments_model.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$ArgumentsModel {
bool get htpcMode => throw _privateConstructorUsedError;
}
/// @nodoc
class _$ArgumentsModelImpl extends _ArgumentsModel {
_$ArgumentsModelImpl({this.htpcMode = false}) : super._();
@override
@JsonKey()
final bool htpcMode;
@override
String toString() {
return 'ArgumentsModel(htpcMode: $htpcMode)';
}
}
abstract class _ArgumentsModel extends ArgumentsModel {
factory _ArgumentsModel({final bool htpcMode}) = _$ArgumentsModelImpl;
_ArgumentsModel._() : super._();
@override
bool get htpcMode;
}

View file

@ -11,7 +11,7 @@ import 'package:fladder/util/custom_color_themes.dart';
part 'client_settings_model.freezed.dart';
part 'client_settings_model.g.dart';
@freezed
@Freezed(copyWith: true)
class ClientSettingsModel with _$ClientSettingsModel {
const ClientSettingsModel._();
factory ClientSettingsModel({
@ -32,7 +32,11 @@ class ClientSettingsModel with _$ClientSettingsModel {
@Default(false) bool mouseDragSupport,
@Default(true) bool requireWifi,
@Default(false) bool showAllCollectionTypes,
@Default(DynamicSchemeVariant.tonalSpot) DynamicSchemeVariant schemeVariant,
@Default(2) int maxConcurrentDownloads,
@Default(DynamicSchemeVariant.rainbow) DynamicSchemeVariant schemeVariant,
@Default(true) bool backgroundPosters,
@Default(true) bool checkForUpdates,
String? lastViewedUpdate,
int? libraryPageSize,
}) = _ClientSettingsModel;

View file

@ -38,7 +38,11 @@ mixin _$ClientSettingsModel {
bool get mouseDragSupport => throw _privateConstructorUsedError;
bool get requireWifi => throw _privateConstructorUsedError;
bool get showAllCollectionTypes => throw _privateConstructorUsedError;
int get maxConcurrentDownloads => throw _privateConstructorUsedError;
DynamicSchemeVariant get schemeVariant => throw _privateConstructorUsedError;
bool get backgroundPosters => throw _privateConstructorUsedError;
bool get checkForUpdates => throw _privateConstructorUsedError;
String? get lastViewedUpdate => throw _privateConstructorUsedError;
int? get libraryPageSize => throw _privateConstructorUsedError;
/// Serializes this ClientSettingsModel to a JSON map.
@ -75,7 +79,11 @@ abstract class $ClientSettingsModelCopyWith<$Res> {
bool mouseDragSupport,
bool requireWifi,
bool showAllCollectionTypes,
int maxConcurrentDownloads,
DynamicSchemeVariant schemeVariant,
bool backgroundPosters,
bool checkForUpdates,
String? lastViewedUpdate,
int? libraryPageSize});
}
@ -111,7 +119,11 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
Object? mouseDragSupport = null,
Object? requireWifi = null,
Object? showAllCollectionTypes = null,
Object? maxConcurrentDownloads = null,
Object? schemeVariant = null,
Object? backgroundPosters = null,
Object? checkForUpdates = null,
Object? lastViewedUpdate = freezed,
Object? libraryPageSize = freezed,
}) {
return _then(_value.copyWith(
@ -183,10 +195,26 @@ class _$ClientSettingsModelCopyWithImpl<$Res, $Val extends ClientSettingsModel>
? _value.showAllCollectionTypes
: showAllCollectionTypes // ignore: cast_nullable_to_non_nullable
as bool,
maxConcurrentDownloads: null == maxConcurrentDownloads
? _value.maxConcurrentDownloads
: maxConcurrentDownloads // ignore: cast_nullable_to_non_nullable
as int,
schemeVariant: null == schemeVariant
? _value.schemeVariant
: schemeVariant // ignore: cast_nullable_to_non_nullable
as DynamicSchemeVariant,
backgroundPosters: null == backgroundPosters
? _value.backgroundPosters
: backgroundPosters // ignore: cast_nullable_to_non_nullable
as bool,
checkForUpdates: null == checkForUpdates
? _value.checkForUpdates
: checkForUpdates // ignore: cast_nullable_to_non_nullable
as bool,
lastViewedUpdate: freezed == lastViewedUpdate
? _value.lastViewedUpdate
: lastViewedUpdate // ignore: cast_nullable_to_non_nullable
as String?,
libraryPageSize: freezed == libraryPageSize
? _value.libraryPageSize
: libraryPageSize // ignore: cast_nullable_to_non_nullable
@ -221,7 +249,11 @@ abstract class _$$ClientSettingsModelImplCopyWith<$Res>
bool mouseDragSupport,
bool requireWifi,
bool showAllCollectionTypes,
int maxConcurrentDownloads,
DynamicSchemeVariant schemeVariant,
bool backgroundPosters,
bool checkForUpdates,
String? lastViewedUpdate,
int? libraryPageSize});
}
@ -255,7 +287,11 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
Object? mouseDragSupport = null,
Object? requireWifi = null,
Object? showAllCollectionTypes = null,
Object? maxConcurrentDownloads = null,
Object? schemeVariant = null,
Object? backgroundPosters = null,
Object? checkForUpdates = null,
Object? lastViewedUpdate = freezed,
Object? libraryPageSize = freezed,
}) {
return _then(_$ClientSettingsModelImpl(
@ -327,10 +363,26 @@ class __$$ClientSettingsModelImplCopyWithImpl<$Res>
? _value.showAllCollectionTypes
: showAllCollectionTypes // ignore: cast_nullable_to_non_nullable
as bool,
maxConcurrentDownloads: null == maxConcurrentDownloads
? _value.maxConcurrentDownloads
: maxConcurrentDownloads // ignore: cast_nullable_to_non_nullable
as int,
schemeVariant: null == schemeVariant
? _value.schemeVariant
: schemeVariant // ignore: cast_nullable_to_non_nullable
as DynamicSchemeVariant,
backgroundPosters: null == backgroundPosters
? _value.backgroundPosters
: backgroundPosters // ignore: cast_nullable_to_non_nullable
as bool,
checkForUpdates: null == checkForUpdates
? _value.checkForUpdates
: checkForUpdates // ignore: cast_nullable_to_non_nullable
as bool,
lastViewedUpdate: freezed == lastViewedUpdate
? _value.lastViewedUpdate
: lastViewedUpdate // ignore: cast_nullable_to_non_nullable
as String?,
libraryPageSize: freezed == libraryPageSize
? _value.libraryPageSize
: libraryPageSize // ignore: cast_nullable_to_non_nullable
@ -361,7 +413,11 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
this.mouseDragSupport = false,
this.requireWifi = true,
this.showAllCollectionTypes = false,
this.schemeVariant = DynamicSchemeVariant.tonalSpot,
this.maxConcurrentDownloads = 2,
this.schemeVariant = DynamicSchemeVariant.rainbow,
this.backgroundPosters = true,
this.checkForUpdates = true,
this.lastViewedUpdate,
this.libraryPageSize})
: super._();
@ -418,13 +474,24 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
final bool showAllCollectionTypes;
@override
@JsonKey()
final int maxConcurrentDownloads;
@override
@JsonKey()
final DynamicSchemeVariant schemeVariant;
@override
@JsonKey()
final bool backgroundPosters;
@override
@JsonKey()
final bool checkForUpdates;
@override
final String? lastViewedUpdate;
@override
final int? libraryPageSize;
@override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, schemeVariant: $schemeVariant, libraryPageSize: $libraryPageSize)';
return 'ClientSettingsModel(syncPath: $syncPath, position: $position, size: $size, timeOut: $timeOut, nextUpDateCutoff: $nextUpDateCutoff, themeMode: $themeMode, themeColor: $themeColor, amoledBlack: $amoledBlack, blurPlaceHolders: $blurPlaceHolders, blurUpcomingEpisodes: $blurUpcomingEpisodes, selectedLocale: $selectedLocale, enableMediaKeys: $enableMediaKeys, posterSize: $posterSize, pinchPosterZoom: $pinchPosterZoom, mouseDragSupport: $mouseDragSupport, requireWifi: $requireWifi, showAllCollectionTypes: $showAllCollectionTypes, maxConcurrentDownloads: $maxConcurrentDownloads, schemeVariant: $schemeVariant, backgroundPosters: $backgroundPosters, checkForUpdates: $checkForUpdates, lastViewedUpdate: $lastViewedUpdate, libraryPageSize: $libraryPageSize)';
}
@override
@ -450,78 +517,15 @@ class _$ClientSettingsModelImpl extends _ClientSettingsModel
..add(DiagnosticsProperty('requireWifi', requireWifi))
..add(
DiagnosticsProperty('showAllCollectionTypes', showAllCollectionTypes))
..add(
DiagnosticsProperty('maxConcurrentDownloads', maxConcurrentDownloads))
..add(DiagnosticsProperty('schemeVariant', schemeVariant))
..add(DiagnosticsProperty('backgroundPosters', backgroundPosters))
..add(DiagnosticsProperty('checkForUpdates', checkForUpdates))
..add(DiagnosticsProperty('lastViewedUpdate', lastViewedUpdate))
..add(DiagnosticsProperty('libraryPageSize', libraryPageSize));
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ClientSettingsModelImpl &&
(identical(other.syncPath, syncPath) ||
other.syncPath == syncPath) &&
(identical(other.position, position) ||
other.position == position) &&
(identical(other.size, size) || other.size == size) &&
(identical(other.timeOut, timeOut) || other.timeOut == timeOut) &&
(identical(other.nextUpDateCutoff, nextUpDateCutoff) ||
other.nextUpDateCutoff == nextUpDateCutoff) &&
(identical(other.themeMode, themeMode) ||
other.themeMode == themeMode) &&
(identical(other.themeColor, themeColor) ||
other.themeColor == themeColor) &&
(identical(other.amoledBlack, amoledBlack) ||
other.amoledBlack == amoledBlack) &&
(identical(other.blurPlaceHolders, blurPlaceHolders) ||
other.blurPlaceHolders == blurPlaceHolders) &&
(identical(other.blurUpcomingEpisodes, blurUpcomingEpisodes) ||
other.blurUpcomingEpisodes == blurUpcomingEpisodes) &&
(identical(other.selectedLocale, selectedLocale) ||
other.selectedLocale == selectedLocale) &&
(identical(other.enableMediaKeys, enableMediaKeys) ||
other.enableMediaKeys == enableMediaKeys) &&
(identical(other.posterSize, posterSize) ||
other.posterSize == posterSize) &&
(identical(other.pinchPosterZoom, pinchPosterZoom) ||
other.pinchPosterZoom == pinchPosterZoom) &&
(identical(other.mouseDragSupport, mouseDragSupport) ||
other.mouseDragSupport == mouseDragSupport) &&
(identical(other.requireWifi, requireWifi) ||
other.requireWifi == requireWifi) &&
(identical(other.showAllCollectionTypes, showAllCollectionTypes) ||
other.showAllCollectionTypes == showAllCollectionTypes) &&
(identical(other.schemeVariant, schemeVariant) ||
other.schemeVariant == schemeVariant) &&
(identical(other.libraryPageSize, libraryPageSize) ||
other.libraryPageSize == libraryPageSize));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hashAll([
runtimeType,
syncPath,
position,
size,
timeOut,
nextUpDateCutoff,
themeMode,
themeColor,
amoledBlack,
blurPlaceHolders,
blurUpcomingEpisodes,
selectedLocale,
enableMediaKeys,
posterSize,
pinchPosterZoom,
mouseDragSupport,
requireWifi,
showAllCollectionTypes,
schemeVariant,
libraryPageSize
]);
/// Create a copy of ClientSettingsModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@ -558,7 +562,11 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
final bool mouseDragSupport,
final bool requireWifi,
final bool showAllCollectionTypes,
final int maxConcurrentDownloads,
final DynamicSchemeVariant schemeVariant,
final bool backgroundPosters,
final bool checkForUpdates,
final String? lastViewedUpdate,
final int? libraryPageSize}) = _$ClientSettingsModelImpl;
_ClientSettingsModel._() : super._();
@ -601,8 +609,16 @@ abstract class _ClientSettingsModel extends ClientSettingsModel {
@override
bool get showAllCollectionTypes;
@override
int get maxConcurrentDownloads;
@override
DynamicSchemeVariant get schemeVariant;
@override
bool get backgroundPosters;
@override
bool get checkForUpdates;
@override
String? get lastViewedUpdate;
@override
int? get libraryPageSize;
/// Create a copy of ClientSettingsModel

View file

@ -36,9 +36,14 @@ _$ClientSettingsModelImpl _$$ClientSettingsModelImplFromJson(
mouseDragSupport: json['mouseDragSupport'] as bool? ?? false,
requireWifi: json['requireWifi'] as bool? ?? true,
showAllCollectionTypes: json['showAllCollectionTypes'] as bool? ?? false,
maxConcurrentDownloads:
(json['maxConcurrentDownloads'] as num?)?.toInt() ?? 2,
schemeVariant: $enumDecodeNullable(
_$DynamicSchemeVariantEnumMap, json['schemeVariant']) ??
DynamicSchemeVariant.tonalSpot,
DynamicSchemeVariant.rainbow,
backgroundPosters: json['backgroundPosters'] as bool? ?? true,
checkForUpdates: json['checkForUpdates'] as bool? ?? true,
lastViewedUpdate: json['lastViewedUpdate'] as String?,
libraryPageSize: (json['libraryPageSize'] as num?)?.toInt(),
);
@ -62,7 +67,11 @@ Map<String, dynamic> _$$ClientSettingsModelImplToJson(
'mouseDragSupport': instance.mouseDragSupport,
'requireWifi': instance.requireWifi,
'showAllCollectionTypes': instance.showAllCollectionTypes,
'maxConcurrentDownloads': instance.maxConcurrentDownloads,
'schemeVariant': _$DynamicSchemeVariantEnumMap[instance.schemeVariant]!,
'backgroundPosters': instance.backgroundPosters,
'checkForUpdates': instance.checkForUpdates,
'lastViewedUpdate': instance.lastViewedUpdate,
'libraryPageSize': instance.libraryPageSize,
};

View file

@ -2,12 +2,13 @@ import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/localization_helper.dart';
part 'home_settings_model.freezed.dart';
part 'home_settings_model.g.dart';
@freezed
@Freezed(copyWith: true)
class HomeSettingsModel with _$HomeSettingsModel {
factory HomeSettingsModel({
@Default({...LayoutMode.values}) Set<LayoutMode> screenLayouts,
@ -36,42 +37,6 @@ T selectAvailableOrSmaller<T>(T value, Set<T> availableOptions, List<T> allOptio
return availableOptions.first;
}
enum ViewSize {
phone,
tablet,
desktop;
const ViewSize();
String label(BuildContext context) => switch (this) {
ViewSize.phone => context.localized.phone,
ViewSize.tablet => context.localized.tablet,
ViewSize.desktop => context.localized.desktop,
};
bool operator >(ViewSize other) => index > other.index;
bool operator >=(ViewSize other) => index >= other.index;
bool operator <(ViewSize other) => index < other.index;
bool operator <=(ViewSize other) => index <= other.index;
}
enum LayoutMode {
single,
dual;
const LayoutMode();
String label(BuildContext context) => switch (this) {
LayoutMode.single => context.localized.layoutModeSingle,
LayoutMode.dual => context.localized.layoutModeDual,
};
bool operator >(ViewSize other) => index > other.index;
bool operator >=(ViewSize other) => index >= other.index;
bool operator <(ViewSize other) => index < other.index;
bool operator <=(ViewSize other) => index <= other.index;
}
enum HomeBanner {
hide,
carousel,

View file

@ -205,32 +205,6 @@ class _$HomeSettingsModelImpl implements _HomeSettingsModel {
return 'HomeSettingsModel(screenLayouts: $screenLayouts, layoutStates: $layoutStates, homeBanner: $homeBanner, carouselSettings: $carouselSettings, nextUp: $nextUp)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$HomeSettingsModelImpl &&
const DeepCollectionEquality()
.equals(other._screenLayouts, _screenLayouts) &&
const DeepCollectionEquality()
.equals(other._layoutStates, _layoutStates) &&
(identical(other.homeBanner, homeBanner) ||
other.homeBanner == homeBanner) &&
(identical(other.carouselSettings, carouselSettings) ||
other.carouselSettings == carouselSettings) &&
(identical(other.nextUp, nextUp) || other.nextUp == nextUp));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_screenLayouts),
const DeepCollectionEquality().hash(_layoutStates),
homeBanner,
carouselSettings,
nextUp);
/// Create a copy of HomeSettingsModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)

View file

@ -12,7 +12,7 @@ import 'package:fladder/util/localization_helper.dart';
part 'video_player_settings.freezed.dart';
part 'video_player_settings.g.dart';
@freezed
@Freezed(copyWith: true)
class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
const VideoPlayerSettingsModel._();
@ -43,7 +43,10 @@ class VideoPlayerSettingsModel with _$VideoPlayerSettingsModel {
PlayerOptions get wantedPlayer => playerOptions ?? PlayerOptions.platformDefaults;
bool playerSame(VideoPlayerSettingsModel other) {
return other.hardwareAccel == hardwareAccel && other.useLibass == useLibass && other.bufferSize == bufferSize && other.wantedPlayer == wantedPlayer;
return other.hardwareAccel == hardwareAccel &&
other.useLibass == useLibass &&
other.bufferSize == bufferSize &&
other.wantedPlayer == wantedPlayer;
}
@override

View file

@ -4,17 +4,17 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:path/path.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/models/syncing/i_synced_item.dart';
@ -24,7 +24,7 @@ import 'package:fladder/util/localization_helper.dart';
part 'sync_item.freezed.dart';
@freezed
@Freezed(copyWith: true)
class SyncedItem with _$SyncedItem {
const SyncedItem._();

View file

@ -63,7 +63,6 @@ abstract class $SyncedItemCopyWith<$Res> {
List<SubStreamModel> subtitles,
@UserDataJsonSerializer() UserData? userData});
$MediaSegmentsModelCopyWith<$Res>? get mediaSegments;
$TrickPlayModelCopyWith<$Res>? get fTrickPlayModel;
}
@ -162,20 +161,6 @@ class _$SyncedItemCopyWithImpl<$Res, $Val extends SyncedItem>
) as $Val);
}
/// Create a copy of SyncedItem
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$MediaSegmentsModelCopyWith<$Res>? get mediaSegments {
if (_value.mediaSegments == null) {
return null;
}
return $MediaSegmentsModelCopyWith<$Res>(_value.mediaSegments!, (value) {
return _then(_value.copyWith(mediaSegments: value) as $Val);
});
}
/// Create a copy of SyncedItem
/// with the given fields replaced by the non-null parameter values.
@override
@ -216,8 +201,6 @@ abstract class _$$SyncItemImplCopyWith<$Res>
List<SubStreamModel> subtitles,
@UserDataJsonSerializer() UserData? userData});
@override
$MediaSegmentsModelCopyWith<$Res>? get mediaSegments;
@override
$TrickPlayModelCopyWith<$Res>? get fTrickPlayModel;
}
@ -392,57 +375,6 @@ class _$SyncItemImpl extends _SyncItem {
return 'SyncedItem(id: $id, syncing: $syncing, parentId: $parentId, userId: $userId, path: $path, markedForDelete: $markedForDelete, sortName: $sortName, fileSize: $fileSize, videoFileName: $videoFileName, mediaSegments: $mediaSegments, fTrickPlayModel: $fTrickPlayModel, fImages: $fImages, fChapters: $fChapters, subtitles: $subtitles, userData: $userData)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SyncItemImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.syncing, syncing) || other.syncing == syncing) &&
(identical(other.parentId, parentId) ||
other.parentId == parentId) &&
(identical(other.userId, userId) || other.userId == userId) &&
(identical(other.path, path) || other.path == path) &&
(identical(other.markedForDelete, markedForDelete) ||
other.markedForDelete == markedForDelete) &&
(identical(other.sortName, sortName) ||
other.sortName == sortName) &&
(identical(other.fileSize, fileSize) ||
other.fileSize == fileSize) &&
(identical(other.videoFileName, videoFileName) ||
other.videoFileName == videoFileName) &&
(identical(other.mediaSegments, mediaSegments) ||
other.mediaSegments == mediaSegments) &&
(identical(other.fTrickPlayModel, fTrickPlayModel) ||
other.fTrickPlayModel == fTrickPlayModel) &&
(identical(other.fImages, fImages) || other.fImages == fImages) &&
const DeepCollectionEquality()
.equals(other._fChapters, _fChapters) &&
const DeepCollectionEquality()
.equals(other._subtitles, _subtitles) &&
(identical(other.userData, userData) ||
other.userData == userData));
}
@override
int get hashCode => Object.hash(
runtimeType,
id,
syncing,
parentId,
userId,
path,
markedForDelete,
sortName,
fileSize,
videoFileName,
mediaSegments,
fTrickPlayModel,
fImages,
const DeepCollectionEquality().hash(_fChapters),
const DeepCollectionEquality().hash(_subtitles),
userData);
/// Create a copy of SyncedItem
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)

View file

@ -6,7 +6,7 @@ import 'package:fladder/models/syncing/sync_item.dart';
part 'sync_settings_model.freezed.dart';
@Freezed(toJson: false, fromJson: false)
@Freezed(toJson: false, fromJson: false, copyWith: true)
class SyncSettingsModel with _$SyncSettingsModel {
const SyncSettingsModel._();

View file

@ -116,18 +116,6 @@ class _$SyncSettignsModelImpl extends _SyncSettignsModel {
return 'SyncSettingsModel(items: $items)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$SyncSettignsModelImpl &&
const DeepCollectionEquality().equals(other._items, _items));
}
@override
int get hashCode =>
Object.hash(runtimeType, const DeepCollectionEquality().hash(_items));
/// Create a copy of SyncSettingsModel
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)

View file

@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'package:collection/collection.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/items/chapters_model.dart';
@ -12,6 +12,7 @@ import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/util/localization_helper.dart';
enum PlaybackType {
directStream,
@ -24,16 +25,11 @@ enum PlaybackType {
PlaybackType.transcode => IconsaxPlusLinear.convert,
};
String get name {
switch (this) {
case PlaybackType.directStream:
return "Direct";
case PlaybackType.offline:
return "Offline";
case PlaybackType.transcode:
return "Transcoding";
}
}
String name(BuildContext context) => switch (this) {
PlaybackType.directStream => context.localized.playbackTypeDirect,
PlaybackType.offline => context.localized.playbackTypeOffline,
PlaybackType.transcode => context.localized.playbackTypeTranscode
};
}
class VideoPlayback {

Some files were not shown because too many files have changed in this diff Show more