diff --git a/.fvmrc b/.fvmrc index 4cac08f..b91fa3c 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.29.2" + "flutter": "3.32.1" } \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fb9ffd4..0f2ebef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.gitignore b/.gitignore index 34c98af..c277ce1 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.vscode/launch.json b/.vscode/launch.json index 641207a..db1705f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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" ], }, ], diff --git a/.vscode/settings.json b/.vscode/settings.json index fca954e..58c95f1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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" + ] } \ No newline at end of file diff --git a/DEVELOPEMENT.md b/DEVELOPEMENT.md new file mode 100644 index 0000000..8dab172 --- /dev/null +++ b/DEVELOPEMENT.md @@ -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/ \ No newline at end of file diff --git a/INSTALL.md b/INSTALL.md index e9d0a6f..9ea0c18 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -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) + diff --git a/README.md b/README.md index ec7fa57..0804f6b 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,14 @@
Mobile Fladder + Fladder Fladder Fladder Fladder + Fladder Fladder Fladder + Fladder Fladder
@@ -65,8 +68,14 @@ Tablet Fladder Fladder + Fladder + Fladder + Fladder + Fladder + Fladder + Fladder Fladder - Fladder + Fladder Web/Desktop [try out the web build!](https://DonutWare.github.io/Fladder) diff --git a/analysis_options.yaml b/analysis_options.yaml index ae5a2af..f5a4656 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -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 diff --git a/assets/macos-dmg/Fladder-DMG-Background.afphoto b/assets/macos-dmg/Fladder-DMG-Background.afphoto new file mode 100644 index 0000000..5db5e34 Binary files /dev/null and b/assets/macos-dmg/Fladder-DMG-Background.afphoto differ diff --git a/assets/macos-dmg/Fladder-DMG-Background.jpg b/assets/macos-dmg/Fladder-DMG-Background.jpg new file mode 100644 index 0000000..217a7d4 Binary files /dev/null and b/assets/macos-dmg/Fladder-DMG-Background.jpg differ diff --git a/assets/marketing/screenshots/Mobile/Dashboard.png b/assets/marketing/screenshots/Mobile/Dashboard.png index 9ad9c74..a7d46bd 100644 Binary files a/assets/marketing/screenshots/Mobile/Dashboard.png and b/assets/marketing/screenshots/Mobile/Dashboard.png differ diff --git a/assets/marketing/screenshots/Mobile/Dashboard_2.png b/assets/marketing/screenshots/Mobile/Dashboard_2.png deleted file mode 100644 index 0fe0459..0000000 Binary files a/assets/marketing/screenshots/Mobile/Dashboard_2.png and /dev/null differ diff --git a/assets/marketing/screenshots/Mobile/Details.png b/assets/marketing/screenshots/Mobile/Details.png index c56ccfd..f41046b 100644 Binary files a/assets/marketing/screenshots/Mobile/Details.png and b/assets/marketing/screenshots/Mobile/Details.png differ diff --git a/assets/marketing/screenshots/Mobile/Details_2.png b/assets/marketing/screenshots/Mobile/Details_2.png index e551f3e..26f2828 100644 Binary files a/assets/marketing/screenshots/Mobile/Details_2.png and b/assets/marketing/screenshots/Mobile/Details_2.png differ diff --git a/assets/marketing/screenshots/Mobile/Favourites.png b/assets/marketing/screenshots/Mobile/Favourites.png index 585f1bb..20763f6 100644 Binary files a/assets/marketing/screenshots/Mobile/Favourites.png and b/assets/marketing/screenshots/Mobile/Favourites.png differ diff --git a/assets/marketing/screenshots/Mobile/Library.png b/assets/marketing/screenshots/Mobile/Library.png index e990197..0a27852 100644 Binary files a/assets/marketing/screenshots/Mobile/Library.png and b/assets/marketing/screenshots/Mobile/Library.png differ diff --git a/assets/marketing/screenshots/Mobile/Library_Search.png b/assets/marketing/screenshots/Mobile/Library_Search.png new file mode 100644 index 0000000..4a09053 Binary files /dev/null and b/assets/marketing/screenshots/Mobile/Library_Search.png differ diff --git a/assets/marketing/screenshots/Mobile/Player.png b/assets/marketing/screenshots/Mobile/Player.png index 151a834..20687e6 100644 Binary files a/assets/marketing/screenshots/Mobile/Player.png and b/assets/marketing/screenshots/Mobile/Player.png differ diff --git a/assets/marketing/screenshots/Mobile/Resume_Tab.png b/assets/marketing/screenshots/Mobile/Resume_Tab.png index 2b918b1..03a797b 100644 Binary files a/assets/marketing/screenshots/Mobile/Resume_Tab.png and b/assets/marketing/screenshots/Mobile/Resume_Tab.png differ diff --git a/assets/marketing/screenshots/Mobile/Settings.png b/assets/marketing/screenshots/Mobile/Settings.png index 666031d..efd44c1 100644 Binary files a/assets/marketing/screenshots/Mobile/Settings.png and b/assets/marketing/screenshots/Mobile/Settings.png differ diff --git a/assets/marketing/screenshots/Mobile/Sync.png b/assets/marketing/screenshots/Mobile/Sync.png index 04f22fe..b17e1a7 100644 Binary files a/assets/marketing/screenshots/Mobile/Sync.png and b/assets/marketing/screenshots/Mobile/Sync.png differ diff --git a/assets/marketing/screenshots/Tablet/Dashboard.png b/assets/marketing/screenshots/Tablet/Dashboard.png index ff361cd..107c853 100644 Binary files a/assets/marketing/screenshots/Tablet/Dashboard.png and b/assets/marketing/screenshots/Tablet/Dashboard.png differ diff --git a/assets/marketing/screenshots/Tablet/Details.png b/assets/marketing/screenshots/Tablet/Details.png index 4944efe..9d5e01f 100644 Binary files a/assets/marketing/screenshots/Tablet/Details.png and b/assets/marketing/screenshots/Tablet/Details.png differ diff --git a/assets/marketing/screenshots/Tablet/Details_2.png b/assets/marketing/screenshots/Tablet/Details_2.png new file mode 100644 index 0000000..197d44f Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Details_2.png differ diff --git a/assets/marketing/screenshots/Tablet/Favourites.png b/assets/marketing/screenshots/Tablet/Favourites.png new file mode 100644 index 0000000..6c4ff94 Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Favourites.png differ diff --git a/assets/marketing/screenshots/Tablet/Library.png b/assets/marketing/screenshots/Tablet/Library.png new file mode 100644 index 0000000..569373c Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Library.png differ diff --git a/assets/marketing/screenshots/Tablet/Library_Search.png b/assets/marketing/screenshots/Tablet/Library_Search.png new file mode 100644 index 0000000..2119aac Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Library_Search.png differ diff --git a/assets/marketing/screenshots/Tablet/Player.png b/assets/marketing/screenshots/Tablet/Player.png new file mode 100644 index 0000000..72d96a2 Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Player.png differ diff --git a/assets/marketing/screenshots/Tablet/Resume_Tab.png b/assets/marketing/screenshots/Tablet/Resume_Tab.png new file mode 100644 index 0000000..48750a7 Binary files /dev/null and b/assets/marketing/screenshots/Tablet/Resume_Tab.png differ diff --git a/assets/marketing/screenshots/Tablet/Settings.png b/assets/marketing/screenshots/Tablet/Settings.png index 3ed3277..b9e2a64 100644 Binary files a/assets/marketing/screenshots/Tablet/Settings.png and b/assets/marketing/screenshots/Tablet/Settings.png differ diff --git a/assets/marketing/screenshots/Tablet/Sync.png b/assets/marketing/screenshots/Tablet/Sync.png index 8a2b677..e5ae951 100644 Binary files a/assets/marketing/screenshots/Tablet/Sync.png and b/assets/marketing/screenshots/Tablet/Sync.png differ diff --git a/build.yaml b/build.yaml index 82d02cd..4acfabe 100644 --- a/build.yaml +++ b/build.yaml @@ -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" diff --git a/flatpak/nl.jknaapen.fladder.yaml b/flatpak/nl.jknaapen.fladder.yaml index 5a55f0b..9a31566 100644 --- a/flatpak/nl.jknaapen.fladder.yaml +++ b/flatpak/nl.jknaapen.fladder.yaml @@ -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: .. \ No newline at end of file + path: .. diff --git a/l10n.yaml b/l10n.yaml index b1cc141..5340fac 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -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 diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 24f3255..a766506 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -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": {} } diff --git a/lib/l10n/app_cs.arb b/lib/l10n/app_cs.arb new file mode 100644 index 0000000..11dc705 --- /dev/null +++ b/lib/l10n/app_cs.arb @@ -0,0 +1,1345 @@ +{ + "active": "Aktivní", + "@active": {}, + "nativeName": "Čeština", + "@nativeName": {}, + "about": "O nás", + "@about": {}, + "accept": "Přijmout", + "@accept": {}, + "actor": "{count, plural, one{Herec/Herečka} two{Herci} few{Herců} many{Herců} other{Herců}}", + "@actor": { + "description": "actor", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "addAsFavorite": "Přidat do oblíbených", + "@addAsFavorite": {}, + "addToCollection": "Přidat do kolekce", + "@addToCollection": {}, + "addToPlaylist": "Přidat do seznamu skladeb", + "@addToPlaylist": {}, + "advanced": "Pokročilé", + "@advanced": {}, + "director": "{count, plural, other{Režisérů} two{Režiséři} one{Režisér}}", + "@director": { + "description": "director", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "appLockBiometrics": "Biometrika", + "@appLockBiometrics": {}, + "backgroundBlur": "Rozmazání pozadí", + "@backgroundBlur": {}, + "appLockPasscode": "Přístupový kód", + "@appLockPasscode": {}, + "communityRating": "Hodnocení komunity", + "@communityRating": {}, + "dashboardContinueListening": "Pokračovat v poslechu", + "@dashboardContinueListening": {}, + "filter": "{count, plural, other{Filtrů} one{Filtr} few{Filtry}}", + "@filter": { + "description": "filter", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "amoledBlack": "Amoled černá", + "@amoledBlack": {}, + "appLockAutoLogin": "Automatické přihlášení", + "@appLockAutoLogin": {}, + "audio": "Zvuk", + "@audio": {}, + "autoPlay": "Automatické přehrávání", + "@autoPlay": {}, + "backgroundOpacity": "Průhlednost pozadí", + "@backgroundOpacity": {}, + "biometricsFailedCheckAgain": "Biometrické údaje selhaly. Zkontrolujte nastavení a zkuste to znovu.", + "@biometricsFailedCheckAgain": {}, + "bold": "Tučné písmo", + "@bold": {}, + "cancel": "Zrušit", + "@cancel": {}, + "change": "Změna", + "@change": {}, + "clearAllSettings": "Vymazat všechna nastavení", + "@clearAllSettings": {}, + "clear": "Vymazat", + "@clear": {}, + "clearAllSettingsQuestion": "Vymazat všechna nastavení?", + "@clearAllSettingsQuestion": {}, + "code": "Kód", + "@code": {}, + "collectionFolder": "Složka kolekcí", + "@collectionFolder": {}, + "color": "Barva", + "@color": {}, + "combined": "Kombinovaný", + "@combined": {}, + "dashboardContinue": "Pokračovat", + "@dashboardContinue": {}, + "dashboardContinueReading": "Pokračovat ve čtení", + "@dashboardContinueReading": {}, + "dashboardRecentlyAdded": "Nedávno přidáno do {name}", + "@dashboardRecentlyAdded": { + "description": "Recently added on home screen", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "days": "Dny", + "@days": {}, + "delete": "Smazat", + "@delete": {}, + "deleteItem": "Smazat {item}?", + "@deleteItem": { + "description": "deleteItem", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "disabled": "Vypnuto", + "@disabled": {}, + "discovered": "Objeven", + "@discovered": {}, + "displayLanguage": "Jazyk zobrazení", + "@displayLanguage": {}, + "downloadsClearDesc": "Odstranit všechna synchronizovaná data a vymazat všechna data pro každého synchronizovaného uživatele?", + "@downloadsClearDesc": {}, + "downloadsPath": "Cesta", + "@downloadsPath": {}, + "downloadsSyncedData": "Synchronizovaná data", + "@downloadsSyncedData": {}, + "downloadsTitle": "Stažené", + "@downloadsTitle": {}, + "dynamicText": "Dynamický", + "@dynamicText": {}, + "editMetadata": "Upravit metadata", + "@editMetadata": {}, + "empty": "Prazdné", + "@empty": {}, + "enabled": "Zapnuto", + "@enabled": {}, + "endsAt": "končí v {date}", + "@endsAt": { + "description": "endsAt", + "placeholders": { + "date": { + "type": "DateTime", + "format": "jm" + } + } + }, + "episode": "{count, plural, other{Epizod} one{Epizoda} few{Epizody}}", + "@episode": { + "description": "episode", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "error": "Chyba", + "@error": {}, + "folders": "Složky", + "@folders": {}, + "forceRefresh": "Vynutit obnovení", + "@forceRefresh": {}, + "groupBy": "Skupina od", + "@groupBy": {}, + "home": "Domů", + "@home": {}, + "homeBannerSlideshow": "Prezentace", + "@homeBannerSlideshow": {}, + "immediately": "Ihned", + "@immediately": {}, + "incorrectPinTryAgain": "Nesprávný PIN. Zkuste to znovu.", + "@incorrectPinTryAgain": {}, + "info": "Info", + "@info": {}, + "invalidUrlDesc": "URL adresa musí začínat http(s)://", + "@invalidUrlDesc": {}, + "itemCount": "Počet položek: {count}", + "@itemCount": { + "description": "Item count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "label": "{count, plural, other{Označení} one{Označení}}", + "@label": { + "description": "label", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "libraryPageSizeTitle": "Velikost stránky knihovny", + "@libraryPageSizeTitle": {}, + "light": "Světlý", + "@light": {}, + "list": "List", + "@list": {}, + "markAsWatched": "Označit jako zhlédnuté", + "@markAsWatched": {}, + "mediaTypeBase": "Typ základny", + "@mediaTypeBase": {}, + "mediaTypeBoxset": "Média", + "@mediaTypeBoxset": {}, + "mediaTypePerson": "Osoba", + "@mediaTypePerson": {}, + "mediaTypePhoto": "Fotka", + "@mediaTypePhoto": {}, + "mediaTypePlaylist": "Seznam skladeb", + "@mediaTypePlaylist": {}, + "mediaTypeSeason": "Sezóna", + "@mediaTypeSeason": {}, + "metadataRefreshDefault": "Vyhledat nové a aktualizované soubory", + "@metadataRefreshDefault": {}, + "metadataRefreshFull": "Nahradit všechna metadata", + "@metadataRefreshFull": {}, + "metadataRefreshValidation": "Vyhledat chybějící metadata", + "@metadataRefreshValidation": {}, + "name": "Název", + "@name": {}, + "navigation": "Navigace", + "@navigation": {}, + "navigationFavorites": "Oblíbené", + "@navigationFavorites": {}, + "navigationSync": "Synchronizováno", + "@navigationSync": {}, + "noItemsSynced": "Žádné synchronizované položky", + "@noItemsSynced": {}, + "noItemsToShow": "Žádné položky k zobrazení", + "@noItemsToShow": {}, + "noRating": "Žádné hodnocení", + "@noRating": {}, + "noResults": "Žádné výsledky", + "@noResults": {}, + "noServersFound": "Nenalezeny žádné nové servery", + "@noServersFound": {}, + "noSuggestionsFound": "Nenalezeny žádné návrhy", + "@noSuggestionsFound": {}, + "favorite": "Oblíbené", + "@favorite": {}, + "mediaTypeSeries": "Série", + "@mediaTypeSeries": {}, + "all": "Všechny", + "@all": {}, + "dateLastContentAdded": "Datum posledního přidání obsahu", + "@dateLastContentAdded": {}, + "disableFilters": "Vypnout filtry", + "@disableFilters": {}, + "dateAdded": "Datum přidání", + "@dateAdded": {}, + "ascending": "Vzestupně", + "@ascending": {}, + "controls": "Ovládání", + "@controls": {}, + "appLockTitle": "Nastavte způsob přihlášení pro {userName}", + "@appLockTitle": { + "description": "Pop-up to pick a login method", + "placeholders": { + "userName": { + "type": "String" + } + } + }, + "chapter": "{count, plural, other{Kapitol} one{Kapitola}}", + "@chapter": { + "description": "chapter", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "clearChanges": "Vymazat změny", + "@clearChanges": {}, + "dashboard": "Řídící panel", + "@dashboard": {}, + "clearSelection": "Vymazat výběr", + "@clearSelection": {}, + "datePlayed": "Datum přehrání", + "@datePlayed": {}, + "mediaTypePhotoAlbum": "Foto album", + "@mediaTypePhotoAlbum": {}, + "metaDataSavedFor": "Metadata uložena pro {item}", + "@metaDataSavedFor": { + "description": "metaDataSavedFor", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "dashboardContinueWatching": "Pokračovat ve sledování", + "@dashboardContinueWatching": {}, + "loop": "Smyčka", + "@loop": {}, + "heightOffset": "Výškový posun", + "@heightOffset": {}, + "genre": "{count, plural, other{Žánrů} one{Žánr} few{Žánry}}", + "@genre": { + "description": "genre", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "hide": "Skrýt", + "@hide": {}, + "mediaTypeEpisode": "Epizoda", + "@mediaTypeEpisode": {}, + "lockscreen": "Zamykací obrazovka", + "@lockscreen": {}, + "login": "Přihlásit se", + "@login": {}, + "mouseDragSupport": "Táhněte pomocí myši", + "@mouseDragSupport": {}, + "musicAlbum": "Album", + "@musicAlbum": {}, + "fontColor": "Barva písma", + "@fontColor": {}, + "logout": "Odhlásit se", + "@logout": {}, + "mediaTypeBook": "Kniha", + "@mediaTypeBook": {}, + "moreFrom": "Více od {info}", + "@moreFrom": { + "description": "More from", + "placeholders": { + "info": { + "type": "String" + } + } + }, + "hideEmpty": "Skrýt prázdné", + "@hideEmpty": {}, + "mediaTypeMovie": "Film", + "@mediaTypeMovie": {}, + "libraryFetchNoItemsFound": "Nenalezeny žádné položky. Zkuste jiná nastavení.", + "@libraryFetchNoItemsFound": {}, + "libraryPageSizeDesc": "Nastavte množství, které se má načíst najednou. Hodnota 0 vypne stránkování.", + "@libraryPageSizeDesc": {}, + "loggedIn": "Přihlášen/a", + "@loggedIn": {}, + "logoutUserPopupContent": "Tímto odhlásíte uživatele {userName} a uživatel se smaže z aplikace.\nBudete se muset znovu přihlásit na server {serverName}.", + "@logoutUserPopupContent": { + "description": "Pop-up for logging out the user description", + "placeholders": { + "userName": { + "type": "String" + }, + "serverName": { + "type": "String" + } + } + }, + "moreOptions": "Více možností", + "@moreOptions": {}, + "never": "Nikdy", + "@never": {}, + "openWebLink": "Otevřít webový odkaz", + "@openWebLink": {}, + "close": "Zavřít", + "@close": {}, + "nextUp": "Nadcházející", + "@nextUp": {}, + "logoutUserPopupTitle": "Odhlásit uživatele {userName}?", + "@logoutUserPopupTitle": { + "description": "Pop-up for logging out the user", + "placeholders": { + "userName": { + "type": "String" + } + } + }, + "deleteFileFromSystem": "Smazáním této položky {item} ji smažete ze souborového systému i z knihovny médií. Opravdu chcete pokračovat?", + "@deleteFileFromSystem": { + "description": "Delete file from system", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "descending": "Sestupně", + "@descending": {}, + "downloadsClearTitle": "Vymazat synchronizovaná data", + "@downloadsClearTitle": {}, + "group": "Skupina", + "@group": {}, + "fetchingLibrary": "Načítání položek z knihovny…", + "@fetchingLibrary": {}, + "goTo": "Přejít na", + "@goTo": {}, + "mode": "Mód", + "@mode": {}, + "navigationDashboard": "Řídící panel", + "@navigationDashboard": {}, + "page": "Strana {index}", + "@page": { + "description": "page", + "placeholders": { + "index": { + "type": "int" + } + } + }, + "continuePage": "Pokračovat - strana {page}", + "@continuePage": { + "description": "Continue - page 1", + "placeholders": { + "page": { + "type": "int" + } + } + }, + "fontSize": "Velikost písma", + "@fontSize": {}, + "grid": "Mřížka", + "@grid": {}, + "invalidUrl": "Neplatná adresa URL", + "@invalidUrl": {}, + "mediaTypeFolder": "Složka", + "@mediaTypeFolder": {}, + "failedToLoadImage": "Nepodařilo se načíst obrázek", + "@failedToLoadImage": {}, + "favorites": "Oblíbené", + "@favorites": {}, + "homeBannerCarousel": "Kolotoč", + "@homeBannerCarousel": {}, + "identify": "Identifikovat", + "@identify": {}, + "library": "{count, plural, other{knihoven} few{knihovny} one{knihovna}}", + "@library": { + "description": "Plural", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "markAsUnwatched": "Označit jako nezhlédnuté", + "@markAsUnwatched": {}, + "minutes": "{count, plural, other {Minut} few{Minuty} one{Minuta} }", + "@minutes": { + "description": "minute", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "none": "Žádný", + "@none": {}, + "other": "Ostatní", + "@other": {}, + "outlineColor": "Vnější barva", + "@outlineColor": {}, + "outlineSize": "Vnější velikost", + "@outlineSize": {}, + "options": "Možnosti", + "@options": {}, + "notPartOfAlbum": "Není součástí alba", + "@notPartOfAlbum": {}, + "openParent": "Otevřené rodiče", + "@openParent": {}, + "overview": "Přehled", + "@overview": {}, + "normal": "Normální", + "@normal": {}, + "openShow": "Otevřít show", + "@openShow": {}, + "parentalRating": "Rodičovské hodnocení", + "@parentalRating": {}, + "pathClearTitle": "Vymazat cestu ke stažení", + "@pathClearTitle": {}, + "password": "Heslo", + "@password": {}, + "pathEditDesc": "Toto umístění je nastaveno pro všechny uživatele, synchronizovaná data již nebudou přístupná. Zůstanou ve vašem úložišti.", + "@pathEditDesc": {}, + "pathEditSelect": "Vyberte cíl pro stahování", + "@pathEditSelect": {}, + "videoScaling": "Škálování videa", + "@videoScaling": {}, + "unableToReverseAction": "Tuto akci nelze vrátit zpět. Odstraní všechna nastavení.", + "@unableToReverseAction": {}, + "video": "Video", + "@video": {}, + "removedFromCollection": "Odstraněno z kolekce {collectionName}", + "@removedFromCollection": { + "placeholders": { + "collectionName": { + "type": "String" + } + } + }, + "syncStatusNotFound": "Nenalezeno", + "@syncStatusNotFound": {}, + "syncStatusRunning": "Synchronizování", + "@syncStatusRunning": {}, + "subtitleConfiguratorPlaceHolder": "Toto je zástupný text, zde není nic k vidění.", + "@subtitleConfiguratorPlaceHolder": {}, + "subtitles": "Titulky", + "@subtitles": {}, + "themeModeDark": "Tmavé", + "@themeModeDark": {}, + "userName": "Uživatelské jméno", + "@userName": {}, + "sync": "Synchronizace", + "@sync": {}, + "type": "{count, plural, other{Typů} one{Typ} few{Typy}}", + "@type": { + "description": "type", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "syncedItems": "Synchronizované položky", + "@syncedItems": {}, + "syncStatusWaitingToRetry": "Čekání na opakování", + "@syncStatusWaitingToRetry": {}, + "themeColor": "Barva téma", + "@themeColor": {}, + "themeModeLight": "Světlé", + "@themeModeLight": {}, + "guestActor": "{count, plural, other{Hostující herci} one{Hostující herec/herečka}}", + "@guestActor": { + "description": "Guest actors", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "addedToCollection": "Přidáno do kolekce {collectionName}", + "@addedToCollection": { + "placeholders": { + "collectionName": { + "type": "String" + } + } + }, + "unableToConnectHost": "Nelze se připojit k hostiteli", + "@unableToConnectHost": {}, + "syncStatusFailed": "Neúspěšné", + "@syncStatusFailed": {}, + "play": "Přehrát {item}", + "@play": { + "description": "Play with", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "playCount": "Počet přehrání", + "@playCount": {}, + "playFrom": "Přehrát z {name}", + "@playFrom": { + "description": "playFrom", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playFromStart": "Přehrát {name} od začátku", + "@playFromStart": { + "description": "speel vanaf het begin", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playLabel": "Přehrát", + "@playLabel": {}, + "playVideos": "Přehrát videa", + "@playVideos": {}, + "played": "Přehráno", + "@played": {}, + "quickConnectAction": "Zadejte kód pro rychlé připojení", + "@quickConnectAction": {}, + "quickConnectInputACode": "Zadejte kód", + "@quickConnectInputACode": {}, + "quickConnectTitle": "Rychlé připojení", + "@quickConnectTitle": {}, + "quickConnectWrongCode": "Špatný kód", + "@quickConnectWrongCode": {}, + "random": "Náhodně", + "@random": {}, + "rating": "{count, plural, other{Hodnocení} one{Hodnocení}}", + "@rating": { + "description": "rating", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "reWatch": "Znovu se podívat", + "@reWatch": {}, + "read": "Přečíst {item}", + "@read": { + "description": "read", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "readFromStart": "Přečíst {item} od začátku", + "@readFromStart": { + "description": "Read book from start", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "recursive": "Rekurze", + "@recursive": {}, + "refresh": "Obnovit", + "@refresh": {}, + "refreshMetadata": "Obnovit metadata", + "@refreshMetadata": {}, + "refreshPopup": "Obnovit - {name}", + "@refreshPopup": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "refreshPopupContentMetadata": "Metadata se aktualizovala na základě nastavení a internetových služeb zapnutých v řídicím panelu.", + "@refreshPopupContentMetadata": {}, + "related": "Související", + "@related": {}, + "releaseDate": "Datum vydání", + "@releaseDate": {}, + "removeAsFavorite": "Odebrat z oblíbených", + "@removeAsFavorite": {}, + "removeFromCollection": "Odebrat z kolekce", + "@removeFromCollection": {}, + "removeFromPlaylist": "Odebrat ze seznamu skladeb", + "@removeFromPlaylist": {}, + "replaceAllImages": "Nahradit všechny obrázky", + "@replaceAllImages": {}, + "replaceExistingImages": "Nahradit existující obrázky", + "@replaceExistingImages": {}, + "result": "Výsledek", + "@result": {}, + "resumable": "Obnovitelné", + "@resumable": {}, + "resume": "Obnovit {item}", + "@resume": { + "description": "resume", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "retry": "Zkusit znovu", + "@retry": {}, + "runTime": "Trvání", + "@runTime": {}, + "save": "Uložit", + "@save": {}, + "saved": "Uloženo", + "@saved": {}, + "scanBiometricHint": "Ověření totožnosti", + "@scanBiometricHint": {}, + "scanLibrary": "Skenovat knihovnu", + "@scanLibrary": {}, + "scanningName": "Skenování - {name}…", + "@scanningName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "search": "Vyhledat", + "@search": {}, + "seconds": "{count, plural, other{Sekund} few{Sekundy} one{Sekunda} }", + "@seconds": { + "description": "second", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "selectAll": "Označit vše", + "@selectAll": {}, + "selectTime": "Vyberte čas", + "@selectTime": {}, + "selectViewType": "Vyberte typ zobrazení", + "@selectViewType": {}, + "selected": "Označeno", + "@selected": {}, + "selectedWith": "Označeno {info}", + "@selectedWith": { + "description": "selected", + "placeholders": { + "info": { + "type": "String" + } + } + }, + "separate": "Oddělený", + "@separate": {}, + "server": "Server", + "@server": {}, + "settingSecurityApplockTitle": "Zámek aplikace", + "@settingSecurityApplockTitle": {}, + "settings": "Nastavení", + "@settings": {}, + "settingsBlurEpisodesDesc": "Rozmazat všechny nadcházející epizody", + "@settingsBlurEpisodesDesc": {}, + "settingsBlurEpisodesTitle": "Rozmazat nadcházející epizody", + "@settingsBlurEpisodesTitle": {}, + "settingsBlurredPlaceholderDesc": "Zobrazit rozmazané pozadí při načítání plakátů", + "@settingsBlurredPlaceholderDesc": {}, + "settingsBlurredPlaceholderTitle": "Rozmazané placeholdery", + "@settingsBlurredPlaceholderTitle": {}, + "settingsClientDesc": "Obecné, Časový limit, Rozvržení, Téma", + "@settingsClientDesc": {}, + "settingsClientTitle": "Fladder", + "@settingsClientTitle": {}, + "settingsContinue": "Pokračovat", + "@settingsContinue": {}, + "settingsHomeBannerDescription": "Zobrazit jako prezentaci, kolotoč nebo skrýt banner", + "@settingsHomeBannerDescription": {}, + "settingsHomeBannerTitle": "Domovský banner", + "@settingsHomeBannerTitle": {}, + "settingsHomeBannerInformationDesc": "Informace, které se mají zobrazit na domovském banneru", + "@settingsHomeBannerInformationDesc": {}, + "settingsHomeBannerInformationTitle": "Informace na banneru", + "@settingsHomeBannerInformationTitle": {}, + "settingsHomeNextUpTitle": "Nadcházející plakáty", + "@settingsHomeNextUpTitle": {}, + "settingsPlayerCustomSubtitlesDesc": "Přizpůsobit velikost, barvu, polohu, obrys", + "@settingsPlayerCustomSubtitlesDesc": {}, + "settingsPlayerDesc": "Poměr stran, Pokročilý", + "@settingsPlayerDesc": {}, + "settingsPlayerNativeLibassAccelDesc": "Použít vykreslování titulků libass z video přehrávače", + "@settingsPlayerNativeLibassAccelDesc": {}, + "settingsPlayerNativeLibassAccelTitle": "Nativní libass titulky", + "@settingsPlayerNativeLibassAccelTitle": {}, + "settingsPlayerBufferSizeTitle": "Velikost vyrovnávací paměti videa", + "@settingsPlayerBufferSizeTitle": {}, + "settingsPlayerBufferSizeDesc": "Nakonfigurovat velikost vyrovnávací paměti pro přehrávání videa a určit, kolik dat se načte do mezipaměti.", + "@settingsPlayerBufferSizeDesc": {}, + "settingsPlayerTitle": "Přehrávač", + "@settingsPlayerTitle": {}, + "settingsPlayerVideoHWAccelDesc": "Použít GPU na vykreslení videa (doporučeno)", + "@settingsPlayerVideoHWAccelDesc": {}, + "settingsPlayerVideoHWAccelTitle": "Hardwarová akcelerace", + "@settingsPlayerVideoHWAccelTitle": {}, + "settingsPosterSize": "Velikost plakátu", + "@settingsPosterSize": {}, + "settingsPosterSlider": "Zobrazit posuvník měřítka", + "@settingsPosterSlider": {}, + "settingsProfileDesc": "Zamykací obrazovka", + "@settingsProfileDesc": {}, + "settingsProfileTitle": "Profil", + "@settingsProfileTitle": {}, + "settingsQuickConnectTitle": "Rychlé připojení", + "@settingsQuickConnectTitle": {}, + "settingsSecurity": "Zabezpečení", + "@settingsSecurity": {}, + "settingsShowScaleSlider": "Zobrazit posuvník pro změnu velikosti plakátu", + "@settingsShowScaleSlider": {}, + "settingsVisual": "Vizuál", + "@settingsVisual": {}, + "shadow": "Stín", + "@shadow": {}, + "showEmpty": "Zobrazit prázdné", + "@showEmpty": {}, + "showDetails": "Zobrazit detaily", + "@showDetails": {}, + "shuffleVideos": "Náhodné přehrávání videa", + "@shuffleVideos": {}, + "somethingWentWrong": "Něco se pokazilo", + "@somethingWentWrong": {}, + "somethingWentWrongPasswordCheck": "Něco se pokazilo. Zkontrolujte si heslo.", + "@somethingWentWrongPasswordCheck": {}, + "sortBy": "Seřadit podle", + "@sortBy": {}, + "sortName": "Název", + "@sortName": {}, + "sortOrder": "Pořadí řazení", + "@sortOrder": {}, + "start": "Start", + "@start": {}, + "studio": "{count, plural, other{Studia} one{Studio}}", + "@studio": { + "description": "studio", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "subtitleConfigurator": "Konfigurátor titulků", + "@subtitleConfigurator": {}, + "switchUser": "Přepnout uživatele", + "@switchUser": {}, + "syncDetails": "Podrobnosti synchronizace", + "@syncDetails": {}, + "syncOpenParent": "Otevřít nadřazený element", + "@syncOpenParent": {}, + "theme": "Téma", + "@theme": {}, + "themeModeSystem": "Systémové", + "@themeModeSystem": {}, + "timeOut": "Časový limit", + "@timeOut": {}, + "totalSize": "Celková velikost: {size}", + "@totalSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, + "unPlayed": "Nepřehrané", + "@unPlayed": {}, + "useDefaults": "Použít výchozí nastavení", + "@useDefaults": {}, + "videoScalingContain": "Přizpůsobit", + "@videoScalingContain": {}, + "videoScalingCover": "Vyplnit", + "@videoScalingCover": {}, + "viewPhotos": "Zobrazit fotografie", + "@viewPhotos": {}, + "watchOn": "Sledovat na", + "@watchOn": {}, + "addToNewCollection": "Nová kolekce", + "@addToNewCollection": {}, + "addItemsToCollection": "Přidat {itemLength} položku/položek do kolekce", + "@addItemsToCollection": { + "placeholders": { + "itemLength": { + "type": "int" + } + } + }, + "addToNewPlaylist": "Nový seznam skladeb", + "@addToNewPlaylist": {}, + "addItemsToPlaylist": "Přidat {itemLength} položku/položek do seznamu skladeb", + "@addItemsToPlaylist": { + "placeholders": { + "itemLength": { + "type": "int" + } + } + }, + "addedToPlaylist": "Přidáno do seznamu skladeb {playlistName}", + "@addedToPlaylist": { + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "syncStatusEnqueued": "Zařazeno do fronty", + "@syncStatusEnqueued": {}, + "syncStatusComplete": "Dokončeno", + "@syncStatusComplete": {}, + "syncStatusCanceled": "Zrušeno", + "@syncStatusCanceled": {}, + "syncStatusPaused": "Pozastaveno", + "@syncStatusPaused": {}, + "syncStatusSynced": "Synchronizováno", + "@syncStatusSynced": {}, + "syncStatusPartially": "Částečně", + "@syncStatusPartially": {}, + "syncOverlaySyncing": "Synchronizace podrobností o položce", + "@syncOverlaySyncing": {}, + "syncSelectDownloadsFolder": "Vyberte složku pro stahování", + "@syncSelectDownloadsFolder": {}, + "syncNoFolderSetup": "Žádné nastavení synchronizační složky", + "@syncNoFolderSetup": {}, + "syncRemoveUnableToDeleteItem": "Nepodařilo se odstranit synchronizovanou položku, něco se pokazilo", + "@syncRemoveUnableToDeleteItem": {}, + "syncAddItemForSyncing": "Přidána položka {item} pro synchronizaci", + "@syncAddItemForSyncing": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "startedSyncingItem": "Zahájena synchronizace položky {item}", + "@startedSyncingItem": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "unableToSyncItem": "Nelze synchronizovat {item}, něco se pokazilo", + "@unableToSyncItem": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "season": "{count, plural, other{Sezón} few{Sezóny} one{Sezóna­­} }", + "@season": { + "description": "season", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "restart": "Restartovat", + "@restart": {}, + "retrievePublicListOfUsers": "Načíst veřejný seznam uživatelů", + "@retrievePublicListOfUsers": {}, + "scanYourFingerprintToAuthenticate": "Naskenujte svůj otisk prstu pro ověření uživatele {user}", + "@scanYourFingerprintToAuthenticate": { + "placeholders": { + "user": { + "type": "String" + } + } + }, + "scrollToTop": "Přejít nahoru", + "@scrollToTop": {}, + "settingsEnableOsMediaControls": "Povolit systémové ovládání médií", + "@settingsEnableOsMediaControls": {}, + "settingsHomeNextUpDesc": "Typ plakátů zobrazených na řídicím panelu", + "@settingsHomeNextUpDesc": {}, + "syncDeletePopupPermanent": "Tato akce je trvalá a odstraní všechny lokálně synchronizované soubory", + "@syncDeletePopupPermanent": {}, + "set": "Nastavit", + "@set": { + "description": "Use for setting a certain value", + "context": "Set 'time'" + }, + "settingsPlayerCustomSubtitlesTitle": "Přizpůsobení titulků", + "@settingsPlayerCustomSubtitlesTitle": {}, + "settingsPlayerMobileWarning": "Zapnutí hardwarové akcelerace a nativních titulků libass v systému Android může způsobit, že se některé titulky nebudou vykreslovat.", + "@settingsPlayerMobileWarning": {}, + "settingsPosterPinch": "Zvětšení plakátů pomocí přiblížení a sevření prstů", + "@settingsPosterPinch": {}, + "showAlbum": "Zobrazit album", + "@showAlbum": {}, + "year": "{count, plural, other{Let} one{Rok} few{Roky}}", + "@year": { + "description": "year", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "timeAndAnnotation": "{minutes} a {seconds}", + "@timeAndAnnotation": { + "description": "timeAndAnnotation", + "placeholders": { + "minutes": { + "type": "String" + }, + "seconds": { + "type": "String" + } + } + }, + "syncOverlayDeleting": "Odebírání synchronizované položky", + "@syncOverlayDeleting": {}, + "syncDeleteItemDesc": "Smazat všechna synchronizovaná data pro {item}?", + "@syncDeleteItemDesc": { + "description": "Sync delete item pop-up window", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "unknown": "Neznámý", + "@unknown": {}, + "syncRemoveDataTitle": "Odstranit synchronizovaná data?", + "@syncRemoveDataTitle": {}, + "syncDeleteItemTitle": "Smazat synchronizovanou položku", + "@syncDeleteItemTitle": {}, + "writer": "{count, plural, other{Spisovatelů} few{Spisovatelé} one{Spisovatel}}", + "@writer": { + "description": "writer", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "tag": "{count, plural, one{Štítek} few{Štítky} many{Štítků} other{Štítků}}", + "@tag": { + "description": "tag", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "setIdentityTo": "Změnit identitu na {name}", + "@setIdentityTo": { + "description": "setIdentityTo", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "shuffleGallery": "Náhodné přehrávání galerie", + "@shuffleGallery": {}, + "syncRemoveDataDesc": "Smazat synchronizovaná video data? Toto je trvalé a budete muset soubory znovu synchronizovat", + "@syncRemoveDataDesc": {}, + "pathEditTitle": "Změnit umístění", + "@pathEditTitle": {}, + "aboutVersion": "Verze: {version}", + "@aboutVersion": { + "placeholders": { + "version": { + "type": "String" + } + } + }, + "aboutBuild": "Verze sestavení: {buildNumber}", + "@aboutBuild": { + "placeholders": { + "buildNumber": { + "type": "String" + } + } + }, + "aboutCreatedBy": "Vytvořil DonutWare", + "@aboutCreatedBy": {}, + "autoNextOffStaticDesc": "Zobrazit nadcházející obrazovku, když zbývá 30 sekund přehrávání", + "@autoNextOffStaticDesc": {}, + "phone": "Telefon", + "@phone": {}, + "mediaSegmentPreview": "Náhled", + "@mediaSegmentPreview": {}, + "rememberAudioSelections": "Nastavit zvukovou stopu na základě předchozí položky", + "@rememberAudioSelections": {}, + "settingsAutoNextDesc": "Zobrazí náhled nadcházející položky ke konci, pokud je ve frontě jiná položka", + "@settingsAutoNextDesc": {}, + "autoNextOffSmartDesc": "Zobrazí nadcházející obrazovku na začátku titulků, pokud po nich nezbývá více než 10 sekund. Jinak zobrazí další obrazovku s 30 sekundami zbývajícího času přehrávání", + "@autoNextOffSmartDesc": {}, + "playerSettingsBackendDesc": "Vyberte si preferovaný přehrávač médií pro optimální zážitek z přehrávání", + "@playerSettingsBackendDesc": {}, + "autoNextOffStaticTitle": "Statický", + "@autoNextOffStaticTitle": {}, + "unableToPlayMedia": "Při hledání kompatibilního typu média došlo k chybě", + "@unableToPlayMedia": {}, + "removeFilterForLibrary": "Odebrat {filter}?", + "@removeFilterForLibrary": { + "description": "removeFilterForLibrary", + "placeholders": { + "filter": { + "type": "String" + } + } + }, + "deviceOrientationLandscapeLeft": "Horizontálně vlevo", + "@deviceOrientationLandscapeLeft": {}, + "noVideoPlayerOptions": "Vybraný backend nemá žádné možnosti", + "@noVideoPlayerOptions": {}, + "clientSettingsShowAllCollectionsDesc": "Pokud je povoleno, zobrazit všechny typy kolekcí, včetně těch, které Fladder nepodporuje", + "@clientSettingsShowAllCollectionsDesc": {}, + "skipButtonLabel": "(S) Přeskočit {segment}", + "@skipButtonLabel": { + "placeholders": { + "segment": { + "type": "String" + } + } + }, + "mediaSegmentUnknown": "Neznámý", + "@mediaSegmentUnknown": {}, + "rememberSubtitleSelectionsDesc": "Zkusit nastavit titulkovou stopu tak, aby se co nejvíce shodovala s posledním videem.", + "@rememberSubtitleSelectionsDesc": {}, + "copyStreamUrl": "Kopírovat URL streamu", + "@copyStreamUrl": {}, + "mediaSegmentRecap": "Shrnutí", + "@mediaSegmentRecap": {}, + "mdkExperimental": "MDK je stále v experimentální fázi", + "@mdkExperimental": {}, + "mediaSegmentOutro": "Konec", + "@mediaSegmentOutro": {}, + "layoutModeSingle": "Jeden", + "@layoutModeSingle": {}, + "copiedToClipboard": "Zkopírováno do schránky", + "@copiedToClipboard": {}, + "defaultLabel": "Výchozí", + "@defaultLabel": { + "description": "To indicate a default value, default video player backend" + }, + "external": "Externí", + "@external": {}, + "settingsLayoutSizesDesc": "Vyberte, které velikosti rozvržení může aplikace použít na základě velikosti okna", + "@settingsLayoutSizesDesc": {}, + "episodeUnaired": "Nevysílána", + "@episodeUnaired": {}, + "episodeMissing": "Chybí", + "@episodeMissing": {}, + "rememberSubtitleSelections": "Nastavení titulkové stopy na základě předchozí položky", + "@rememberSubtitleSelections": {}, + "autoCheckForUpdates": "Pravidelně kontrolovat aktualizace", + "@autoCheckForUpdates": {}, + "castAndCrew": "Obsazení a štáb", + "@castAndCrew": {}, + "segmentActionSkip": "Přeskočit", + "@segmentActionSkip": {}, + "rememberAudioSelectionsDesc": "Zkuste nastavit zvukovou stopu tak, aby se co nejvíce shodovala s posledním videem.", + "@rememberAudioSelectionsDesc": {}, + "mediaSegmentCommercial": "Reklama", + "@mediaSegmentCommercial": {}, + "off": "Vypnuto", + "@off": {}, + "qualityOptionsAuto": "Auto", + "@qualityOptionsAuto": {}, + "subtitle": "Titulek", + "@subtitle": {}, + "deleteFilterConfirmation": "Opravdu chcete tento filtr smazat?", + "@deleteFilterConfirmation": {}, + "episodeAvailable": "Dostupná", + "@episodeAvailable": {}, + "similarToLikedItem": "Podobné položce, která se vám líbila", + "@similarToLikedItem": {}, + "homeStreamingQualityDesc": "Maximální kvalita streamování při připojení k domácí síti", + "@homeStreamingQualityDesc": {}, + "homeStreamingQualityTitle": "Kvalita v domácí síti", + "@homeStreamingQualityTitle": {}, + "playbackTrackSelection": "Výběr skladby pro přehrávání", + "@playbackTrackSelection": {}, + "masonry": "Dlaždice", + "@masonry": {}, + "settingsNextUpCutoffDays": "Počet dní pro nadcházející epizodu", + "@settingsNextUpCutoffDays": {}, + "videoScalingFill": "Roztáhnout", + "@videoScalingFill": {}, + "videoScalingFillScreenDesc": "Vyplnit i oblast navigace a stavového řádku", + "@videoScalingFillScreenDesc": {}, + "videoScalingFillScreenNotif": "Režim na celou obrazovku přepíše přizpůsobení videa při otočení na šířku", + "@videoScalingFillScreenNotif": {}, + "videoScalingFillScreenTitle": "Celá obrazovka", + "@videoScalingFillScreenTitle": {}, + "videoScalingFitHeight": "Přizpůsobit výšce", + "@videoScalingFitHeight": {}, + "videoScalingFitWidth": "Přizpůsobit šířce", + "@videoScalingFitWidth": {}, + "videoScalingScaleDown": "Zmenšit aby se vešlo", + "@videoScalingScaleDown": {}, + "aboutSocials": "Sociální sítě", + "@aboutSocials": {}, + "aboutLicenses": "Licence", + "@aboutLicenses": {}, + "subtitleConfiguration": "Konfigurace titulků", + "@subtitleConfiguration": {}, + "screenBrightness": "Jas obrazovky", + "@screenBrightness": {}, + "scale": "Škála", + "@scale": {}, + "playBackSettings": "Nastavení přehrávání", + "@playBackSettings": {}, + "settingsAutoNextTitle": "Nadcházející náhled", + "@settingsAutoNextTitle": {}, + "autoNextOffSmartTitle": "Chytrý", + "@autoNextOffSmartTitle": {}, + "playbackRate": "Rychlost přehrávání", + "@playbackRate": {}, + "speed": "Rychlost", + "@speed": {}, + "errorOpeningMedia": "Při přehrávání tohoto média se něco pokazilo", + "@errorOpeningMedia": {}, + "unableToPlayBooksOnWeb": "Knihy momentálně nejsou na webu podporovány", + "@unableToPlayBooksOnWeb": {}, + "defaultFilterForLibrary": "Výchozí filtr pro knihovnu", + "@defaultFilterForLibrary": {}, + "updateFilterForLibrary": "Aktualizovat filtr", + "@updateFilterForLibrary": {}, + "libraryFiltersLimitReached": "Dosažen limit filtrů (10), odstraňte některé filtry", + "@libraryFiltersLimitReached": {}, + "libraryFiltersRemoveAll": "Odebrat všechny filtry", + "@libraryFiltersRemoveAll": {}, + "libraryFiltersRemoveAllConfirm": "Toto smaže všechny uložené filtry pro každou knihovnu", + "@libraryFiltersRemoveAllConfirm": {}, + "playerSettingsOrientationTitle": "Orientace přehrávače", + "@playerSettingsOrientationTitle": {}, + "playerSettingsOrientationDesc": "Vynucení určité orientace přehrávače videa", + "@playerSettingsOrientationDesc": {}, + "deviceOrientationPortraitUp": "Vertikálně nahoru", + "@deviceOrientationPortraitUp": {}, + "deviceOrientationPortraitDown": "Vertikálně dolů", + "@deviceOrientationPortraitDown": {}, + "deviceOrientationLandscapeRight": "Horizontálně vpravo", + "@deviceOrientationLandscapeRight": {}, + "clientSettingsSchemeVariantTitle": "Varianta schématu", + "@clientSettingsSchemeVariantTitle": {}, + "schemeSettingsTonalSpot": "Tónová skvrna", + "@schemeSettingsTonalSpot": {}, + "schemeSettingsFidelity": "Věrnost", + "@schemeSettingsFidelity": {}, + "schemeSettingsMonochrome": "Monochromatický", + "@schemeSettingsMonochrome": {}, + "schemeSettingsNeutral": "Neutrální", + "@schemeSettingsNeutral": {}, + "schemeSettingsVibrant": "Živé", + "@schemeSettingsVibrant": {}, + "schemeSettingsExpressive": "Expresivní", + "@schemeSettingsExpressive": {}, + "schemeSettingsContent": "Content", + "@schemeSettingsContent": {}, + "schemeSettingsRainbow": "Duha", + "@schemeSettingsRainbow": {}, + "schemeSettingsFruitSalad": "Ovocný salát", + "@schemeSettingsFruitSalad": {}, + "clientSettingsRequireWifiTitle": "Vyžadovat Wi-Fi", + "@clientSettingsRequireWifiTitle": {}, + "clientSettingsRequireWifiDesc": "Stahovat pouze při připojení k síti Wi-Fi", + "@clientSettingsRequireWifiDesc": {}, + "libraryShuffleAndPlayItems": "Zamíchat a přehrát položky", + "@libraryShuffleAndPlayItems": {}, + "libraryPlayItems": "Přehrát položky", + "@libraryPlayItems": {}, + "clientSettingsShowAllCollectionsTitle": "Zobrazit všechny typy kolekcí", + "@clientSettingsShowAllCollectionsTitle": {}, + "stop": "Zastavit", + "@stop": {}, + "resumeVideo": "Obnovit video", + "@resumeVideo": {}, + "closeVideo": "Zavřít video", + "@closeVideo": {}, + "playNextVideo": "Přehrát další video", + "@playNextVideo": {}, + "playerSettingsBackendTitle": "Backend přehrávače videa", + "@playerSettingsBackendTitle": {}, + "mediaSegmentIntro": "Začátek", + "@mediaSegmentIntro": {}, + "errorLogs": "Protokoly chyb", + "@errorLogs": {}, + "settingsLayoutSizesTitle": "Velikosti rozvržení", + "@settingsLayoutSizesTitle": {}, + "settingsLayoutModesTitle": "Režimy rozvržení", + "@settingsLayoutModesTitle": {}, + "settingsLayoutModesDesc": "Určete, zda aplikace může používat rozvržení s jedním nebo dvěma panely", + "@settingsLayoutModesDesc": {}, + "tablet": "Tablet", + "@tablet": {}, + "desktop": "Počítač", + "@desktop": {}, + "layoutModeDual": "Dvojitý", + "@layoutModeDual": {}, + "internetStreamingQualityTitle": "Kvalita přes internet", + "@internetStreamingQualityTitle": {}, + "qualityOptionsOriginal": "Originál", + "@qualityOptionsOriginal": {}, + "mediaSegmentActions": "Akce segmentu médií", + "@mediaSegmentActions": {}, + "segmentActionNone": "Žádný", + "@segmentActionNone": {}, + "loading": "Načítání", + "@loading": {}, + "exitFladderTitle": "Ukončit Fladder", + "@exitFladderTitle": {}, + "maxConcurrentDownloadsTitle": "Maximální počet souběžných stahování", + "@maxConcurrentDownloadsTitle": {}, + "maxConcurrentDownloadsDesc": "Nastavuje maximální počet stahování, která mohou být spuštěna současně. Nastavením na 0 limit deaktivujete.", + "@maxConcurrentDownloadsDesc": {}, + "similarToRecentlyPlayed": "Podobné položce, kterou jste nedávno viděli", + "@similarToRecentlyPlayed": {}, + "hasActorFromRecentlyPlayed": "Stejný/á herec/herečka jako nedávno shlédnutá položka", + "@hasActorFromRecentlyPlayed": {}, + "hasLikedDirector": "Oblíbený/á režisér/ka", + "@hasLikedDirector": {}, + "hasLikedActor": "Oblíbený/á herec/herečka", + "@hasLikedActor": {}, + "recommended": "Doporučeno", + "@recommended": {}, + "latest": "Nejnovější", + "@latest": {}, + "playbackType": "Typ přehrávání", + "@playbackType": {}, + "playbackTypeDirect": "Přímý", + "@playbackTypeDirect": {}, + "playbackTypeTranscode": "Překódovaný", + "@playbackTypeTranscode": {}, + "playbackTypeOffline": "Offline", + "@playbackTypeOffline": {}, + "latestReleases": "Nejnovější vydání", + "@latestReleases": {}, + "newReleaseFoundTitle": "Aktualizace {newRelease} je k dispozici!", + "@newReleaseFoundTitle": { + "placeholders": { + "newRelease": { + "type": "String" + } + } + }, + "newUpdateFoundOnGithub": "Na Githubu je nová aktualizaci", + "@newUpdateFoundOnGithub": {}, + "downloadFile": "Stáhnout {type}", + "@downloadFile": { + "placeholders": { + "type": { + "type": "String" + } + } + }, + "qualityOptionsTitle": "Možnosti kvality", + "@qualityOptionsTitle": {}, + "version": "Verze", + "@version": {}, + "segmentActionAskToSkip": "Zeptat se na přeskočení", + "@segmentActionAskToSkip": {}, + "hasDirectorFromRecentlyPlayed": "Stejný/á režisér/ka jako nedávno shlédnutá položka", + "@hasDirectorFromRecentlyPlayed": {}, + "internetStreamingQualityDesc": "Maximální kvalita streamování přes internet (mobilní zařízení)", + "@internetStreamingQualityDesc": {} +} diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index b2461c8..3aa3d88 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -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": {} } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f392f79..0232257 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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" } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index dbf7d12..0d54227 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -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": {} } diff --git a/lib/l10n/app_mr.arb b/lib/l10n/app_mr.arb new file mode 100644 index 0000000..212ceb3 --- /dev/null +++ b/lib/l10n/app_mr.arb @@ -0,0 +1,1082 @@ +{ + "about": "Fladder बद्दल", + "@about": {}, + "accept": "स्वीकार करा", + "@accept": {}, + "addToCollection": "संग्रहात जोडा", + "@addToCollection": {}, + "advanced": "प्रगत", + "@advanced": {}, + "all": "सर्व", + "@all": {}, + "amoledBlack": "AMOLED काळा", + "@amoledBlack": {}, + "appLockBiometrics": "बायोमेट्रिक्स", + "@appLockBiometrics": {}, + "appLockPasscode": "पास-कोड", + "@appLockPasscode": {}, + "autoPlay": "आपोआप-प्ले", + "@autoPlay": {}, + "bold": "ठळक", + "@bold": {}, + "change": "बदला", + "@change": {}, + "clearAllSettings": "सर्व सेटिंग साफ करा", + "@clearAllSettings": {}, + "clearAllSettingsQuestion": "सर्व सेटिंग साफ करायचे का?", + "@clearAllSettingsQuestion": {}, + "clearChanges": "बदल साफ करा", + "@clearChanges": {}, + "color": "रंग", + "@color": {}, + "combined": "मिळून", + "@combined": {}, + "controls": "कंट्रोल", + "@controls": {}, + "dashboardContinueReading": "वाचणे चालू ठेवा", + "@dashboardContinueReading": {}, + "nextUp": "यापुढे", + "@nextUp": {}, + "dateAdded": "जोडण्याचा दिनांक", + "@dateAdded": {}, + "days": "दिवस", + "@days": {}, + "disabled": "बंद", + "@disabled": {}, + "downloadsClearTitle": "सिंक केलेला डेटा साफ करून टाका", + "@downloadsClearTitle": {}, + "downloadsSyncedData": "सिंक केलेला डेटा", + "@downloadsSyncedData": {}, + "empty": "रिकामा", + "@empty": {}, + "enabled": "चालू", + "@enabled": {}, + "error": "त्रुटी", + "@error": {}, + "failedToLoadImage": "चित्र लोड करता आले नाही", + "@failedToLoadImage": {}, + "folders": "फोल्डर", + "@folders": {}, + "forceRefresh": "जबरदस्ती रिफ्रेश करा", + "@forceRefresh": {}, + "goTo": "येथे जा", + "@goTo": {}, + "grid": "ग्रिड", + "@grid": {}, + "group": "गट", + "@group": {}, + "hide": "लपवा", + "@hide": {}, + "hideEmpty": "रिकाम्या लपवा", + "@hideEmpty": {}, + "immediately": "ताबडतोब", + "@immediately": {}, + "info": "माहिती", + "@info": {}, + "libraryFetchNoItemsFound": "कोणत्याही वस्तू सापडल्या नाहीत. सेटिंग बदलून पाहा.", + "@libraryFetchNoItemsFound": {}, + "lockscreen": "लॉकस्क्रीन", + "@lockscreen": {}, + "login": "लॉगइन", + "@login": {}, + "logout": "लॉगआउट", + "@logout": {}, + "markAsUnwatched": "न पाहिलेले असे चिन्हित करा", + "@markAsUnwatched": {}, + "mediaTypeBook": "पुस्तक", + "@mediaTypeBook": {}, + "mediaTypeEpisode": "एपिसोड", + "@mediaTypeEpisode": {}, + "mediaTypeMovie": "चित्रपट", + "@mediaTypeMovie": {}, + "mediaTypePerson": "व्यक्ती", + "@mediaTypePerson": {}, + "mediaTypePhotoAlbum": "फोटो अल्बम", + "@mediaTypePhotoAlbum": {}, + "mediaTypeSeason": "सीझन", + "@mediaTypeSeason": {}, + "mediaTypeSeries": "मालिका", + "@mediaTypeSeries": {}, + "mode": "मोड", + "@mode": {}, + "musicAlbum": "अल्बम", + "@musicAlbum": {}, + "name": "नाव", + "@name": {}, + "noResults": "निकाल नाहीत", + "@noResults": {}, + "noServersFound": "कोणतेही नवीन सर्व्हर सापडले नाहीत", + "@noServersFound": {}, + "none": "कोणतेही नाही", + "@none": {}, + "notPartOfAlbum": "अल्बमचा भाग नाही", + "@notPartOfAlbum": {}, + "openWebLink": "वेब लिंक उघडा", + "@openWebLink": {}, + "password": "पासवर्ड", + "@password": {}, + "pathEditTitle": "स्थान बदला", + "@pathEditTitle": {}, + "playCount": "प्ले केलेल्याची मोजणी", + "@playCount": {}, + "playFromStart": "{name} सुरुवातीपासून प्ले करा", + "@playFromStart": { + "description": "speel vanaf het begin", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playVideos": "व्हिडिओ प्ले करा", + "@playVideos": {}, + "quickConnectInputACode": "कोड प्रविष्ट करा", + "@quickConnectInputACode": {}, + "quickConnectTitle": "जलद-कनेक्ट", + "@quickConnectTitle": {}, + "quickConnectAction": "यासाठी जलद-कनेक्ट कोड प्रविष्ट करा", + "@quickConnectAction": {}, + "read": "{item} वाचा", + "@read": { + "description": "read", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "refresh": "रिफ्रेश", + "@refresh": {}, + "related": "यासंबंधी", + "@related": {}, + "removeAsFavorite": "आवडीच्यांमधून काढून टाका", + "@removeAsFavorite": {}, + "restart": "रिस्टार्ट करा", + "@restart": {}, + "result": "निकाल", + "@result": {}, + "resumable": "चालू ठेवता येण्यासारखे", + "@resumable": {}, + "save": "सेव्ह करा", + "@save": {}, + "saved": "सेव्ह केले", + "@saved": {}, + "search": "शोध घ्या", + "@search": {}, + "selectAll": "सर्व निवडा", + "@selectAll": {}, + "selectViewType": "दृश्य प्रकार निवडा", + "@selectViewType": {}, + "separate": "विभाजित", + "@separate": {}, + "settingsClientTitle": "फ्लॅडर", + "@settingsClientTitle": {}, + "settingsContinue": "चालू ठेवा", + "@settingsContinue": {}, + "showAlbum": "अल्बम दाखवा", + "@showAlbum": {}, + "showEmpty": "रिकामे दाखवा", + "@showEmpty": {}, + "sortName": "नाव", + "@sortName": {}, + "start": "सुरू करा", + "@start": {}, + "themeModeDark": "गडद", + "@themeModeDark": {}, + "unPlayed": "प्ले न केलेले", + "@unPlayed": {}, + "totalSize": "एकूण आकार: {size}", + "@totalSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, + "unknown": "अज्ञात", + "@unknown": {}, + "useDefaults": "डिफॉल्ट वापरा", + "@useDefaults": {}, + "userName": "प्रयोक्ता नाव", + "@userName": {}, + "videoScalingFill": "भरा", + "@videoScalingFill": {}, + "videoScalingFillScreenTitle": "पडदा भरा", + "@videoScalingFillScreenTitle": {}, + "videoScalingFitHeight": "उंची भरा", + "@videoScalingFitHeight": {}, + "videoScalingFitWidth": "रुंदी भरा", + "@videoScalingFitWidth": {}, + "addItemsToCollection": "{itemLength} वस्तू संग्रहात जोडा", + "@addItemsToCollection": { + "placeholders": { + "itemLength": { + "type": "int" + } + } + }, + "addedToPlaylist": "{playlistName} प्लेलिस्टमध्ये जोडले गेले", + "@addedToPlaylist": { + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "removedFromCollection": "{collectionName} संग्रहातून काढून टाकले गेले", + "@removedFromCollection": { + "placeholders": { + "collectionName": { + "type": "String" + } + } + }, + "syncStatusNotFound": "सापडले नाही", + "@syncStatusNotFound": {}, + "syncOverlayDeleting": "सिंक केलेली वस्तू काढून टाकत आहे", + "@syncOverlayDeleting": {}, + "mediaSegmentUnknown": "अज्ञात", + "@mediaSegmentUnknown": {}, + "mediaSegmentCommercial": "जाहिरात", + "@mediaSegmentCommercial": {}, + "mediaSegmentOutro": "आउट्रो", + "@mediaSegmentOutro": {}, + "mediaSegmentIntro": "इनट्रो", + "@mediaSegmentIntro": {}, + "ascending": "चढत्या क्रमात", + "@ascending": {}, + "clearSelection": "निवड साफ करा", + "@clearSelection": {}, + "dashboardContinue": "चालू ठेवा", + "@dashboardContinue": {}, + "continuePage": "पुढे - पान {page}", + "@continuePage": { + "description": "Continue - page 1", + "placeholders": { + "page": { + "type": "int" + } + } + }, + "mouseDragSupport": "माउसने खेचा", + "@mouseDragSupport": {}, + "playLabel": "प्ले करा", + "@playLabel": {}, + "themeModeSystem": "सिस्टम", + "@themeModeSystem": {}, + "dashboard": "मुख्य पान", + "@dashboard": {}, + "dashboardContinueListening": "ऐकणे चालू ठेवा", + "@dashboardContinueListening": {}, + "removeFromCollection": "संग्रहातून काढून टाका", + "@removeFromCollection": {}, + "datePlayed": "प्ले केलेल्याचा दिनांक", + "@datePlayed": {}, + "displayLanguage": "दर्शनाची भाषा", + "@displayLanguage": {}, + "favorites": "आवडीचे", + "@favorites": {}, + "fontSize": "फाँटचा आकार", + "@fontSize": {}, + "home": "घर", + "@home": {}, + "selected": "निवडलेले", + "@selected": {}, + "mediaTypeFolder": "फोल्डर", + "@mediaTypeFolder": {}, + "navigationSync": "सिंक केले", + "@navigationSync": {}, + "refreshPopup": "रिफ्रेश करा - {name}", + "@refreshPopup": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "invalidUrlDesc": "URL ची सुरुवात http(s):// अशी झाली पाहिजे", + "@invalidUrlDesc": {}, + "list": "यादी", + "@list": {}, + "syncStatusComplete": "पूर्ण", + "@syncStatusComplete": {}, + "metaDataSavedFor": "{item} यासाठी मेटाडेटा सेव्ह केला गेला", + "@metaDataSavedFor": { + "description": "metaDataSavedFor", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "noRating": "रेटिंग नाही", + "@noRating": {}, + "readFromStart": "{item} सुरुवातीपासून वाचा", + "@readFromStart": { + "description": "Read book from start", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "options": "पर्याय", + "@options": {}, + "playFrom": "{name} पासून प्ले करा", + "@playFrom": { + "description": "playFrom", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "removeFromPlaylist": "प्लेलिस्टमधून काढून टाका", + "@removeFromPlaylist": {}, + "themeModeLight": "हलका", + "@themeModeLight": {}, + "resume": "{item} चालू ठेवा", + "@resume": { + "description": "resume", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "settingsProfileDesc": "लॉकस्क्रीन", + "@settingsProfileDesc": {}, + "addToNewCollection": "नवीन संग्रह", + "@addToNewCollection": {}, + "selectedWith": "{info} निवडलेले", + "@selectedWith": { + "description": "selected", + "placeholders": { + "info": { + "type": "String" + } + } + }, + "settings": "सेटिंग", + "@settings": {}, + "settingsProfileTitle": "प्रोफाइल", + "@settingsProfileTitle": {}, + "settingsQuickConnectTitle": "जलद-कनेक्ट", + "@settingsQuickConnectTitle": {}, + "switchUser": "प्रयोक्ता बदला", + "@switchUser": {}, + "sync": "सिंक करा", + "@sync": {}, + "theme": "थीम", + "@theme": {}, + "addItemsToPlaylist": "{itemLength} वस्तू प्लेसिस्टमध्ये जोडा", + "@addItemsToPlaylist": { + "placeholders": { + "itemLength": { + "type": "int" + } + } + }, + "syncStatusFailed": "अपयशी", + "@syncStatusFailed": {}, + "timeAndAnnotation": "{minutes} आणि {seconds}", + "@timeAndAnnotation": { + "description": "timeAndAnnotation", + "placeholders": { + "minutes": { + "type": "String" + }, + "seconds": { + "type": "String" + } + } + }, + "viewPhotos": "फोटो पाहा", + "@viewPhotos": {}, + "watchOn": "यावर पाहा", + "@watchOn": {}, + "syncStatusPaused": "पॉझ केले", + "@syncStatusPaused": {}, + "syncOverlaySyncing": "वस्तू तपशील सिंक करत आहे", + "@syncOverlaySyncing": {}, + "syncSelectDownloadsFolder": "डाउनलोड फोल्डर निवडा", + "@syncSelectDownloadsFolder": {}, + "active": "सक्रिय", + "@active": {}, + "nativeName": "मराठी", + "@nativeName": {}, + "mediaTypePlaylist": "प्लेलिस्ट", + "@mediaTypePlaylist": {}, + "retry": "पुन्हा प्रयत्न करा", + "@retry": {}, + "syncStatusCanceled": "रद्द केले गेले", + "@syncStatusCanceled": {}, + "cancel": "रद्द करा", + "@cancel": {}, + "code": "कोड", + "@code": {}, + "downloadsTitle": "डाउनलोड", + "@downloadsTitle": {}, + "endsAt": "{date} ला समाप्त", + "@endsAt": { + "description": "endsAt", + "placeholders": { + "date": { + "type": "DateTime", + "format": "jm" + } + } + }, + "fontColor": "फाँटचा रंग", + "@fontColor": {}, + "homeBannerSlideshow": "स्लाइडशो", + "@homeBannerSlideshow": {}, + "reWatch": "पुन्हा-पाहणे", + "@reWatch": {}, + "shadow": "सावली", + "@shadow": {}, + "showDetails": "तपशील दाखवा", + "@showDetails": {}, + "themeColor": "थीमचा रंग", + "@themeColor": {}, + "video": "व्हिडिओ", + "@video": {}, + "addedToCollection": "{collectionName} या संग्रहात जोडले गेले", + "@addedToCollection": { + "placeholders": { + "collectionName": { + "type": "String" + } + } + }, + "syncStatusSynced": "सिंक केले", + "@syncStatusSynced": {}, + "noItemsSynced": "कोणत्याही वस्तू सिंक केल्या नाहीत", + "@noItemsSynced": {}, + "noItemsToShow": "दाखवण्यास कोणत्याही वस्तू नाहीत", + "@noItemsToShow": {}, + "other": "इतर", + "@other": {}, + "play": "{item} प्ले करा", + "@play": { + "description": "Play with", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "selectTime": "वेळ निवडा", + "@selectTime": {}, + "addToNewPlaylist": "नवीन प्लेलिस्ट", + "@addToNewPlaylist": {}, + "scanningName": "स्कॅन करत आहे - {name}…", + "@scanningName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "descending": "उतरत्या क्रमात", + "@descending": {}, + "mediaTypePhoto": "फोटो", + "@mediaTypePhoto": {}, + "delete": "डिलीट करा", + "@delete": {}, + "invalidUrl": "अवैध URL", + "@invalidUrl": {}, + "itemCount": "वस्तूंची मोजणी: {count}", + "@itemCount": { + "description": "Item count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "loggedIn": "लॉगइन झालेले", + "@loggedIn": {}, + "openShow": "मालिका उघडा", + "@openShow": {}, + "server": "सर्व्हर", + "@server": {}, + "audio": "ऑडिओ", + "@audio": {}, + "clear": "साफ करा", + "@clear": {}, + "editMetadata": "मेटाडेटा संपादित करा", + "@editMetadata": {}, + "incorrectPinTryAgain": "चुकीचा पीन. पुन्हा प्रयत्न करा.", + "@incorrectPinTryAgain": {}, + "markAsWatched": "पाहिलेले असे चिन्हित करा", + "@markAsWatched": {}, + "masonry": "भिंतीसारखे", + "@masonry": {}, + "navigationFavorites": "आवडीचे", + "@navigationFavorites": {}, + "played": "प्ले केलेले", + "@played": {}, + "collectionFolder": "संग्रह फोल्डर", + "@collectionFolder": {}, + "downloadsPath": "पाथ", + "@downloadsPath": {}, + "favorite": "आवडीचा", + "@favorite": {}, + "moreOptions": "अधिक पर्याय", + "@moreOptions": {}, + "addAsFavorite": "आवडीचे म्हणून जोडा", + "@addAsFavorite": {}, + "addToPlaylist": "प्लेलिस्टमध्ये जोडा", + "@addToPlaylist": {}, + "appLockAutoLogin": "आपोआप लॉगइन करा", + "@appLockAutoLogin": {}, + "close": "बंद करा", + "@close": {}, + "deleteItem": "{item} डिलीट करायचे का?", + "@deleteItem": { + "description": "deleteItem", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "disableFilters": "गाळण्या बंद करा", + "@disableFilters": {}, + "groupBy": "असा गट बनवा", + "@groupBy": {}, + "homeBannerCarousel": "कॅरूझेल", + "@homeBannerCarousel": {}, + "navigationDashboard": "मुख्य पान", + "@navigationDashboard": {}, + "never": "कधीच नाही", + "@never": {}, + "page": "पान {index}", + "@page": { + "description": "page", + "placeholders": { + "index": { + "type": "int" + } + } + }, + "quickConnectWrongCode": "चुकीचा कोड", + "@quickConnectWrongCode": {}, + "dashboardContinueWatching": "पाहणे चालू ठेवा", + "@dashboardContinueWatching": {}, + "logoutUserPopupTitle": "{userName} यांना लॉगआउट करायचे का?", + "@logoutUserPopupTitle": { + "description": "Pop-up for logging out the user", + "placeholders": { + "userName": { + "type": "String" + } + } + }, + "refreshMetadata": "मेटाडेटा रिफ्रेश करा", + "@refreshMetadata": {}, + "settingsPosterSize": "पोस्टरचा आकार", + "@settingsPosterSize": {}, + "syncStatusRunning": "चालू", + "@syncStatusRunning": {}, + "aboutSocials": "सोशल मीडियावर", + "@aboutSocials": {}, + "playBackSettings": "प्लेबॅक सेटिंग", + "@playBackSettings": {}, + "unableToPlayBooksOnWeb": "पुस्तके सध्या वेब आवृत्तीत समर्थित नाहीत", + "@unableToPlayBooksOnWeb": {}, + "libraryFiltersRemoveAll": "सर्व गाळण्या काढून टाका", + "@libraryFiltersRemoveAll": {}, + "schemeSettingsRainbow": "इंद्रधनुष्य", + "@schemeSettingsRainbow": {}, + "schemeSettingsFruitSalad": "फ्रुट सॅलड", + "@schemeSettingsFruitSalad": {}, + "clientSettingsShowAllCollectionsTitle": "सर्व संग्रह प्रकार दाखवा", + "@clientSettingsShowAllCollectionsTitle": {}, + "stop": "थांबवा", + "@stop": {}, + "playNextVideo": "पुढचा व्हिडिओ प्ले करा", + "@playNextVideo": {}, + "external": "बाह्य", + "@external": {}, + "errorLogs": "त्रुटी नोंदवही", + "@errorLogs": {}, + "tablet": "टॅबलेट", + "@tablet": {}, + "desktop": "डेस्कटॉप", + "@desktop": {}, + "internetStreamingQualityTitle": "इंटरनेट गुणवत्ता", + "@internetStreamingQualityTitle": {}, + "qualityOptionsTitle": "गुणवत्ता पर्याय", + "@qualityOptionsTitle": {}, + "segmentActionAskToSkip": "स्किप करण्यास विचारा", + "@segmentActionAskToSkip": {}, + "libraryPlayItems": "वस्तू प्ले करा", + "@libraryPlayItems": {}, + "closeVideo": "व्हिडिओ बंद करा", + "@closeVideo": {}, + "layoutModeDual": "दुहेरी", + "@layoutModeDual": {}, + "phone": "फोन", + "@phone": {}, + "segmentActionSkip": "स्किप करा", + "@segmentActionSkip": {}, + "playbackRate": "प्लेबॅक दर", + "@playbackRate": {}, + "defaultLabel": "डिफॉल्ट", + "@defaultLabel": { + "description": "To indicate a default value, default video player backend" + }, + "episodeAvailable": "उपलब्ध", + "@episodeAvailable": {}, + "updateFilterForLibrary": "गाळणी अपडेट करा", + "@updateFilterForLibrary": {}, + "playerSettingsBackendTitle": "व्हिडिओ प्लेयर बॅकएंड", + "@playerSettingsBackendTitle": {}, + "layoutModeSingle": "एकेरी", + "@layoutModeSingle": {}, + "clientSettingsRequireWifiTitle": "वाय-फाय आवश्यक", + "@clientSettingsRequireWifiTitle": {}, + "aboutCreatedBy": "DonutWare द्वारा निर्मित", + "@aboutCreatedBy": {}, + "copiedToClipboard": "क्लिपबोर्डवर कॉपी केले", + "@copiedToClipboard": {}, + "version": "आवृत्ती", + "@version": {}, + "homeStreamingQualityTitle": "घरातील गुणवत्ता", + "@homeStreamingQualityTitle": {}, + "resumeVideo": "व्हिडिओ चालू ठेवा", + "@resumeVideo": {}, + "noVideoPlayerOptions": "निवडलेल्या बॅकएंडमध्ये कोणतेही पर्याय नाहीत", + "@noVideoPlayerOptions": {}, + "qualityOptionsOriginal": "मूळ", + "@qualityOptionsOriginal": {}, + "loading": "लोड करत आहे", + "@loading": {}, + "removeFilterForLibrary": "{filter} काढून टाकायचे का?", + "@removeFilterForLibrary": { + "description": "removeFilterForLibrary", + "placeholders": { + "filter": { + "type": "String" + } + } + }, + "downloadFile": "{type} डाउनलोड करा", + "@downloadFile": { + "placeholders": { + "type": { + "type": "String" + } + } + }, + "qualityOptionsAuto": "आपोआप", + "@qualityOptionsAuto": {}, + "segmentActionNone": "कोणतेही नाही", + "@segmentActionNone": {}, + "unableToSyncItem": "{item} सिंक करता आले नाही, काहीतरी गडबड झाली", + "@unableToSyncItem": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "deleteFilterConfirmation": "तुम्हाला ही गाळणी नक्की डिलीट करायची आहे का?", + "@deleteFilterConfirmation": {}, + "aboutVersion": "आवृत्ती: {version}", + "@aboutVersion": { + "placeholders": { + "version": { + "type": "String" + } + } + }, + "speed": "गती", + "@speed": {}, + "off": "बंद", + "@off": {}, + "screenBrightness": "पडद्याचा तेजस्वीपणा", + "@screenBrightness": {}, + "subtitle": "सबटायटल", + "@subtitle": {}, + "aboutBuild": "बिल्ड: {buildNumber}", + "@aboutBuild": { + "placeholders": { + "buildNumber": { + "type": "String" + } + } + }, + "startedSyncingItem": "{item} सिंक करणे सुरू केले", + "@startedSyncingItem": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "syncAddItemForSyncing": "सिंक करण्यास {item} जोडले गेले", + "@syncAddItemForSyncing": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "backgroundBlur": "पार्श्वभूमीचा ब्लर", + "@backgroundBlur": {}, + "backgroundOpacity": "पार्श्वभूमीची अपारदर्शकता", + "@backgroundOpacity": {}, + "biometricsFailedCheckAgain": "बायोमेट्रिक्स अपयशी. सेटिंग तपासा आणि पुन्हा प्रयत्न करा.", + "@biometricsFailedCheckAgain": {}, + "chapter": "{count, plural, other{अध्याय} one{अध्याय}}", + "@chapter": { + "description": "chapter", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "deleteFileFromSystem": "{item} ही वस्तू डिलिट केल्याने ती फाईल सिस्टम आणि मीडिया लायब्ररी दोघांमधून डिलिट केली जाईल. तुम्हाला नक्की असे करायचे आहे का?", + "@deleteFileFromSystem": { + "description": "Delete file from system", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "episode": "{count, plural, other{भाग} one{भाग}}", + "@episode": { + "description": "episode", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "filter": "{count, plural, other{गाळण्या} one{गाळणी}}", + "@filter": { + "description": "filter", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "genre": "{count, plural, other{जाँरे} one{जाँरे}}", + "@genre": { + "description": "genre", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "identify": "ओळख पटवा", + "@identify": {}, + "libraryPageSizeTitle": "लायब्ररी पानाचा आकार", + "@libraryPageSizeTitle": {}, + "mediaTypeBoxset": "बॉक्स सेट", + "@mediaTypeBoxset": {}, + "metadataRefreshDefault": "नवीन आणि अपडेट केलेल्या फायलींचा स्कॅन करा", + "@metadataRefreshDefault": {}, + "metadataRefreshFull": "सर्व मेटाडेटा बदलून घ्या", + "@metadataRefreshFull": {}, + "metadataRefreshValidation": "नसलेल्या मेटाडेटाचा शोध घ्या", + "@metadataRefreshValidation": {}, + "minutes": "{count, plural, other{मिनिटे} one{मिनिट} }", + "@minutes": { + "description": "minute", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "normal": "साधारण", + "@normal": {}, + "openParent": "पालक उघडा", + "@openParent": {}, + "outlineColor": "बाह्यरेखेचा रंग", + "@outlineColor": {}, + "overview": "सारांश", + "@overview": {}, + "pathClearTitle": "डाउनलोड पाथ रिंकामा करा", + "@pathClearTitle": {}, + "recursive": "रिकर्सिव्ह", + "@recursive": {}, + "releaseDate": "प्रदर्शन दिनांक", + "@releaseDate": {}, + "replaceExistingImages": "अस्तित्वातील चित्रे बदलून घ्या", + "@replaceExistingImages": {}, + "replaceAllImages": "सर्व चित्रे बदलून घ्या", + "@replaceAllImages": {}, + "retrievePublicListOfUsers": "प्रयोक्त्यांची सार्वजनिक यादी मिळवा", + "@retrievePublicListOfUsers": {}, + "scanLibrary": "लायब्ररी स्कॅन करा", + "@scanLibrary": {}, + "scrollToTop": "वरपर्यंत स्क्रोल करा", + "@scrollToTop": {}, + "season": "{count, plural, other{सीझन} one{सीझन} }", + "@season": { + "description": "season", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "set": "सेट करा", + "@set": { + "description": "Use for setting a certain value", + "context": "Set 'time'" + }, + "settingSecurityApplockTitle": "ॲप लॉक", + "@settingSecurityApplockTitle": {}, + "settingsEnableOsMediaControls": "OS मिडिया कंट्रोल सक्षम करा", + "@settingsEnableOsMediaControls": {}, + "settingsHomeBannerInformationTitle": "बॅनर माहिती", + "@settingsHomeBannerInformationTitle": {}, + "settingsPlayerTitle": "प्लेयर", + "@settingsPlayerTitle": {}, + "settingsPlayerCustomSubtitlesTitle": "सबटायटल सानुकूलित करा", + "@settingsPlayerCustomSubtitlesTitle": {}, + "settingsPlayerNativeLibassAccelTitle": "नेटिव्ह libass सबटायटल", + "@settingsPlayerNativeLibassAccelTitle": {}, + "settingsSecurity": "सुरक्षता", + "@settingsSecurity": {}, + "somethingWentWrong": "काहीतरी गडबड झाली", + "@somethingWentWrong": {}, + "somethingWentWrongPasswordCheck": "काहीतरी गडबड झाली. आपला पासवर्ड तपासा.", + "@somethingWentWrongPasswordCheck": {}, + "settingsVisual": "दृश्य संबंधित", + "@settingsVisual": {}, + "studio": "{count, plural, other{स्टुडियो} one{स्टुडियो}}", + "@studio": { + "description": "studio", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "subtitles": "सबटायटल", + "@subtitles": {}, + "syncOpenParent": "पालक उघडा", + "@syncOpenParent": {}, + "syncRemoveDataTitle": "सिंक केलेला डेटा काढून टाकायचा का?", + "@syncRemoveDataTitle": {}, + "syncedItems": "सिंक केलेल्या वस्तू", + "@syncedItems": {}, + "tag": "{count, plural, one{टॅग} other{टॅग}}", + "@tag": { + "description": "tag", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "unableToConnectHost": "होस्टला कनेक्ट करता आले नाही", + "@unableToConnectHost": {}, + "unableToReverseAction": "ही क्रिया मागे घेता येत नाही. याने सर्व सेटिंग काढून टाकले जातील.", + "@unableToReverseAction": {}, + "type": "{count, plural, other{प्रकार} one{प्रकार}}", + "@type": { + "description": "type", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "syncStatusWaitingToRetry": "पुन्हा प्रयत्न करण्यास प्रतीक्षा करत आहे", + "@syncStatusWaitingToRetry": {}, + "syncRemoveUnableToDeleteItem": "सिंक केलेली वस्तू काढून टाकता आली नाही, काहीतरी गडबड झाली", + "@syncRemoveUnableToDeleteItem": {}, + "aboutLicenses": "लायसंस", + "@aboutLicenses": {}, + "errorOpeningMedia": "हा मिडिया प्ले करताना काहीतरी गडबड झाली", + "@errorOpeningMedia": {}, + "defaultFilterForLibrary": "लायब्ररीसाठी डिफॉल्ट गाळणी", + "@defaultFilterForLibrary": {}, + "libraryFiltersLimitReached": "गाळण्यांची सीमा गाठली (10) काही गाळण्या काढून टाका", + "@libraryFiltersLimitReached": {}, + "skipButtonLabel": "{segment} स्किप करा (S)", + "@skipButtonLabel": { + "placeholders": { + "segment": { + "type": "String" + } + } + }, + "copyStreamUrl": "स्ट्रीम यु.आर.एल. कॉपी करा", + "@copyStreamUrl": {}, + "episodeUnaired": "अप्रदर्शित", + "@episodeUnaired": {}, + "episodeMissing": "नसलेले", + "@episodeMissing": {}, + "castAndCrew": "कलाकार & कर्मचारी", + "@castAndCrew": {}, + "hasLikedDirector": "आवडीचा दिग्दर्शक असलेला", + "@hasLikedDirector": {}, + "latest": "अलीकडील", + "@latest": {}, + "playbackTypeOffline": "ऑफलाईन", + "@playbackTypeOffline": {}, + "latestReleases": "अलीकडील प्रदर्शित", + "@latestReleases": {}, + "newUpdateFoundOnGithub": "Github वर नवीन आवृत्ती सापडली", + "@newUpdateFoundOnGithub": {}, + "exitFladderTitle": "Fladder बंद करा", + "@exitFladderTitle": {}, + "playbackTypeTranscode": "ट्रान्सकोड", + "@playbackTypeTranscode": {}, + "hasLikedActor": "आवडीचा अभिनेता असलेला", + "@hasLikedActor": {}, + "fetchingLibrary": "लायब्ररीतील वस्तू मिळवत आहे…", + "@fetchingLibrary": {}, + "libraryPageSizeDesc": "एका वेळी लोड करायची संख्या सेट करा. 0 असे सेट केल्याने पाने केली जाणार नाहीत.", + "@libraryPageSizeDesc": {}, + "moreFrom": "{info} यांच्याकडून अजून", + "@moreFrom": { + "description": "More from", + "placeholders": { + "info": { + "type": "String" + } + } + }, + "outlineSize": "बाह्यरेखेचा आकार", + "@outlineSize": {}, + "settingsHomeBannerTitle": "मुख्य पानाचा बॅनर", + "@settingsHomeBannerTitle": {}, + "newReleaseFoundTitle": "नवीन आवृत्ती {newRelease} उपलब्ध आहे!", + "@newReleaseFoundTitle": { + "placeholders": { + "newRelease": { + "type": "String" + } + } + }, + "communityRating": "सामाजिक रेटिंग", + "@communityRating": {}, + "random": "यादृच्छिक", + "@random": {}, + "settingsHomeBannerInformationDesc": "मुख्य पानाच्या बॅनरमध्ये दाखवायची माहिती", + "@settingsHomeBannerInformationDesc": {}, + "dashboardRecentlyAdded": "{name} मध्ये अलीकडील जोडलेले", + "@dashboardRecentlyAdded": { + "description": "Recently added on home screen", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playbackType": "प्लेबॅक प्रकार", + "@playbackType": {}, + "library": "{count, plural, other{लायब्ररी} one{लायब्ररी}}", + "@library": { + "description": "Plural", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "seconds": "{count, plural, other{सेकंदे} one{सेकंद}}", + "@seconds": { + "description": "second", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "director": "{count, plural, other{दिग्दर्शक} two{दिग्दर्शक}}", + "@director": { + "description": "director", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "writer": "{count, plural, other{लेखक} two{लेखक}}", + "@writer": { + "description": "writer", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "actor": "{count, plural, other{अभिनेते} one{अभिनेता}}", + "@actor": { + "description": "actor", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "label": "{count, plural, other{लेबल} one{लेबल}}", + "@label": { + "description": "label", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "settingsPlayerCustomSubtitlesDesc": "आकार, रंग, स्थान, बाह्यरेखा सानुकूलित करा", + "@settingsPlayerCustomSubtitlesDesc": {}, + "rating": "{count, plural, other{रेटिंग} one{रेटिंग}}", + "@rating": { + "description": "rating", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "year": "{count, plural, other{वर्षे} one{वर्ष}}", + "@year": { + "description": "year", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "libraryFiltersRemoveAllConfirm": "याने प्रत्येक लायब्ररीसाठी सेव्ह केलेल्या गाळण्या डिलिट केल्या जातील", + "@libraryFiltersRemoveAllConfirm": {}, + "playbackTrackSelection": "प्लेबॅक ट्रॅक निवड", + "@playbackTrackSelection": {}, + "rememberSubtitleSelections": "आधीच्या वस्तूनुसार सबटायटल ट्रॅक सेट करा", + "@rememberSubtitleSelections": {}, + "rememberAudioSelections": "आधीच्या वस्तूनुसार ऑडियो ट्रॅक सेट करा", + "@rememberAudioSelections": {} +} diff --git a/lib/l10n/app_mt.arb b/lib/l10n/app_mt.arb new file mode 100644 index 0000000..448db2c --- /dev/null +++ b/lib/l10n/app_mt.arb @@ -0,0 +1,1265 @@ +{ + "code": "Kodiċi", + "@code": {}, + "loggedIn": "Illoggjat", + "@loggedIn": {}, + "clear": "Neħħi", + "@clear": {}, + "communityRating": "Klassifikazzjoni Komunitarja", + "@communityRating": {}, + "accept": "Aċċetta", + "@accept": {}, + "appLockAutoLogin": "Login awtomatiku", + "@appLockAutoLogin": {}, + "clearChanges": "Neħħi l-bidliet", + "@clearChanges": {}, + "identify": "Identifika", + "@identify": {}, + "mediaTypePhoto": "Ritratt", + "@mediaTypePhoto": {}, + "metaDataSavedFor": "Metadejta ssejvjata għal {item}", + "@metaDataSavedFor": { + "description": "metaDataSavedFor", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "director": "{count, plural, other{Direttur} two{Diretturi}}", + "@director": { + "description": "director", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "deleteFileFromSystem": "It-tħassir ta’ dan l-item {item} se jħassru kemm mis-sistema tal-fajls kif ukoll mil-librerija tal-midja tiegħek. Żgur li tixtieq tkompli?", + "@deleteFileFromSystem": { + "description": "Delete file from system", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "libraryPageSizeDesc": "Issettja l-ammont biex tgħabbi kull darba. 0 titfi l-ippejġjar.", + "@libraryPageSizeDesc": {}, + "homeBannerCarousel": "Karużell", + "@homeBannerCarousel": {}, + "metadataRefreshFull": "Ibdel il-metadejta kollha", + "@metadataRefreshFull": {}, + "parentalRating": "Klassifikazzjoni tal-Ġenituri", + "@parentalRating": {}, + "quickConnectAction": "Daħħal kodiċi tal-konnessjoni veloċi għal", + "@quickConnectAction": {}, + "other": "Oħrajn", + "@other": {}, + "markAsWatched": "Immarka li rajtu", + "@markAsWatched": {}, + "noSuggestionsFound": "Ma nstab l-ebda suġġeriment", + "@noSuggestionsFound": {}, + "notPartOfAlbum": "Mhux parti minn album", + "@notPartOfAlbum": {}, + "pathEditDesc": "Dan il-post huwa ssettjat għall-utenti kollha, kwalunkwe dejta ssinkronizzata mhux se tibqa’ aċċessibbli. Se jibqa’ fuq l-istorage tiegħek.", + "@pathEditDesc": {}, + "quickConnectWrongCode": "Kodiċi ħażin", + "@quickConnectWrongCode": {}, + "rating": "{count, plural, other{Klassifikazzjonijiet} one{Klassifikazzjoni}}", + "@rating": { + "description": "rating", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "refreshMetadata": "Aġġorna l-metadejta", + "@refreshMetadata": {}, + "removeAsFavorite": "Neħħi mil-favorit", + "@removeAsFavorite": {}, + "metadataRefreshDefault": "Skennja għal fajls ġodda u aġġornati", + "@metadataRefreshDefault": {}, + "noResults": "Ebda riżultat", + "@noResults": {}, + "downloadsClearTitle": "Neħħi d-dejta sinkronizzata", + "@downloadsClearTitle": {}, + "clearAllSettings": "Neħħi l-issettjar kollu", + "@clearAllSettings": {}, + "logoutUserPopupContent": "Dan joħroġ {userName} u jħassar l-utent mill-applikazzjoni.\nIkollok terġa’ tidħol fuq {serverName}.", + "@logoutUserPopupContent": { + "description": "Pop-up for logging out the user description", + "placeholders": { + "userName": { + "type": "String" + }, + "serverName": { + "type": "String" + } + } + }, + "fontSize": "Daqs tal-font", + "@fontSize": {}, + "navigationSync": "Sinkronizzat", + "@navigationSync": {}, + "hideEmpty": "Aħbi l-vojt", + "@hideEmpty": {}, + "library": "{count, plural, other{Libreriji} one{Librerija}}", + "@library": { + "description": "Plural", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "mediaTypeBook": "Ktieb", + "@mediaTypeBook": {}, + "noItemsSynced": "Ebda item issinkronizzat", + "@noItemsSynced": {}, + "outlineColor": "Kulur tal-kontorn", + "@outlineColor": {}, + "playFromStart": "Ibda {name} mill-bidu", + "@playFromStart": { + "description": "speel vanaf het begin", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "restart": "Irristartja", + "@restart": {}, + "actor": "{count, plural, other{Atturi} one{Attur}}", + "@actor": { + "description": "actor", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "endsAt": "jintemm fi {date}", + "@endsAt": { + "description": "endsAt", + "placeholders": { + "date": { + "type": "DateTime", + "format": "jm" + } + } + }, + "navigationDashboard": "Daxxbord", + "@navigationDashboard": {}, + "clearSelection": "Neħħi l-għażla", + "@clearSelection": {}, + "mediaTypePhotoAlbum": "Album tar-Ritratti", + "@mediaTypePhotoAlbum": {}, + "nativeName": "Ingliż", + "@nativeName": {}, + "about": "Dwar", + "@about": {}, + "addAsFavorite": "Żid bħala favorit", + "@addAsFavorite": {}, + "addToCollection": "Żid mal-kollezzjoni", + "@addToCollection": {}, + "addToPlaylist": "Żid mal-playlist", + "@addToPlaylist": {}, + "advanced": "Avvanzat", + "@advanced": {}, + "all": "Kollha", + "@all": {}, + "amoledBlack": "Amoled iswed", + "@amoledBlack": {}, + "appLockBiometrics": "Bijometrika", + "@appLockBiometrics": {}, + "appLockTitle": "Issettja l-metodu log-in għal {userName}", + "@appLockTitle": { + "description": "Pop-up to pick a login method", + "placeholders": { + "userName": { + "type": "String" + } + } + }, + "ascending": "Telgħin", + "@ascending": {}, + "audio": "Awdjo", + "@audio": {}, + "biometricsFailedCheckAgain": "Il-bijometrika ma rnexxietx. Iċċekkja l-issettjar u erġa ’pprova.", + "@biometricsFailedCheckAgain": {}, + "bold": "Grassett", + "@bold": {}, + "cancel": "Ikkanċella", + "@cancel": {}, + "change": "Ibdel", + "@change": {}, + "chapter": "{count, plural, other{Kapitli} one{Kapitlu}}", + "@chapter": { + "description": "chapter", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "continuePage": "Kompli - paġna {page}", + "@continuePage": { + "description": "Continue - page 1", + "placeholders": { + "page": { + "type": "int" + } + } + }, + "controls": "Kontrolli", + "@controls": {}, + "dashboard": "Daxxbord", + "@dashboard": {}, + "dashboardContinue": "Kompli", + "@dashboardContinue": {}, + "dashboardContinueListening": "Kompli Isma’", + "@dashboardContinueListening": {}, + "dashboardContinueReading": "Kompli Aqra", + "@dashboardContinueReading": {}, + "dashboardContinueWatching": "Kompli Ara", + "@dashboardContinueWatching": {}, + "nextUp": "Li jmiss", + "@nextUp": {}, + "dashboardRecentlyAdded": "Riċentement miżjud f ’{name}", + "@dashboardRecentlyAdded": { + "description": "Recently added on home screen", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "dateAdded": "Data miżjuda", + "@dateAdded": {}, + "dateLastContentAdded": "Data tal-aħħar kontenut miżjud", + "@dateLastContentAdded": {}, + "datePlayed": "Id-data li ndaqqet", + "@datePlayed": {}, + "days": "Jiem", + "@days": {}, + "delete": "Ħassar", + "@delete": {}, + "deleteItem": "Ħassar {item}?", + "@deleteItem": { + "description": "deleteItem", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "descending": "Neżlin", + "@descending": {}, + "disableFilters": "Itfi l-filtri", + "@disableFilters": {}, + "disabled": "Mitfi", + "@disabled": {}, + "discovered": "Skoperti", + "@discovered": {}, + "displayLanguage": "Lingwa tal-wiri", + "@displayLanguage": {}, + "downloadsSyncedData": "Dejta ssinkronizzata", + "@downloadsSyncedData": {}, + "dynamicText": "Dinamika", + "@dynamicText": {}, + "editMetadata": "Editja l-metadejta", + "@editMetadata": {}, + "empty": "Vojta", + "@empty": {}, + "enabled": "Mixgħul", + "@enabled": {}, + "failedToLoadImage": "Ma setax jġib l-istampa", + "@failedToLoadImage": {}, + "favorites": "Favoriti", + "@favorites": {}, + "filter": "{count, plural, other{Filtri} one{Filtra}}", + "@filter": { + "description": "filter", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "folders": "Fowlders", + "@folders": {}, + "fontColor": "Kulur tal-font", + "@fontColor": {}, + "forceRefresh": "Sfurza l-aġġornar", + "@forceRefresh": {}, + "genre": "{count, plural, other{Ġeneri} one{Ġeneru}}", + "@genre": { + "description": "genre", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "groupBy": "Grupp skont", + "@groupBy": {}, + "heightOffset": "Offset tal-għoli", + "@heightOffset": {}, + "home": "Id-dar", + "@home": {}, + "info": "Informazzjoni", + "@info": {}, + "invalidUrlDesc": "Il-URL jeħtieġ li jibda bil-http(s)://", + "@invalidUrlDesc": {}, + "itemCount": "Għadd tal-item: {count}", + "@itemCount": { + "description": "Item count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "libraryFetchNoItemsFound": "Ma nstabu ebda items. Ipprova settings differenti.", + "@libraryFetchNoItemsFound": {}, + "light": "Ċar", + "@light": {}, + "list": "Lista", + "@list": {}, + "logoutUserPopupTitle": "Oħroġ {userName}?", + "@logoutUserPopupTitle": { + "description": "Pop-up for logging out the user", + "placeholders": { + "userName": { + "type": "String" + } + } + }, + "logout": "Oħroġ", + "@logout": {}, + "loop": "Ingassa", + "@loop": {}, + "mediaTypeMovie": "Film", + "@mediaTypeMovie": {}, + "metadataRefreshValidation": "Fittex għal metadejta nieqsa", + "@metadataRefreshValidation": {}, + "minutes": "{count, plural, other{Minuti} one{Minuta} }", + "@minutes": { + "description": "minute", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "mode": "Mod", + "@mode": {}, + "moreFrom": "Aktar minn {info}", + "@moreFrom": { + "description": "More from", + "placeholders": { + "info": { + "type": "String" + } + } + }, + "moreOptions": "Aktar għażliet", + "@moreOptions": {}, + "pathEditTitle": "Ibdel il-post", + "@pathEditTitle": {}, + "play": "Ibda {item}", + "@play": { + "description": "Play with", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "playCount": "Għadd tal-logħob", + "@playCount": {}, + "playFrom": "Ibda min {name}", + "@playFrom": { + "description": "playFrom", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playVideos": "Ibda l-vidjos", + "@playVideos": {}, + "played": "Lestejt", + "@played": {}, + "related": "Relatati", + "@related": {}, + "removeFromCollection": "Neħħi mill-kollezzjoni", + "@removeFromCollection": {}, + "removeFromPlaylist": "Neħħi mil-playlist", + "@removeFromPlaylist": {}, + "retrievePublicListOfUsers": "Irkupra l-lista pubblika tal-utenti", + "@retrievePublicListOfUsers": {}, + "retry": "Erġa ’pprova", + "@retry": {}, + "runTime": "Ħin użat", + "@runTime": {}, + "active": "Attiva", + "@active": {}, + "login": "Illoggja", + "@login": {}, + "collectionFolder": "Fowlder tal-ġbir", + "@collectionFolder": {}, + "goTo": "Mur", + "@goTo": {}, + "color": "Kulur", + "@color": {}, + "combined": "Ikkombinati", + "@combined": {}, + "homeBannerSlideshow": "Preżentazzjoni bis-slajds", + "@homeBannerSlideshow": {}, + "mediaTypeBase": "Tip tal-bażi", + "@mediaTypeBase": {}, + "mediaTypeSeason": "Staġun", + "@mediaTypeSeason": {}, + "mediaTypeSeries": "Serje", + "@mediaTypeSeries": {}, + "name": "Isem", + "@name": {}, + "noItemsToShow": "Ebda item x’ juri", + "@noItemsToShow": {}, + "overview": "Ħarsa ġenerali", + "@overview": {}, + "quickConnectInputACode": "Daħħal kodiċi", + "@quickConnectInputACode": {}, + "refresh": "Aġġorna", + "@refresh": {}, + "result": "Riżultat", + "@result": {}, + "downloadsClearDesc": "Neħħi d-dejta sinkronizzata kollha, tneħħi d-dejta kollha għal kull utent sinkronizzat?", + "@downloadsClearDesc": {}, + "libraryPageSizeTitle": "Daqs tal-paġna tal-librerija", + "@libraryPageSizeTitle": {}, + "read": "Aqra {item}", + "@read": { + "description": "read", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "close": "Agħlaq", + "@close": {}, + "navigation": "Navigazzjoni", + "@navigation": {}, + "mediaTypeEpisode": "Episodju", + "@mediaTypeEpisode": {}, + "mediaTypeFolder": "Fowlder", + "@mediaTypeFolder": {}, + "recursive": "Rikorrenti", + "@recursive": {}, + "replaceExistingImages": "Ibdel ir-ritratti eżistenti", + "@replaceExistingImages": {}, + "episode": "{count, plural, other{Episodji} one{Episodju} }", + "@episode": { + "description": "episode", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "immediately": "Immedjatament", + "@immediately": {}, + "options": "Għażliet", + "@options": {}, + "outlineSize": "Daqs tal-kontorn", + "@outlineSize": {}, + "pathEditSelect": "Agħżel il-post tat-tniżżil", + "@pathEditSelect": {}, + "random": "Każwali", + "@random": {}, + "favorite": "Favorit", + "@favorite": {}, + "noRating": "Ebda klassifikazzjoni", + "@noRating": {}, + "page": "Paġna {index}", + "@page": { + "description": "page", + "placeholders": { + "index": { + "type": "int" + } + } + }, + "never": "Qatt", + "@never": {}, + "fetchingLibrary": "Il-ksib ta’ oġġetti mil-librerija…", + "@fetchingLibrary": {}, + "group": "Grupp", + "@group": {}, + "hide": "Aħbi", + "@hide": {}, + "invalidUrl": "URL Invalidu", + "@invalidUrl": {}, + "markAsUnwatched": "Immarka li ma rajtux", + "@markAsUnwatched": {}, + "mouseDragSupport": "Iddreggja billi tuża l-maws", + "@mouseDragSupport": {}, + "navigationFavorites": "Favoriti", + "@navigationFavorites": {}, + "normal": "Normali", + "@normal": {}, + "openShow": "Iftaħ l-ispettaklu", + "@openShow": {}, + "playLabel": "Ibda", + "@playLabel": {}, + "reWatch": "Erġa ara", + "@reWatch": {}, + "incorrectPinTryAgain": "PIN żbaljat. Erġa ’pprova.", + "@incorrectPinTryAgain": {}, + "readFromStart": "Aqra {item} mill-bidu", + "@readFromStart": { + "description": "Read book from start", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "resume": "Kompli {item}", + "@resume": { + "description": "resume", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "resumable": "Kompli", + "@resumable": {}, + "label": "{count, plural, other{Tikketti} one{Tikketta}}", + "@label": { + "description": "label", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "mediaTypePerson": "Persuna", + "@mediaTypePerson": {}, + "noServersFound": "Ma nstab ebda server ġdid", + "@noServersFound": {}, + "none": "Xejn", + "@none": {}, + "openWebLink": "Iftaħ il-link tal-web", + "@openWebLink": {}, + "quickConnectTitle": "Konnesjoni veloċi", + "@quickConnectTitle": {}, + "refreshPopup": "Aġġorna - {name}", + "@refreshPopup": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "refreshPopupContentMetadata": "Il-metadejta tiġi aġġornata abbażi tas-settings u s-servizzi tal-Internet mixgħula fid-daxxbord.", + "@refreshPopupContentMetadata": {}, + "releaseDate": "Data tar-rilaxx", + "@releaseDate": {}, + "replaceAllImages": "Neħħi ir-ritratti kollha", + "@replaceAllImages": {}, + "openParent": "Iftaħ il-ġenitur", + "@openParent": {}, + "clearAllSettingsQuestion": "Neħħi l-issettjar kollu?", + "@clearAllSettingsQuestion": {}, + "save": "Issejvja", + "@save": {}, + "saved": "Issejvjat", + "@saved": {}, + "scanBiometricHint": "Ivverifika l-identita’", + "@scanBiometricHint": {}, + "scanLibrary": "Skennja l-librerija", + "@scanLibrary": {}, + "scanYourFingerprintToAuthenticate": "Skennja subgħajk biex tawtentika {user}", + "@scanYourFingerprintToAuthenticate": { + "placeholders": { + "user": { + "type": "String" + } + } + }, + "scanningName": "Skenjar - {name}…", + "@scanningName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "scrollToTop": "Skrollja għal fuq", + "@scrollToTop": {}, + "search": "Fittex", + "@search": {}, + "season": "{count, plural, other{Staġuni} one{Staġun} }", + "@season": { + "description": "season", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "selectedWith": "Magħżula {info}", + "@selectedWith": { + "description": "selected", + "placeholders": { + "info": { + "type": "String" + } + } + }, + "separate": "Issepara", + "@separate": {}, + "set": "Issettja", + "@set": { + "description": "Use for setting a certain value", + "context": "Set 'time'" + }, + "setIdentityTo": "Ibdel l-identita għal {name}", + "@setIdentityTo": { + "description": "setIdentityTo", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "settingSecurityApplockTitle": "Illokja l-applikazzjoni", + "@settingSecurityApplockTitle": {}, + "settingsBlurEpisodesTitle": "Ċajpar l-episodji li jmiss", + "@settingsBlurEpisodesTitle": {}, + "settingsBlurredPlaceholderDesc": "Uri l-isfond imċajpar meta tillowdja l-powsters", + "@settingsBlurredPlaceholderDesc": {}, + "settingsHomeBannerInformationDesc": "Informazzjoni li għandha tintwera fl-istrixxun tad-dar", + "@settingsHomeBannerInformationDesc": {}, + "settingsHomeBannerInformationTitle": "Informazzjoni dwar l-istrixxun", + "@settingsHomeBannerInformationTitle": {}, + "settingsHomeNextUpTitle": "Kartelluni ta' li jmiss", + "@settingsHomeNextUpTitle": {}, + "settingsNextUpCutoffDays": "Jiem ta’ għeluq li jmiss", + "@settingsNextUpCutoffDays": {}, + "settingsPlayerCustomSubtitlesDesc": "Ippersonalizza d-Daqs, il-Kulur, il-Pożizzjoni, il-Kontorn", + "@settingsPlayerCustomSubtitlesDesc": {}, + "settingsPlayerDesc": "Aspect-ratio, Avvanzat", + "@settingsPlayerDesc": {}, + "settingsPlayerNativeLibassAccelDesc": "Uża r-renderer tas-sottotitlu tal-libass tal-vidjoplejer", + "@settingsPlayerNativeLibassAccelDesc": {}, + "settingsPlayerNativeLibassAccelTitle": "Sottotitoli tal-libass nattivi", + "@settingsPlayerNativeLibassAccelTitle": {}, + "settingsPlayerVideoHWAccelTitle": "Aċċellerazzjoni tal-ħardwer", + "@settingsPlayerVideoHWAccelTitle": {}, + "settingsPosterSlider": "Uri s-slajder tal-iskala", + "@settingsPosterSlider": {}, + "settingsProfileTitle": "Profil", + "@settingsProfileTitle": {}, + "somethingWentWrong": "Ġrat xi ħaġa ħażina", + "@somethingWentWrong": {}, + "somethingWentWrongPasswordCheck": "Ġrat xi ħaġa ħażina. Iċċekkja l-password tiegħek.", + "@somethingWentWrongPasswordCheck": {}, + "studio": "{count, plural, other{Studjos} one{Studjo}}", + "@studio": { + "description": "studio", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "subtitleConfigurator": "Konfiguratur tas-sottotitlu", + "@subtitleConfigurator": {}, + "subtitleConfiguratorPlaceHolder": "Dan huwa kitba tal-placeholder, xejn x’ tara hawnhekk.", + "@subtitleConfiguratorPlaceHolder": {}, + "subtitles": "Sottotitoli", + "@subtitles": {}, + "sync": "Issinkronizza", + "@sync": {}, + "syncDeleteItemDesc": "Ħassar id-dejta ssinkronizzata kollha għal {item}?", + "@syncDeleteItemDesc": { + "description": "Sync delete item pop-up window", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "syncDeletePopupPermanent": "Din l-azzjoni hija permanenti u se tneħħi l-fajls kollha ssinkronizzati lokalment", + "@syncDeletePopupPermanent": {}, + "syncOpenParent": "Iftaħ il-ġenitur", + "@syncOpenParent": {}, + "syncRemoveDataDesc": "Ħassar id-dejta tal-vidjo ssinkronizzata? Dan huwa permanenti u se jkollok bżonn biex tissinkronizza mill-ġdid il-fajls", + "@syncRemoveDataDesc": {}, + "syncedItems": "Items issinkronizzati", + "@syncedItems": {}, + "tag": "{count, plural, one{Tikketta} other{Tikketti}}", + "@tag": { + "description": "tag", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "theme": "Tema", + "@theme": {}, + "themeColor": "Kulur tat-tema", + "@themeColor": {}, + "themeModeDark": "Skur", + "@themeModeDark": {}, + "totalSize": "Daqs totali: {size}", + "@totalSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, + "type": "{count, plural, other{Tipi} one{Tip}}", + "@type": { + "description": "type", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "unPlayed": "Ma intweriex", + "@unPlayed": {}, + "unknown": "Mhux magħruf", + "@unknown": {}, + "useDefaults": "Uża d-defaults", + "@useDefaults": {}, + "userName": "Isem l-utent", + "@userName": {}, + "video": "Vidjo", + "@video": {}, + "videoScalingContain": "", + "@videoScalingContain": {}, + "videoScalingFillScreenDesc": "Imla n-navigazzjoni u l-istatusbar", + "@videoScalingFillScreenDesc": {}, + "videoScalingFillScreenNotif": "Fill-screen jissostitwixxi l-video fit, f’ rotazzjoni orizzontali", + "@videoScalingFillScreenNotif": {}, + "viewPhotos": "Ara r-ritratti", + "@viewPhotos": {}, + "year": "{count, plural, other{Snin} one{Sena}}", + "@year": { + "description": "year", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "addItemsToCollection": "Żid {itemLength} item(s) mal-kollezzjoni", + "@addItemsToCollection": { + "placeholders": { + "itemLength": { + "type": "int" + } + } + }, + "removedFromCollection": "Tneħħa mill-kollezzjoni ta' {collectionName}", + "@removedFromCollection": { + "placeholders": { + "collectionName": { + "type": "String" + } + } + }, + "addToNewPlaylist": "Playlist ġdida", + "@addToNewPlaylist": {}, + "addedToPlaylist": "Miżjud mal-playlist ta' {playlistName}", + "@addedToPlaylist": { + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "syncStatusEnqueued": "Fil-kju", + "@syncStatusEnqueued": {}, + "syncStatusRunning": "Qed jaħdem", + "@syncStatusRunning": {}, + "syncStatusComplete": "Kompluta", + "@syncStatusComplete": {}, + "syncStatusPaused": "Sospiż", + "@syncStatusPaused": {}, + "syncStatusSynced": "Sinkronizzat", + "@syncStatusSynced": {}, + "syncOverlaySyncing": "Qed jissinkronizza d-dettalji tal-item", + "@syncOverlaySyncing": {}, + "syncRemoveUnableToDeleteItem": "Ma jistax ineħħi item issinkronizzat, ġrat xi ħaġa ħażina", + "@syncRemoveUnableToDeleteItem": {}, + "syncAddItemForSyncing": "Żied {item} għas-sinkronizzazzjoni", + "@syncAddItemForSyncing": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "startedSyncingItem": "Beda jissinkronizza {item}", + "@startedSyncingItem": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "selectAll": "Agħżel kollox", + "@selectAll": {}, + "settingsClientDesc": "Ġenerali, Time-out, Tqassim, Tema", + "@settingsClientDesc": {}, + "settingsVisual": "Viżwali", + "@settingsVisual": {}, + "selectTime": "Agħżel il-ħin", + "@selectTime": {}, + "selected": "Magħżula", + "@selected": {}, + "settingsHomeBannerTitle": "Strixxun tad-dar", + "@settingsHomeBannerTitle": {}, + "settingsBlurEpisodesDesc": "Ċajpar l-episodji kollha li ġejjin", + "@settingsBlurEpisodesDesc": {}, + "settingsPlayerBufferSizeTitle": "Daqs tal-bafer tal-vidjo", + "@settingsPlayerBufferSizeTitle": {}, + "shuffleVideos": "Ħarbat il-vidjows", + "@shuffleVideos": {}, + "settingsPlayerVideoHWAccelDesc": "Uża l-GPU biex turi l-vidjo (rakkomandat)", + "@settingsPlayerVideoHWAccelDesc": {}, + "settingsPosterSize": "Daqs tal-kartellun", + "@settingsPosterSize": {}, + "shadow": "Dell", + "@shadow": {}, + "showEmpty": "Uri l-vojt", + "@showEmpty": {}, + "settingsSecurity": "Sigurta’", + "@settingsSecurity": {}, + "showAlbum": "Uri l-album", + "@showAlbum": {}, + "themeModeLight": "Ċar", + "@themeModeLight": {}, + "themeModeSystem": "Sistema", + "@themeModeSystem": {}, + "sortName": "Isem", + "@sortName": {}, + "sortOrder": "Issortja l-ordni", + "@sortOrder": {}, + "switchUser": "Aqleb l-utent", + "@switchUser": {}, + "syncStatusWaitingToRetry": "Qed jistenna biex jerġa’ jipprova", + "@syncStatusWaitingToRetry": {}, + "videoScaling": "Skala tal-vidjow", + "@videoScaling": {}, + "syncStatusNotFound": "Ma nstabx", + "@syncStatusNotFound": {}, + "syncOverlayDeleting": "Qed ineħħi item issinkronizzat", + "@syncOverlayDeleting": {}, + "syncSelectDownloadsFolder": "Agħżel fowlder tat-tniżżil", + "@syncSelectDownloadsFolder": {}, + "settingsContinue": "Kompli", + "@settingsContinue": {}, + "selectViewType": "Agħżel it-tip tad-dehra", + "@selectViewType": {}, + "settingsPosterPinch": "Pinch-zoom għal powsters fuq skala", + "@settingsPosterPinch": {}, + "shuffleGallery": "Ħarbat il-gallerija", + "@shuffleGallery": {}, + "settingsHomeBannerDescription": "Uri bħala slideshow, karużell, jew aħbi l-banner", + "@settingsHomeBannerDescription": {}, + "settingsBlurredPlaceholderTitle": "Placeholder imċajpar", + "@settingsBlurredPlaceholderTitle": {}, + "settingsEnableOsMediaControls": "Attiva l-kontrolli tal-midja tal-OS", + "@settingsEnableOsMediaControls": {}, + "settingsPlayerCustomSubtitlesTitle": "Ippersonalizza s-sottotitoli", + "@settingsPlayerCustomSubtitlesTitle": {}, + "settingsPlayerMobileWarning": "Meta tixgħel l-aċċellerazzjoni tal-ħardwer u s-sottotitoli tal-libass nattivi fuq l-Android dan jista’ jikkawża li xi sottotitoli ma jirrendux.", + "@settingsPlayerMobileWarning": {}, + "settingsHomeNextUpDesc": "Tip ta’ posters murija fid-daxxbord", + "@settingsHomeNextUpDesc": {}, + "settingsShowScaleSlider": "Uri s-slajd tad-daqs tal-powster", + "@settingsShowScaleSlider": {}, + "settingsPlayerBufferSizeDesc": "Ikkonfigura d-daqs tal-bafer għad-daqq tal-vidjo, billi tiddetermina kemm titgħabba data fil-cache.", + "@settingsPlayerBufferSizeDesc": {}, + "timeAndAnnotation": "{minutes} u {seconds}", + "@timeAndAnnotation": { + "description": "timeAndAnnotation", + "placeholders": { + "minutes": { + "type": "String" + }, + "seconds": { + "type": "String" + } + } + }, + "unableToReverseAction": "Din l-azzjoni ma tistax titreġġa ’lura. Se tneħħi l-issettjar kollu.", + "@unableToReverseAction": {}, + "syncNoFolderSetup": "Ebda konfigurazzjoni tal-fowlder tas-sinkronizzazzjoni", + "@syncNoFolderSetup": {}, + "sortBy": "Issortja skont", + "@sortBy": {}, + "syncRemoveDataTitle": "Neħħi d-dejta ssinkronizzata?", + "@syncRemoveDataTitle": {}, + "syncDetails": "Issinkronizza d-dettalji", + "@syncDetails": {}, + "addedToCollection": "Miżjud mal-kollezzjoni ta' {collectionName}", + "@addedToCollection": { + "placeholders": { + "collectionName": { + "type": "String" + } + } + }, + "syncStatusFailed": "Falla", + "@syncStatusFailed": {}, + "syncStatusPartially": "Parzjalment", + "@syncStatusPartially": {}, + "addItemsToPlaylist": "Żid {itemLength} item(s) mal-playlist", + "@addItemsToPlaylist": { + "placeholders": { + "itemLength": { + "type": "int" + } + } + }, + "seconds": "{count, plural, other{Sekondi} one{Sekonda}}", + "@seconds": { + "description": "second", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "showDetails": "Uri d-dettalji", + "@showDetails": {}, + "settingsPlayerTitle": "Plejer", + "@settingsPlayerTitle": {}, + "settingsQuickConnectTitle": "Konnesjoni Veloċi", + "@settingsQuickConnectTitle": {}, + "start": "Ibda", + "@start": {}, + "syncDeleteItemTitle": "Ħassar l-oġġett issinkronizzat", + "@syncDeleteItemTitle": {}, + "writer": "{count, plural, other{Kittieb} two{Kittieba}}", + "@writer": { + "description": "writer", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "addToNewCollection": "Kollezzjoni ġdida", + "@addToNewCollection": {}, + "unableToConnectHost": "Ma jistax jaqbad mal-host", + "@unableToConnectHost": {}, + "watchOn": "Ara fuq", + "@watchOn": {}, + "syncStatusCanceled": "Ikkanċellati", + "@syncStatusCanceled": {}, + "aboutVersion": "Verżjoni: {version}", + "@aboutVersion": { + "placeholders": { + "version": { + "type": "String" + } + } + }, + "aboutSocials": "Socjali", + "@aboutSocials": {}, + "off": "Mitfi", + "@off": {}, + "screenBrightness": "Dawl tal-iskrin", + "@screenBrightness": {}, + "scale": "Skala", + "@scale": {}, + "playBackSettings": "Issettjar tal-Playback", + "@playBackSettings": {}, + "settingsAutoNextTitle": "Previżjoni ta' li jmiss", + "@settingsAutoNextTitle": {}, + "settingsAutoNextDesc": "Juri previżjoni li jmiss qrib it-tmiem jekk item ieħor ikun fil-kju", + "@settingsAutoNextDesc": {}, + "autoNextOffStaticTitle": "Statiku", + "@autoNextOffStaticTitle": {}, + "autoNextOffStaticDesc": "Uri l-iskrin li jmiss meta jifdal 30 sekonda ta’ ħin tal-logħob", + "@autoNextOffStaticDesc": {}, + "speed": "Ħeffa", + "@speed": {}, + "unableToPlayMedia": "Kien hemm żball biex jinstab tip ta’ midja kompatibbli", + "@unableToPlayMedia": {}, + "unableToPlayBooksOnWeb": "Il-kotba mhumiex disponibli fuq il-web għalissa", + "@unableToPlayBooksOnWeb": {}, + "defaultFilterForLibrary": "Filtru default għal-librerija", + "@defaultFilterForLibrary": {}, + "removeFilterForLibrary": "Neħħi {filter}?", + "@removeFilterForLibrary": { + "description": "removeFilterForLibrary", + "placeholders": { + "filter": { + "type": "String" + } + } + }, + "libraryFiltersRemoveAll": "Neħħi l-filtri kollha", + "@libraryFiltersRemoveAll": {}, + "libraryFiltersRemoveAllConfirm": "Dan se jħassar il-filtri kollha ssejvjati għal kull librerija", + "@libraryFiltersRemoveAllConfirm": {}, + "playerSettingsOrientationTitle": "Orjentazzjoni tal-plejer", + "@playerSettingsOrientationTitle": {}, + "playerSettingsOrientationDesc": "Ġiegħel lill-video player f ’ċerti orjentazzjonijiet", + "@playerSettingsOrientationDesc": {}, + "deviceOrientationPortraitUp": "Stampa ’l fuq", + "@deviceOrientationPortraitUp": {}, + "deviceOrientationPortraitDown": "Stampa ’l isfel", + "@deviceOrientationPortraitDown": {}, + "errorOpeningMedia": "Ġrat xi ħaġa ħażina waqt li kien qed jipprova jdoqq din il-midja", + "@errorOpeningMedia": {}, + "subtitle": "Sottotitolu", + "@subtitle": {}, + "deviceOrientationLandscapeRight": "Stampa Lemin", + "@deviceOrientationLandscapeRight": {}, + "aboutLicenses": "Liċenzji", + "@aboutLicenses": {}, + "subtitleConfiguration": "Konfigurazzjoni tas-sottotitlu", + "@subtitleConfiguration": {}, + "playbackRate": "Rata tal-playback", + "@playbackRate": {}, + "autoNextOffSmartDesc": "Juri l-iskrin li jmiss meta jibdew il-krediti jekk ma jibqax aktar minn 10 sekondi wara l-krediti. Inkella juri l-iskrin li jmiss bi 30 sekonda ta’ ħin tal-logħob li jifdal", + "@autoNextOffSmartDesc": {}, + "deviceOrientationLandscapeLeft": "Stampa Xellug", + "@deviceOrientationLandscapeLeft": {}, + "deleteFilterConfirmation": "Żgur tixtieq tħassar dan il-filtru?", + "@deleteFilterConfirmation": {}, + "libraryFiltersLimitReached": "Il-limitu tal-filtru milħuq (10) neħħi xi filtri", + "@libraryFiltersLimitReached": {}, + "unableToSyncItem": "Ma jistax jissinkronizza {item}, ġrat xi ħaġa ħażina", + "@unableToSyncItem": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "updateFilterForLibrary": "Aġġorna l-filtru", + "@updateFilterForLibrary": {}, + "aboutCreatedBy": "Maħluqa minn DonutWare", + "@aboutCreatedBy": {}, + "clientSettingsSchemeVariantTitle": "Varjant tal-iskema", + "@clientSettingsSchemeVariantTitle": {}, + "schemeSettingsFidelity": "Fidelta’", + "@schemeSettingsFidelity": {}, + "schemeSettingsMonochrome": "Monokromju", + "@schemeSettingsMonochrome": {}, + "schemeSettingsNeutral": "Newtrali", + "@schemeSettingsNeutral": {}, + "schemeSettingsVibrant": "Vibranti", + "@schemeSettingsVibrant": {}, + "schemeSettingsExpressive": "Espressiva", + "@schemeSettingsExpressive": {}, + "schemeSettingsContent": "Kontenut", + "@schemeSettingsContent": {}, + "clientSettingsRequireWifiDesc": "Niżżel biss meta tkun imqabbad ma’ netwerk tal-Wi-fi", + "@clientSettingsRequireWifiDesc": {}, + "libraryShuffleAndPlayItems": "Ħarbat u ilgħab l-oġġetti", + "@libraryShuffleAndPlayItems": {}, + "libraryPlayItems": "Ilgħab l-oġġetti", + "@libraryPlayItems": {}, + "clientSettingsShowAllCollectionsTitle": "Uri t-tipi kollha ta’ ġbir", + "@clientSettingsShowAllCollectionsTitle": {}, + "stop": "Waqqaf", + "@stop": {}, + "resumeVideo": "Kompli l-vidjow", + "@resumeVideo": {}, + "playerSettingsBackendTitle": "Backend tal-plejer tal-vidjow", + "@playerSettingsBackendTitle": {}, + "noVideoPlayerOptions": "Il-backend magħżul m' għandux għażliet", + "@noVideoPlayerOptions": {}, + "mdkExperimental": "L-MDK għadha fi stadju sperimentali", + "@mdkExperimental": {}, + "skipButtonLabel": "(A)qbeż {segment}", + "@skipButtonLabel": { + "placeholders": { + "segment": { + "type": "String" + } + } + }, + "mediaSegmentUnknown": "Mhux magħruf", + "@mediaSegmentUnknown": {}, + "mediaSegmentRecap": "", + "@mediaSegmentRecap": {}, + "settingsLayoutSizesTitle": "Daqsijiet tat-tqassim", + "@settingsLayoutSizesTitle": {}, + "settingsLayoutSizesDesc": "Agħżel liema daqsijiet tat-tqassim tista’ tuża l-applikazzjoni abbażi tad-daqs tal-iskrijn", + "@settingsLayoutSizesDesc": {}, + "settingsLayoutModesDesc": "Ikkontrolla jekk l-applikazzjoni tistax tuża tqassim b ’pannell wieħed jew b’ żewġ pannelli", + "@settingsLayoutModesDesc": {}, + "phone": "Telefown", + "@phone": {}, + "layoutModeSingle": "Wieħed", + "@layoutModeSingle": {}, + "layoutModeDual": "Doppju", + "@layoutModeDual": {}, + "internetStreamingQualityTitle": "Il-kwalita tal-Internet", + "@internetStreamingQualityTitle": {}, + "homeStreamingQualityDesc": "Il-kwalita massima tal-istrimjar meta mqabbda man-netwerk tad-dar", + "@homeStreamingQualityDesc": {}, + "qualityOptionsTitle": "Għażliet tal-kwalita", + "@qualityOptionsTitle": {}, + "segmentActionNone": "Xejn", + "@segmentActionNone": {}, + "segmentActionAskToSkip": "Itlob biex taqbeż", + "@segmentActionAskToSkip": {}, + "segmentActionSkip": "Aqbeż", + "@segmentActionSkip": {}, + "loading": "Tagħbija", + "@loading": {}, + "exitFladderTitle": "Oħroġ min Fladder", + "@exitFladderTitle": {}, + "castAndCrew": "Kast u Ekwipaġġ", + "@castAndCrew": {}, + "guestActor": "{count, plural, other{Atturi Mistiedna} one{Attur Mistieden}}", + "@guestActor": { + "description": "Guest actors", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "maxConcurrentDownloadsTitle": "Massimu ta’ tniżżil fl-istess ħin", + "@maxConcurrentDownloadsTitle": {}, + "maxConcurrentDownloadsDesc": "Jistabbilixxi n-numru massimu ta’ tniżżil li jista’ jaħdem fl-istess ħin. Issettja għal 0 biex tneħħi l-limitu.", + "@maxConcurrentDownloadsDesc": {}, + "playbackTrackSelection": "Għażla tat-trekk tad-daqq", + "@playbackTrackSelection": {}, + "rememberSubtitleSelections": "Issettja t-trekk tas-sottotitlu bbażat fuq l-oġġett preċedenti", + "@rememberSubtitleSelections": {}, + "rememberAudioSelections": "Issettja t-trekk tal-awdjo bbażat fuq l-oġġett preċedenti", + "@rememberAudioSelections": {}, + "rememberSubtitleSelectionsDesc": "Ipprova ssettja t-trekk tas-sottotitlu għall-eqreb taqbila għall-aħħar vidjo.", + "@rememberSubtitleSelectionsDesc": {}, + "rememberAudioSelectionsDesc": "Ipprova ssettja t-trekk tal-awdjo għall-eqreb taqbila għall-aħħar vidjo.", + "@rememberAudioSelectionsDesc": {}, + "similarToRecentlyPlayed": "Simili għal dawk li rajt dan l-aħħar", + "@similarToRecentlyPlayed": {}, + "similarToLikedItem": "Simili għal oġġett li għoġbok", + "@similarToLikedItem": {}, + "hasActorFromRecentlyPlayed": "Għandu attur milli rajt l-aħħar", + "@hasActorFromRecentlyPlayed": {}, + "hasDirectorFromRecentlyPlayed": "Għandu direttur milli rajt l-aħħar", + "@hasDirectorFromRecentlyPlayed": {}, + "hasLikedDirector": "Għandu dirretur li għoġbok", + "@hasLikedDirector": {}, + "hasLikedActor": "Għandu attur li għoġbok", + "@hasLikedActor": {}, + "latest": "L-aktar riċenti", + "@latest": {}, + "recommended": "Rakkomandat", + "@recommended": {}, + "playbackType": "Tip ta’ plejbek", + "@playbackType": {}, + "playbackTypeDirect": "Dirett", + "@playbackTypeDirect": {}, + "playbackTypeOffline": "Offlajn", + "@playbackTypeOffline": {}, + "latestReleases": "L-aħħar ħarġiet", + "@latestReleases": {}, + "autoCheckForUpdates": "Iċċekkja perjodikament għall-verżjonijiet ġodda", + "@autoCheckForUpdates": {}, + "newReleaseFoundTitle": "Verżjoni {newRelease} disponibbli!", + "@newReleaseFoundTitle": { + "placeholders": { + "newRelease": { + "type": "String" + } + } + }, + "newUpdateFoundOnGithub": "Verżjoni ġdid fuq GitHub", + "@newUpdateFoundOnGithub": {}, + "errorLogs": "Reġistri tal-iżbalji", + "@errorLogs": {}, + "mediaSegmentCommercial": "Kummerċjali", + "@mediaSegmentCommercial": {}, + "clientSettingsRequireWifiTitle": "Jeħtieġu Wi-fi", + "@clientSettingsRequireWifiTitle": {}, + "episodeMissing": "Nieqes", + "@episodeMissing": {}, + "copyStreamUrl": "Ikkopja l-URL tal-istrimjar", + "@copyStreamUrl": {}, + "clientSettingsShowAllCollectionsDesc": "Meta jkun attivat, uri t-tipi kollha ta' ġbir, inklużi dawk mhux appoġġati minn Fladder", + "@clientSettingsShowAllCollectionsDesc": {}, + "qualityOptionsAuto": "Awto", + "@qualityOptionsAuto": {}, + "mediaSegmentPreview": "Previżjoni", + "@mediaSegmentPreview": {}, + "homeStreamingQualityTitle": "Il-kwalita’ tad-dar", + "@homeStreamingQualityTitle": {}, + "version": "Verżjoni", + "@version": {}, + "schemeSettingsFruitSalad": "Insalata tal-frott", + "@schemeSettingsFruitSalad": {}, + "downloadFile": "Niżżel {type}", + "@downloadFile": { + "placeholders": { + "type": { + "type": "String" + } + } + }, + "playNextVideo": "Ilgħab il-vidjo li jmiss", + "@playNextVideo": {}, + "copiedToClipboard": "Ikkupjat fil-klibbord", + "@copiedToClipboard": {}, + "schemeSettingsRainbow": "Qawsalla", + "@schemeSettingsRainbow": {}, + "playerSettingsBackendDesc": "Agħżel il-media plejer preferut tiegħek għall-aħjar esperjenza ta’ playback", + "@playerSettingsBackendDesc": {}, + "episodeAvailable": "Disponibbli", + "@episodeAvailable": {}, + "external": "Esterni", + "@external": {}, + "settingsLayoutModesTitle": "Modalitajiet tat-Tqassim", + "@settingsLayoutModesTitle": {}, + "closeVideo": "Agħlaq il-vidjo", + "@closeVideo": {}, + "internetStreamingQualityDesc": "Il-kwalita massima ta’ streaming fuq l-internet (mobbli)", + "@internetStreamingQualityDesc": {}, + "mediaSegmentActions": "Azzjonijiet tas-segment tal-midja", + "@mediaSegmentActions": {}, + "qualityOptionsOriginal": "Oriġinali", + "@qualityOptionsOriginal": {} +} diff --git a/lib/l10n/app_nl.arb b/lib/l10n/app_nl.arb index 079658d..0ad39af 100644 --- a/lib/l10n/app_nl.arb +++ b/lib/l10n/app_nl.arb @@ -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": {} } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 0ef89dc..e56fed1 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -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" + } + } + } } diff --git a/lib/l10n/app_pt_BR.arb b/lib/l10n/app_pt_BR.arb index e9e5b97..c67c34b 100644 --- a/lib/l10n/app_pt_BR.arb +++ b/lib/l10n/app_pt_BR.arb @@ -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": {} } diff --git a/lib/l10n/app_ro.arb b/lib/l10n/app_ro.arb new file mode 100644 index 0000000..3154251 --- /dev/null +++ b/lib/l10n/app_ro.arb @@ -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": {} +} diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb new file mode 100644 index 0000000..34119ac --- /dev/null +++ b/lib/l10n/app_ru.arb @@ -0,0 +1,1351 @@ +{ + "dashboardContinueReading": "Продолжить читать", + "@dashboardContinueReading": {}, + "dashboardContinueListening": "Продолжить слушать", + "@dashboardContinueListening": {}, + "clearAllSettings": "Очистите все настройки", + "@clearAllSettings": {}, + "backgroundBlur": "Размытие фона", + "@backgroundBlur": {}, + "color": "Цвет", + "@color": {}, + "biometricsFailedCheckAgain": "Сбой биометрии. Проверьте настройки и повторите попытку.", + "@biometricsFailedCheckAgain": {}, + "clearAllSettingsQuestion": "Очистить все настройки?", + "@clearAllSettingsQuestion": {}, + "appLockTitle": "Установите метод входа в систему для {userName}", + "@appLockTitle": { + "description": "Pop-up to pick a login method", + "placeholders": { + "userName": { + "type": "String" + } + } + }, + "nativeName": "Русский", + "@nativeName": {}, + "about": "О приложении", + "@about": {}, + "accept": "Принять", + "@accept": {}, + "active": "Активно", + "@active": {}, + "addAsFavorite": "Добавить в избранное", + "@addAsFavorite": {}, + "addToCollection": "Добавить в коллекцию", + "@addToCollection": {}, + "addToPlaylist": "Добавить в плейлист", + "@addToPlaylist": {}, + "advanced": "Дополнительно", + "@advanced": {}, + "all": "Все", + "@all": {}, + "amoledBlack": "Матовый черный", + "@amoledBlack": {}, + "appLockAutoLogin": "Автоматический вход в систему", + "@appLockAutoLogin": {}, + "appLockBiometrics": "Биометрия", + "@appLockBiometrics": {}, + "appLockPasscode": "Пароль", + "@appLockPasscode": {}, + "audio": "Аудио", + "@audio": {}, + "autoPlay": "Автоматическое воспроизведение", + "@autoPlay": {}, + "backgroundOpacity": "Непрозрачность фона", + "@backgroundOpacity": {}, + "bold": "Жирный", + "@bold": {}, + "cancel": "Отмена", + "@cancel": {}, + "change": "Изменить", + "@change": {}, + "chapter": "{count, plural, other{Глав} two{Главы} one{Глава}}", + "@chapter": { + "description": "chapter", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "clear": "Очистить", + "@clear": {}, + "close": "Закрыть", + "@close": {}, + "code": "Код", + "@code": {}, + "collectionFolder": "Папка для сбора данных", + "@collectionFolder": {}, + "combined": "Комбинированный", + "@combined": {}, + "controls": "Управление", + "@controls": {}, + "dashboard": "Панель", + "@dashboard": {}, + "dashboardContinue": "Продолжить", + "@dashboardContinue": {}, + "communityRating": "Рейтинг сообщества", + "@communityRating": {}, + "continuePage": "Следующая - страница {page}", + "@continuePage": { + "description": "Continue - page 1", + "placeholders": { + "page": { + "type": "int" + } + } + }, + "ascending": "Восходящий", + "@ascending": {}, + "dashboardRecentlyAdded": "Недавно добавлено в {name}", + "@dashboardRecentlyAdded": { + "description": "Recently added on home screen", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "nextUp": "Следующее", + "@nextUp": {}, + "actor": "{count, plural, other{Актёры} two{Актёра} one{Актёр}}", + "@actor": { + "description": "actor", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "clearChanges": "Очистить изменения", + "@clearChanges": {}, + "clearSelection": "Очистить выделение", + "@clearSelection": {}, + "dashboardContinueWatching": "Продолжить просмотр", + "@dashboardContinueWatching": {}, + "deleteFileFromSystem": "Элемент «{item}» будет удалён из файловой системы и медиатеки. Продолжить?", + "@deleteFileFromSystem": { + "description": "Delete file from system", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "datePlayed": "Дата воспроизведения", + "@datePlayed": {}, + "delete": "Удалить", + "@delete": {}, + "dateAdded": "Добавлено", + "@dateAdded": {}, + "dateLastContentAdded": "Дата добавления", + "@dateLastContentAdded": {}, + "days": "Дни", + "@days": {}, + "deleteItem": "Удалить {item}?", + "@deleteItem": { + "description": "deleteItem", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "grid": "Сетка", + "@grid": {}, + "episode": "{count, plural, other{Эпизодов} two{Эпизода} one{Эпизод} }", + "@episode": { + "description": "episode", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "itemCount": "Кол-во элементов: {count}", + "@itemCount": { + "description": "Item count", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "noItemsSynced": "Ни один элемент не синхронизирован", + "@noItemsSynced": {}, + "heightOffset": "Смещение по высоте", + "@heightOffset": {}, + "homeBannerCarousel": "Карусель", + "@homeBannerCarousel": {}, + "libraryFetchNoItemsFound": "Не найдено ни одного элемента. Попробуйте разные настройки.", + "@libraryFetchNoItemsFound": {}, + "list": "Список", + "@list": {}, + "metaDataSavedFor": "Метаданные сохранены для {item}", + "@metaDataSavedFor": { + "description": "metaDataSavedFor", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "openShow": "Открыть серию", + "@openShow": {}, + "playFromStart": "Воспроизвести {name} с самого начала", + "@playFromStart": { + "description": "speel vanaf het begin", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "navigationSync": "Синхронизировано", + "@navigationSync": {}, + "parentalRating": "Родительский рейтинг", + "@parentalRating": {}, + "openWebLink": "Открыть веб-ссылку", + "@openWebLink": {}, + "retry": "Повторить", + "@retry": {}, + "logout": "Выйти", + "@logout": {}, + "endsAt": "закончится в {date}", + "@endsAt": { + "description": "endsAt", + "placeholders": { + "date": { + "type": "DateTime", + "format": "jm" + } + } + }, + "discovered": "Найдено", + "@discovered": {}, + "descending": "По убыванию", + "@descending": {}, + "director": "{count, plural, other{Режиссеры} two{Режиссера} one{Режиссер}}", + "@director": { + "description": "director", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "disableFilters": "Отключить фильтры", + "@disableFilters": {}, + "disabled": "Выключено", + "@disabled": {}, + "displayLanguage": "Язык интерфейса", + "@displayLanguage": {}, + "downloadsClearTitle": "Очистить синхронизированные данные", + "@downloadsClearTitle": {}, + "downloadsPath": "Путь", + "@downloadsPath": {}, + "downloadsSyncedData": "Синхронизированные данные", + "@downloadsSyncedData": {}, + "downloadsTitle": "Скачанные", + "@downloadsTitle": {}, + "dynamicText": "Динамический", + "@dynamicText": {}, + "editMetadata": "Редактирование метаданных", + "@editMetadata": {}, + "empty": "Пустой", + "@empty": {}, + "enabled": "Включено", + "@enabled": {}, + "error": "Ошибка", + "@error": {}, + "failedToLoadImage": "Не удалось загрузить изображение", + "@failedToLoadImage": {}, + "favorite": "Избранное", + "@favorite": {}, + "favorites": "Избранные", + "@favorites": {}, + "fetchingLibrary": "Получение элементов библиотеки…", + "@fetchingLibrary": {}, + "filter": "{count, plural, other{Фильтров} two{Фильтра} one{Фильтр}}", + "@filter": { + "description": "filter", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "folders": "Каталоги", + "@folders": {}, + "fontColor": "Цвет шрифта", + "@fontColor": {}, + "fontSize": "Размер шрифта", + "@fontSize": {}, + "forceRefresh": "Принудительно обновить", + "@forceRefresh": {}, + "goTo": "Перейти к", + "@goTo": {}, + "group": "Группа", + "@group": {}, + "hide": "Скрыть", + "@hide": {}, + "hideEmpty": "Скрыть пустое", + "@hideEmpty": {}, + "home": "Главная", + "@home": {}, + "identify": "Определить", + "@identify": {}, + "info": "Информация", + "@info": {}, + "invalidUrl": "Недопустимый URL", + "@invalidUrl": {}, + "invalidUrlDesc": "URL должен начинаться с http(s)://", + "@invalidUrlDesc": {}, + "label": "{count, plural, other{Ярлыков} two{Ярлыка} one{Ярлык}}", + "@label": { + "description": "label", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "library": "{count, plural, other{Библиотек} two{Библиотеки} one{Библиотека}}", + "@library": { + "description": "Plural", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "libraryPageSizeDesc": "Установить количество загружаемых за раз файлов. 0 отключит подкачку.", + "@libraryPageSizeDesc": {}, + "light": "Светлая", + "@light": {}, + "lockscreen": "Экран блокировки", + "@lockscreen": {}, + "loggedIn": "Вход в систему", + "@loggedIn": {}, + "login": "Войти", + "@login": {}, + "logoutUserPopupTitle": "Выйти из системы {userName}?", + "@logoutUserPopupTitle": { + "description": "Pop-up for logging out the user", + "placeholders": { + "userName": { + "type": "String" + } + } + }, + "loop": "Цикл", + "@loop": {}, + "markAsUnwatched": "Отметить как непросмотренное", + "@markAsUnwatched": {}, + "markAsWatched": "Отметить как просмотренное", + "@markAsWatched": {}, + "masonry": "Мозаика", + "@masonry": {}, + "mediaTypeBase": "Основной тип", + "@mediaTypeBase": {}, + "mediaTypeBook": "Книга", + "@mediaTypeBook": {}, + "mediaTypeBoxset": "Набор", + "@mediaTypeBoxset": {}, + "mediaTypeEpisode": "Серия", + "@mediaTypeEpisode": {}, + "mediaTypeFolder": "Папка", + "@mediaTypeFolder": {}, + "mediaTypeMovie": "Фильм", + "@mediaTypeMovie": {}, + "mediaTypePerson": "Персона", + "@mediaTypePerson": {}, + "mediaTypePhoto": "Фото", + "@mediaTypePhoto": {}, + "mediaTypePhotoAlbum": "Фотоальбом", + "@mediaTypePhotoAlbum": {}, + "mediaTypePlaylist": "Плейлист", + "@mediaTypePlaylist": {}, + "mediaTypeSeason": "Сезон", + "@mediaTypeSeason": {}, + "mediaTypeSeries": "Серии", + "@mediaTypeSeries": {}, + "metadataRefreshDefault": "Сканирование на наличие новых и обновленных файлов", + "@metadataRefreshDefault": {}, + "metadataRefreshFull": "Заменить все метаданные", + "@metadataRefreshFull": {}, + "metadataRefreshValidation": "Поиск отсутствующих метаданных", + "@metadataRefreshValidation": {}, + "mode": "Режим", + "@mode": {}, + "moreFrom": "Подробнее от {info}", + "@moreFrom": { + "description": "More from", + "placeholders": { + "info": { + "type": "String" + } + } + }, + "moreOptions": "Другие варианты", + "@moreOptions": {}, + "mouseDragSupport": "Перетаскивание с помощью мыши", + "@mouseDragSupport": {}, + "musicAlbum": "Альбом", + "@musicAlbum": {}, + "name": "Название", + "@name": {}, + "navigation": "Навигация", + "@navigation": {}, + "navigationDashboard": "Панель", + "@navigationDashboard": {}, + "navigationFavorites": "Избранное", + "@navigationFavorites": {}, + "never": "Никогда", + "@never": {}, + "noItemsToShow": "Ни один элемент не показан", + "@noItemsToShow": {}, + "noRating": "Нет рейтинга", + "@noRating": {}, + "noServersFound": "Новые серверы не найдены", + "@noServersFound": {}, + "noSuggestionsFound": "Предложений не найдено", + "@noSuggestionsFound": {}, + "none": "Ничего", + "@none": {}, + "normal": "Нормальный", + "@normal": {}, + "notPartOfAlbum": "Не является частью альбома", + "@notPartOfAlbum": {}, + "openParent": "Открыть родительский элемент", + "@openParent": {}, + "options": "Опции", + "@options": {}, + "other": "Другое", + "@other": {}, + "outlineSize": "Размер контура", + "@outlineSize": {}, + "overview": "Обзор", + "@overview": {}, + "page": "Страница {index}", + "@page": { + "description": "page", + "placeholders": { + "index": { + "type": "int" + } + } + }, + "password": "Пароль", + "@password": {}, + "pathClearTitle": "Очистить путь загрузок", + "@pathClearTitle": {}, + "pathEditTitle": "Изменить путь", + "@pathEditTitle": {}, + "playCount": "Количество воспроизведений", + "@playCount": {}, + "playFrom": "Воспроизвести из {name}", + "@playFrom": { + "description": "playFrom", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "playLabel": "Воспроизвести", + "@playLabel": {}, + "playVideos": "Воспроизвести видео", + "@playVideos": {}, + "quickConnectInputACode": "Введите код", + "@quickConnectInputACode": {}, + "quickConnectTitle": "Быстрое подключение", + "@quickConnectTitle": {}, + "quickConnectWrongCode": "Неверный код", + "@quickConnectWrongCode": {}, + "random": "Случайный", + "@random": {}, + "rating": "{count, plural, other{Рейтингов} two{Рейтинга} one{Рейтинг}}", + "@rating": { + "description": "rating", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "reWatch": "Пересмотреть", + "@reWatch": {}, + "read": "Читать {item}", + "@read": { + "description": "read", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "readFromStart": "Читать {item} с позиции", + "@readFromStart": { + "description": "Read book from start", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "recursive": "Рекурсивно", + "@recursive": {}, + "refresh": "Обновить", + "@refresh": {}, + "refreshMetadata": "Обновить метаданные", + "@refreshMetadata": {}, + "refreshPopup": "Обновить - {name}", + "@refreshPopup": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "refreshPopupContentMetadata": "Метаданные обновляются в зависимости от настроек и интернет-сервисов, включенных на панели.", + "@refreshPopupContentMetadata": {}, + "related": "Связанные", + "@related": {}, + "releaseDate": "Дата выхода", + "@releaseDate": {}, + "removeAsFavorite": "Удалить из избранного", + "@removeAsFavorite": {}, + "removeFromCollection": "Удалить из коллекции", + "@removeFromCollection": {}, + "replaceAllImages": "Заменить все изображения", + "@replaceAllImages": {}, + "restart": "Перезапуск", + "@restart": {}, + "result": "Результат", + "@result": {}, + "resumable": "Доступные для продолжения", + "@resumable": {}, + "resume": "Продолжить {item}", + "@resume": { + "description": "resume", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "retrievePublicListOfUsers": "Получение публичного списка пользователей", + "@retrievePublicListOfUsers": {}, + "runTime": "Время выполнения", + "@runTime": {}, + "save": "Сохранить", + "@save": {}, + "scanBiometricHint": "Подтвердить личность", + "@scanBiometricHint": {}, + "scanLibrary": "Сканировать библиотеку", + "@scanLibrary": {}, + "scanningName": "Сканирование - {name}…", + "@scanningName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "scrollToTop": "Прокрутить к верху", + "@scrollToTop": {}, + "search": "Поиск", + "@search": {}, + "season": "{count, plural, other{Сезонов} two{Сезона} one{Сезон} }", + "@season": { + "description": "season", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "seconds": "{count, plural, other{Секунд} two{Секунды} one{Секунда}}", + "@seconds": { + "description": "second", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "selectAll": "Выбрать всё", + "@selectAll": {}, + "selectTime": "Выбрать время", + "@selectTime": {}, + "selectViewType": "Выбрать тип просмотра", + "@selectViewType": {}, + "selected": "Выбрано", + "@selected": {}, + "selectedWith": "Выбрано {info}", + "@selectedWith": { + "description": "selected", + "placeholders": { + "info": { + "type": "String" + } + } + }, + "separate": "Отдельно", + "@separate": {}, + "server": "Сервер", + "@server": {}, + "set": "Установить", + "@set": { + "description": "Use for setting a certain value", + "context": "Set 'time'" + }, + "settingSecurityApplockTitle": "Блокировка приложения", + "@settingSecurityApplockTitle": {}, + "settings": "Настройки", + "@settings": {}, + "settingsBlurEpisodesDesc": "Размыть все предстоящие серии", + "@settingsBlurEpisodesDesc": {}, + "settingsBlurEpisodesTitle": "Размыть следующие серии", + "@settingsBlurEpisodesTitle": {}, + "settingsBlurredPlaceholderDesc": "Показывать размытый фон при загрузке постеров", + "@settingsBlurredPlaceholderDesc": {}, + "settingsBlurredPlaceholderTitle": "Размытый заполнитель", + "@settingsBlurredPlaceholderTitle": {}, + "settingsClientDesc": "Основное, тайм-аут, макет, тема", + "@settingsClientDesc": {}, + "settingsClientTitle": "Fladder", + "@settingsClientTitle": {}, + "settingsContinue": "Продолжить", + "@settingsContinue": {}, + "settingsHomeBannerDescription": "Отображение в виде слайд-шоу, карусели или скрытие баннера", + "@settingsHomeBannerDescription": {}, + "settingsHomeBannerTitle": "Основной баннер", + "@settingsHomeBannerTitle": {}, + "settingsHomeBannerInformationTitle": "Информация о баннере", + "@settingsHomeBannerInformationTitle": {}, + "settingsHomeNextUpDesc": "Тип постеров, отображаемых на экране панели", + "@settingsHomeNextUpDesc": {}, + "settingsHomeNextUpTitle": "Следующие постеры", + "@settingsHomeNextUpTitle": {}, + "settingsPlayerCustomSubtitlesDesc": "Настроить размер, цвет, положение, контур", + "@settingsPlayerCustomSubtitlesDesc": {}, + "settingsPlayerDesc": "Соотношение сторон, Дополнительно", + "@settingsPlayerDesc": {}, + "settingsPlayerMobileWarning": "Включение аппаратного ускорения и нативных субтитров libass на Android может привести к тому, что некоторые субтитры не будут отображаться.", + "@settingsPlayerMobileWarning": {}, + "settingsPlayerNativeLibassAccelDesc": "Использование видеоплеера libass для отображения субтитров", + "@settingsPlayerNativeLibassAccelDesc": {}, + "settingsPlayerNativeLibassAccelTitle": "Наивные субтитры libass", + "@settingsPlayerNativeLibassAccelTitle": {}, + "settingsPlayerBufferSizeDesc": "Настроить размер буфера для воспроизведения видео, определяющий объем данных, загружаемых в кэш.", + "@settingsPlayerBufferSizeDesc": {}, + "settingsPlayerVideoHWAccelDesc": "Используйте GPU для рендеринга видео (рекомендуется)", + "@settingsPlayerVideoHWAccelDesc": {}, + "settingsPosterPinch": "Уменьшение масштаба постеров", + "@settingsPosterPinch": {}, + "settingsPosterSize": "Размер постера", + "@settingsPosterSize": {}, + "settingsPosterSlider": "Показать ползунок масштаба", + "@settingsPosterSlider": {}, + "settingsProfileTitle": "Профиль", + "@settingsProfileTitle": {}, + "settingsQuickConnectTitle": "Быстрое подключение", + "@settingsQuickConnectTitle": {}, + "settingsSecurity": "Безопасность", + "@settingsSecurity": {}, + "settingsShowScaleSlider": "Показать ползунок размером с постер", + "@settingsShowScaleSlider": {}, + "settingsVisual": "Визуальный", + "@settingsVisual": {}, + "showDetails": "Показать детали", + "@showDetails": {}, + "showEmpty": "Показать пустое", + "@showEmpty": {}, + "shuffleGallery": "Перемешать галереи", + "@shuffleGallery": {}, + "shuffleVideos": "Перемешать видео", + "@shuffleVideos": {}, + "somethingWentWrong": "Что-то пошло не так", + "@somethingWentWrong": {}, + "sortBy": "Сортировать по", + "@sortBy": {}, + "sortOrder": "Порядок сортировки", + "@sortOrder": {}, + "start": "Начало", + "@start": {}, + "studio": "{count, plural, other{Студий} two{Студии} one{Студия}}", + "@studio": { + "description": "studio", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "subtitles": "Субтитры", + "@subtitles": {}, + "switchUser": "Переключить пользователя", + "@switchUser": {}, + "sync": "Синхронизировать", + "@sync": {}, + "immediately": "Сразу", + "@immediately": {}, + "incorrectPinTryAgain": "Неверный PIN-код. Попробуйте еще раз.", + "@incorrectPinTryAgain": {}, + "groupBy": "Группировать по", + "@groupBy": {}, + "downloadsClearDesc": "Удалить все синхронизированные данные, очистив все данные для каждого синхронизированного пользователя?", + "@downloadsClearDesc": {}, + "noResults": "Нет результатов", + "@noResults": {}, + "shadow": "Тень", + "@shadow": {}, + "play": "Воспроизвести {item}", + "@play": { + "description": "Play with", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "removeFromPlaylist": "Удалить из плейлиста", + "@removeFromPlaylist": {}, + "libraryPageSizeTitle": "Размер страницы библиотеки", + "@libraryPageSizeTitle": {}, + "setIdentityTo": "Сменить идентификатор на {name}", + "@setIdentityTo": { + "description": "setIdentityTo", + "placeholders": { + "name": { + "type": "String" + } + } + }, + "genre": "{count, plural, other{Жанров} two{Жанра} one{Жанр}}", + "@genre": { + "description": "genre", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "homeBannerSlideshow": "Слайд-шоу", + "@homeBannerSlideshow": {}, + "minutes": "{count, plural, other{Минут} two{Минуты} one{Минута} }", + "@minutes": { + "description": "minute", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "outlineColor": "Цвет контура", + "@outlineColor": {}, + "pathEditDesc": "Это путь установлен для всех пользователей, все синхронизированные данные больше не будут доступны. Они останутся в вашем хранилище.", + "@pathEditDesc": {}, + "scanYourFingerprintToAuthenticate": "Сканирование отпечатка пальца для аутентификации {user}", + "@scanYourFingerprintToAuthenticate": { + "placeholders": { + "user": { + "type": "String" + } + } + }, + "settingsHomeBannerInformationDesc": "Информация для отображения на основном баннере", + "@settingsHomeBannerInformationDesc": {}, + "settingsPlayerBufferSizeTitle": "Размер видеобуфера", + "@settingsPlayerBufferSizeTitle": {}, + "settingsPlayerVideoHWAccelTitle": "Аппаратное ускорение", + "@settingsPlayerVideoHWAccelTitle": {}, + "settingsProfileDesc": "Экран блокировки", + "@settingsProfileDesc": {}, + "showAlbum": "Покатать альбом", + "@showAlbum": {}, + "settingsNextUpCutoffDays": "Лимит дней для \"Следующего\"", + "@settingsNextUpCutoffDays": {}, + "somethingWentWrongPasswordCheck": "Что-то пошло не так. Проверьте свой пароль.", + "@somethingWentWrongPasswordCheck": {}, + "subtitleConfiguratorPlaceHolder": "Это текст-заготовка, здесь нечего смотреть.", + "@subtitleConfiguratorPlaceHolder": {}, + "settingsPlayerTitle": "Плеер", + "@settingsPlayerTitle": {}, + "sortName": "Название", + "@sortName": {}, + "quickConnectAction": "Введите код быстрого подключения для", + "@quickConnectAction": {}, + "settingsEnableOsMediaControls": "Включить средства управления мультимедиа ОС", + "@settingsEnableOsMediaControls": {}, + "settingsPlayerCustomSubtitlesTitle": "Настройка субтитров", + "@settingsPlayerCustomSubtitlesTitle": {}, + "replaceExistingImages": "Заменить существующие изображения", + "@replaceExistingImages": {}, + "played": "Воспроизведено", + "@played": {}, + "logoutUserPopupContent": "Это приведет к выходу из {userName} и удалению пользователя из приложения.\nВам придется снова войти в систему на {serverName}.", + "@logoutUserPopupContent": { + "description": "Pop-up for logging out the user description", + "placeholders": { + "userName": { + "type": "String" + }, + "serverName": { + "type": "String" + } + } + }, + "subtitleConfigurator": "Конфигуратор субтитров", + "@subtitleConfigurator": {}, + "pathEditSelect": "Выберите место загрузки", + "@pathEditSelect": {}, + "saved": "Сохранено", + "@saved": {}, + "maxConcurrentDownloadsTitle": "Максимальное количество одновременных загрузок", + "@maxConcurrentDownloadsTitle": {}, + "exitFladderTitle": "Выйти из Fladder", + "@exitFladderTitle": {}, + "maxConcurrentDownloadsDesc": "Устанавливает максимальное количество загрузок, которые могут выполняться одновременно. Установите значение 0, чтобы отключить ограничение.", + "@maxConcurrentDownloadsDesc": {}, + "rememberAudioSelections": "Установка звуковой дорожки на основе предыдущего элемента", + "@rememberAudioSelections": {}, + "similarToRecentlyPlayed": "Похожие на недавно просмотренные", + "@similarToRecentlyPlayed": {}, + "similarToLikedItem": "Похожие на понравившиеся", + "@similarToLikedItem": {}, + "hasDirectorFromRecentlyPlayed": "Имеет режиссёра из недавно воспроизведённых", + "@hasDirectorFromRecentlyPlayed": {}, + "hasActorFromRecentlyPlayed": "Имеет актера из недавно воспроизведённых", + "@hasActorFromRecentlyPlayed": {}, + "hasLikedDirector": "Имеет понравившегося режиссера", + "@hasLikedDirector": {}, + "hasLikedActor": "Имеет понравившегося актёра", + "@hasLikedActor": {}, + "latest": "Последние", + "@latest": {}, + "recommended": "Рекомендуемое", + "@recommended": {}, + "playbackTypeTranscode": "Транскодируемое", + "@playbackTypeTranscode": {}, + "playbackTypeOffline": "Оффлайн", + "@playbackTypeOffline": {}, + "latestReleases": "Последние выпуски", + "@latestReleases": {}, + "autoCheckForUpdates": "Периодически проверяйте наличие обновлений", + "@autoCheckForUpdates": {}, + "newUpdateFoundOnGithub": "На Github появилось новое обновление", + "@newUpdateFoundOnGithub": {}, + "syncDeletePopupPermanent": "Это действие является постоянным и удаляет все локально синхронизированные файлы", + "@syncDeletePopupPermanent": {}, + "syncDetails": "Детали синхронизации", + "@syncDetails": {}, + "syncOpenParent": "Открытый родительский элемент", + "@syncOpenParent": {}, + "syncedItems": "Синхронизированные элементы", + "@syncedItems": {}, + "tag": "{count, plural, one{Тег} two{Тега} other{Тегов}}", + "@tag": { + "description": "tag", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "theme": "Тема", + "@theme": {}, + "themeModeDark": "Темная", + "@themeModeDark": {}, + "timeAndAnnotation": "{minutes} и {seconds}", + "@timeAndAnnotation": { + "description": "timeAndAnnotation", + "placeholders": { + "minutes": { + "type": "String" + }, + "seconds": { + "type": "String" + } + } + }, + "timeOut": "Тайм-аут", + "@timeOut": {}, + "totalSize": "Общий размер: {size}", + "@totalSize": { + "placeholders": { + "size": { + "type": "String" + } + } + }, + "type": "{count, plural, other{Типов} two{Типа} one{Тип}}", + "@type": { + "description": "type", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "unableToReverseAction": "Это действие не может быть отменено. Оно удалит все настройки.", + "@unableToReverseAction": {}, + "unknown": "Неизвестный", + "@unknown": {}, + "useDefaults": "Использовать значения по умолчанию", + "@useDefaults": {}, + "userName": "Имя пользователя", + "@userName": {}, + "videoScaling": "Масштабирование видео", + "@videoScaling": {}, + "videoScalingContain": "Вместить", + "@videoScalingContain": {}, + "videoScalingCover": "Перекрыть", + "@videoScalingCover": {}, + "videoScalingFillScreenNotif": "Полноэкранный режим переопределяет заполнение видео при горизонтальном повороте", + "@videoScalingFillScreenNotif": {}, + "videoScalingFitHeight": "Заполнить по высоте", + "@videoScalingFitHeight": {}, + "viewPhotos": "Посмотреть фотографии", + "@viewPhotos": {}, + "year": "{count, plural, other{Лет} two{Года} one{Год}}", + "@year": { + "description": "year", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "addToNewPlaylist": "Новый плейлист", + "@addToNewPlaylist": {}, + "addItemsToPlaylist": "Добавить элемент(ы) {itemLength} в плейлист", + "@addItemsToPlaylist": { + "placeholders": { + "itemLength": { + "type": "int" + } + } + }, + "syncStatusEnqueued": "Записано", + "@syncStatusEnqueued": {}, + "syncStatusComplete": "Завершено", + "@syncStatusComplete": {}, + "syncStatusNotFound": "Не найдено", + "@syncStatusNotFound": {}, + "syncStatusFailed": "Не удалось", + "@syncStatusFailed": {}, + "syncStatusRunning": "В процессе", + "@syncStatusRunning": {}, + "syncStatusWaitingToRetry": "Ожидание повторной попытки", + "@syncStatusWaitingToRetry": {}, + "syncStatusPaused": "Приостановлено", + "@syncStatusPaused": {}, + "syncStatusSynced": "Синхронизировано", + "@syncStatusSynced": {}, + "syncStatusPartially": "Частично", + "@syncStatusPartially": {}, + "syncOverlayDeleting": "Удаление синхронизированного элемента", + "@syncOverlayDeleting": {}, + "syncSelectDownloadsFolder": "Выбрать папку для загрузок", + "@syncSelectDownloadsFolder": {}, + "syncRemoveUnableToDeleteItem": "Невозможно удалить синхронизированный элемент, что-то пошло не так", + "@syncRemoveUnableToDeleteItem": {}, + "syncAddItemForSyncing": "Добавлен {item} для синхронизации", + "@syncAddItemForSyncing": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "startedSyncingItem": "Началась синхронизация {item}", + "@startedSyncingItem": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "aboutVersion": "Версия: {version}", + "@aboutVersion": { + "placeholders": { + "version": { + "type": "String" + } + } + }, + "aboutCreatedBy": "Создано DonutWare", + "@aboutCreatedBy": {}, + "aboutSocials": "Социальные сети", + "@aboutSocials": {}, + "aboutLicenses": "Лицензии", + "@aboutLicenses": {}, + "subtitleConfiguration": "Настройка субтитров", + "@subtitleConfiguration": {}, + "off": "Выключено", + "@off": {}, + "screenBrightness": "Яркость экрана", + "@screenBrightness": {}, + "autoNextOffStaticTitle": "Статический", + "@autoNextOffStaticTitle": {}, + "autoNextOffStaticDesc": "Показывайте следующее, когда остается 30 секунд до конца воспроизведения", + "@autoNextOffStaticDesc": {}, + "errorOpeningMedia": "Что-то пошло не так при попытке воспроизвести это медиа", + "@errorOpeningMedia": {}, + "unableToPlayBooksOnWeb": "Книги пока не поддерживаются в вебе", + "@unableToPlayBooksOnWeb": {}, + "defaultFilterForLibrary": "Фильтр по умолчанию для библиотеки", + "@defaultFilterForLibrary": {}, + "removeFilterForLibrary": "Удалить {filter}?", + "@removeFilterForLibrary": { + "description": "removeFilterForLibrary", + "placeholders": { + "filter": { + "type": "String" + } + } + }, + "libraryFiltersLimitReached": "Достигнут предел фильтрации (10) Удалите некоторые фильтры", + "@libraryFiltersLimitReached": {}, + "libraryFiltersRemoveAll": "Удалить все фильтры", + "@libraryFiltersRemoveAll": {}, + "libraryFiltersRemoveAllConfirm": "Это приведет к удалению всех сохраненных фильтров для каждой библиотеки", + "@libraryFiltersRemoveAllConfirm": {}, + "deviceOrientationPortraitDown": "Портретная ориентация вниз", + "@deviceOrientationPortraitDown": {}, + "schemeSettingsFidelity": "Точность", + "@schemeSettingsFidelity": {}, + "schemeSettingsVibrant": "Яркий", + "@schemeSettingsVibrant": {}, + "schemeSettingsExpressive": "Выразительный", + "@schemeSettingsExpressive": {}, + "schemeSettingsContent": "Содержание", + "@schemeSettingsContent": {}, + "schemeSettingsRainbow": "Радуга", + "@schemeSettingsRainbow": {}, + "schemeSettingsFruitSalad": "Фруктовый салат", + "@schemeSettingsFruitSalad": {}, + "clientSettingsRequireWifiTitle": "Требовать Wi-Fi", + "@clientSettingsRequireWifiTitle": {}, + "clientSettingsRequireWifiDesc": "Загружать только при подключении к сети Wi-Fi", + "@clientSettingsRequireWifiDesc": {}, + "libraryShuffleAndPlayItems": "Перемешать и воспроизвести элементы", + "@libraryShuffleAndPlayItems": {}, + "libraryPlayItems": "Воспроизвести элементы", + "@libraryPlayItems": {}, + "clientSettingsShowAllCollectionsTitle": "Показать все типы коллекций", + "@clientSettingsShowAllCollectionsTitle": {}, + "clientSettingsShowAllCollectionsDesc": "Если включить эту функцию, будут показаны все типы коллекций, включая те, которые не поддерживаются Fladder", + "@clientSettingsShowAllCollectionsDesc": {}, + "stop": "Остановить", + "@stop": {}, + "playNextVideo": "Воспроизвести следующее видео", + "@playNextVideo": {}, + "playerSettingsBackendTitle": "Бэкенд видеоплеера", + "@playerSettingsBackendTitle": {}, + "playerSettingsBackendDesc": "Выберите предпочтительный медиаплеер для оптимального воспроизведения", + "@playerSettingsBackendDesc": {}, + "defaultLabel": "По умолчанию", + "@defaultLabel": { + "description": "To indicate a default value, default video player backend" + }, + "skipButtonLabel": "Пропустить {segment}", + "@skipButtonLabel": { + "placeholders": { + "segment": { + "type": "String" + } + } + }, + "mediaSegmentUnknown": "Неизвестный", + "@mediaSegmentUnknown": {}, + "mediaSegmentOutro": "Финал", + "@mediaSegmentOutro": {}, + "mediaSegmentIntro": "Вступление", + "@mediaSegmentIntro": {}, + "errorLogs": "Журнал ошибок", + "@errorLogs": {}, + "external": "Внешний", + "@external": {}, + "downloadFile": "Загрузить {type}", + "@downloadFile": { + "placeholders": { + "type": { + "type": "String" + } + } + }, + "copyStreamUrl": "Скопировать url потока", + "@copyStreamUrl": {}, + "settingsLayoutSizesTitle": "Размеры компоновки", + "@settingsLayoutSizesTitle": {}, + "settingsLayoutSizesDesc": "Выберите, какие размеры компоновки может использовать приложение в зависимости от размера окна", + "@settingsLayoutSizesDesc": {}, + "settingsLayoutModesTitle": "Режимы компоновки", + "@settingsLayoutModesTitle": {}, + "settingsLayoutModesDesc": "Контролируйте, может ли приложение использовать однопанельную или двухпанельную компоновку", + "@settingsLayoutModesDesc": {}, + "phone": "Телефон", + "@phone": {}, + "tablet": "Планшет", + "@tablet": {}, + "desktop": "Настольный компьютер", + "@desktop": {}, + "layoutModeSingle": "Одиночная", + "@layoutModeSingle": {}, + "layoutModeDual": "Двойная", + "@layoutModeDual": {}, + "episodeAvailable": "Доступно", + "@episodeAvailable": {}, + "episodeMissing": "Отсутствует", + "@episodeMissing": {}, + "homeStreamingQualityTitle": "Качество дома", + "@homeStreamingQualityTitle": {}, + "qualityOptionsTitle": "Настройки качества", + "@qualityOptionsTitle": {}, + "guestActor": "{count, plural, other{Приглашенных актеров} two{Приглашенных актёра} one{Приглашенный актёр}}", + "@guestActor": { + "description": "Guest actors", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "qualityOptionsAuto": "Авто", + "@qualityOptionsAuto": {}, + "version": "Версия", + "@version": {}, + "mediaSegmentActions": "Действия с сегментами медиа", + "@mediaSegmentActions": {}, + "segmentActionNone": "Нет", + "@segmentActionNone": {}, + "segmentActionAskToSkip": "Спросить пропустить", + "@segmentActionAskToSkip": {}, + "segmentActionSkip": "Пропустить", + "@segmentActionSkip": {}, + "loading": "Загрузка", + "@loading": {}, + "castAndCrew": "Кастинг и съемочная группа", + "@castAndCrew": {}, + "syncRemoveDataDesc": "Удалить синхронизированные видеоданные? Это необратимо, и вам придется заново синхронизировать файлы", + "@syncRemoveDataDesc": {}, + "videoScalingFitWidth": "Заполнить по ширине", + "@videoScalingFitWidth": {}, + "subtitle": "Субтитры", + "@subtitle": {}, + "deviceOrientationPortraitUp": "Портретная ориентация вверх", + "@deviceOrientationPortraitUp": {}, + "unableToConnectHost": "Невозможно подключиться к хосту", + "@unableToConnectHost": {}, + "videoScalingFill": "Заполнить", + "@videoScalingFill": {}, + "watchOn": "Смотреть на", + "@watchOn": {}, + "removedFromCollection": "Удалено из коллекции {collectionName}", + "@removedFromCollection": { + "placeholders": { + "collectionName": { + "type": "String" + } + } + }, + "copiedToClipboard": "Скопировано в буфер обмена", + "@copiedToClipboard": {}, + "schemeSettingsNeutral": "Нейтральный", + "@schemeSettingsNeutral": {}, + "clientSettingsSchemeVariantTitle": "Вариант схемы", + "@clientSettingsSchemeVariantTitle": {}, + "closeVideo": "Закрыть видео", + "@closeVideo": {}, + "mediaSegmentPreview": "Предварительный просмотр", + "@mediaSegmentPreview": {}, + "addToNewCollection": "Новая коллекция", + "@addToNewCollection": {}, + "syncOverlaySyncing": "Синхронизация сведений об элементе", + "@syncOverlaySyncing": {}, + "themeModeLight": "Светлая", + "@themeModeLight": {}, + "writer": "{count, plural, other{Писателей} two{Писателя} one{Писатель}}", + "@writer": { + "description": "writer", + "placeholders": { + "count": { + "type": "int", + "example": "1" + } + } + }, + "syncDeleteItemDesc": "Удалить все синхронизированные данные для {item}?", + "@syncDeleteItemDesc": { + "description": "Sync delete item pop-up window", + "placeholders": { + "item": { + "type": "String" + } + } + }, + "playBackSettings": "Настройки воспроизведения", + "@playBackSettings": {}, + "syncRemoveDataTitle": "Удалить синхронизированные данные?", + "@syncRemoveDataTitle": {}, + "video": "Видео", + "@video": {}, + "syncStatusCanceled": "Отменено", + "@syncStatusCanceled": {}, + "syncDeleteItemTitle": "Удалить синхронизированный элемент", + "@syncDeleteItemTitle": {}, + "schemeSettingsTonalSpot": "Тональное пятно", + "@schemeSettingsTonalSpot": {}, + "videoScalingFillScreenTitle": "Заполнить экран", + "@videoScalingFillScreenTitle": {}, + "unPlayed": "Не воспроизведено", + "@unPlayed": {}, + "deviceOrientationLandscapeRight": "Ландшафтная ориентация вправо", + "@deviceOrientationLandscapeRight": {}, + "resumeVideo": "Продолжить видео", + "@resumeVideo": {}, + "aboutBuild": "Сборка: {buildNumber}", + "@aboutBuild": { + "placeholders": { + "buildNumber": { + "type": "String" + } + } + }, + "schemeSettingsMonochrome": "Монохром", + "@schemeSettingsMonochrome": {}, + "addedToCollection": "Добавлено в коллекцию {collectionName}", + "@addedToCollection": { + "placeholders": { + "collectionName": { + "type": "String" + } + } + }, + "settingsAutoNextTitle": "Следующее превью", + "@settingsAutoNextTitle": {}, + "playbackRate": "Скорость воспроизведения", + "@playbackRate": {}, + "deviceOrientationLandscapeLeft": "Ландшафтная ориентация влево", + "@deviceOrientationLandscapeLeft": {}, + "mediaSegmentRecap": "Краткое содержание", + "@mediaSegmentRecap": {}, + "episodeUnaired": "Без рекламы", + "@episodeUnaired": {}, + "mediaSegmentCommercial": "Реклама", + "@mediaSegmentCommercial": {}, + "rememberSubtitleSelectionsDesc": "Постараться установить дорожку субтитров так, чтобы она максимально соответствовала последнему видео.", + "@rememberSubtitleSelectionsDesc": {}, + "themeColor": "Цвет темы", + "@themeColor": {}, + "unableToPlayMedia": "Возникла ошибка при поиске совместимого типа носителя", + "@unableToPlayMedia": {}, + "addItemsToCollection": "Добавить {itemLength} элемент(ы) в коллекцию", + "@addItemsToCollection": { + "placeholders": { + "itemLength": { + "type": "int" + } + } + }, + "speed": "Скорость", + "@speed": {}, + "videoScalingScaleDown": "Уменьшить масштаб", + "@videoScalingScaleDown": {}, + "syncNoFolderSetup": "Отсутствует настройка папки синхронизации", + "@syncNoFolderSetup": {}, + "unableToSyncItem": "Невозможно синхронизировать {item}, что-то пошло не так", + "@unableToSyncItem": { + "placeholders": { + "item": { + "type": "String" + } + } + }, + "deleteFilterConfirmation": "Вы уверены, что хотите удалить этот фильтр?", + "@deleteFilterConfirmation": {}, + "settingsAutoNextDesc": "Отображает предварительный просмотр следующего элемента в конце, если другой элемент находится в очереди", + "@settingsAutoNextDesc": {}, + "playerSettingsOrientationDesc": "Принудительно установить ориентацию для видеоплеера", + "@playerSettingsOrientationDesc": {}, + "autoNextOffSmartTitle": "Умный", + "@autoNextOffSmartTitle": {}, + "updateFilterForLibrary": "Обновить фильтр", + "@updateFilterForLibrary": {}, + "noVideoPlayerOptions": "Выбранный бэкэнд не имеет опций", + "@noVideoPlayerOptions": {}, + "mdkExperimental": "MDK все еще находится на экспериментальной стадии", + "@mdkExperimental": {}, + "playbackType": "Тип воспроизведения", + "@playbackType": {}, + "internetStreamingQualityDesc": "Максимальное качество потокового вещания через интернет (мобильный)", + "@internetStreamingQualityDesc": {}, + "homeStreamingQualityDesc": "Максимальное качество потокового вещания при подключении в домашней сети", + "@homeStreamingQualityDesc": {}, + "qualityOptionsOriginal": "Оригинал", + "@qualityOptionsOriginal": {}, + "playbackTypeDirect": "Прямое", + "@playbackTypeDirect": {}, + "autoNextOffSmartDesc": "Показывает следующее, когда начинаются титры, если после титров осталось не более 10 секунд. В противном случае показывать следующиее, когда остается 30 секунд", + "@autoNextOffSmartDesc": {}, + "playbackTrackSelection": "Выбор дорожки воспроизведения", + "@playbackTrackSelection": {}, + "rememberSubtitleSelections": "Установка дорожки субтитров на основе предыдущего элемента", + "@rememberSubtitleSelections": {}, + "rememberAudioSelectionsDesc": "Постараться установить звуковую дорожку, максимально приближенную к последнему видео.", + "@rememberAudioSelectionsDesc": {}, + "videoScalingFillScreenDesc": "Заполните панель навигации и статусную панель", + "@videoScalingFillScreenDesc": {}, + "addedToPlaylist": "Добавлено в плейлист {playlistName}", + "@addedToPlaylist": { + "placeholders": { + "playlistName": { + "type": "String" + } + } + }, + "playerSettingsOrientationTitle": "Ориентация плеера", + "@playerSettingsOrientationTitle": {}, + "internetStreamingQualityTitle": "Качество интернета", + "@internetStreamingQualityTitle": {}, + "themeModeSystem": "Системная", + "@themeModeSystem": {}, + "scale": "Масштаб", + "@scale": {}, + "newReleaseFoundTitle": "Доступно обновление {newRelease}!", + "@newReleaseFoundTitle": { + "placeholders": { + "newRelease": { + "type": "String" + } + } + }, + "enableBackgroundPostersTitle": "Включить фоновые постеры", + "@enableBackgroundPostersTitle": {}, + "enableBackgroundPostersDesc": "Показывать случайные постеры на соответствующих экранах", + "@enableBackgroundPostersDesc": {}, + "settingsEnableOsMediaControlsDesc": "Позволяет управлять воспроизведением с помощью медиа-клавиш и показывать текущее воспроизводимое медиа в ОС", + "@settingsEnableOsMediaControlsDesc": {} +} diff --git a/lib/l10n/app_sk.arb b/lib/l10n/app_sk.arb new file mode 100644 index 0000000..b0180b9 --- /dev/null +++ b/lib/l10n/app_sk.arb @@ -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" + } + } + } +} diff --git a/lib/l10n/app_ta.arb b/lib/l10n/app_ta.arb index 2203a0f..12de5af 100644 --- a/lib/l10n/app_ta.arb +++ b/lib/l10n/app_ta.arb @@ -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" + } + } + } } diff --git a/lib/l10n/app_uk.arb b/lib/l10n/app_uk.arb index ea38183..2f090ae 100644 --- a/lib/l10n/app_uk.arb +++ b/lib/l10n/app_uk.arb @@ -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": {} } diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 5c7f784..3e05ab7 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -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": {} } diff --git a/lib/main.dart b/lib/main.dart index e01ef0f..e21ea46 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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> loadConfig() async { return jsonDecode(configString); } -void main() async { +void main(List 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
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
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
with WindowListener, WidgetsBinding colorScheme: darkTheme.colorScheme.copyWith( surface: amoledOverwrite, surfaceContainerHighest: amoledOverwrite, + surfaceContainerLow: amoledOverwrite, ), ), themeMode: themeMode, diff --git a/lib/models/account_model.dart b/lib/models/account_model.dart index b5070d2..16bd46f 100644 --- a/lib/models/account_model.dart +++ b/lib/models/account_model.dart @@ -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 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 json) => _$AccountModelFromJson(json); diff --git a/lib/models/account_model.freezed.dart b/lib/models/account_model.freezed.dart index 568621b..e251539 100644 --- a/lib/models/account_model.freezed.dart +++ b/lib/models/account_model.freezed.dart @@ -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 toJson() => throw _privateConstructorUsedError; @@ -68,7 +71,9 @@ abstract class $AccountModelCopyWith<$Res> { List 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 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 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 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. diff --git a/lib/models/boxset_model.mapper.dart b/lib/models/boxset_model.mapper.dart index 3904439..14ca869 100644 --- a/lib/models/boxset_model.mapper.dart +++ b/lib/models/boxset_model.mapper.dart @@ -108,34 +108,11 @@ class BoxSetModelMapper extends SubClassMapperBase { @override final Function instantiate = _instantiate; - - static BoxSetModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static BoxSetModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin BoxSetModelMappable { - String toJson() { - return BoxSetModelMapper.ensureInitialized() - .encodeJson(this as BoxSetModel); - } - - Map toMap() { - return BoxSetModelMapper.ensureInitialized() - .encodeMap(this as BoxSetModel); - } - BoxSetModelCopyWith get copyWith => _BoxSetModelCopyWithImpl(this as BoxSetModel, $identity, $identity); - @override - String toString() { - return BoxSetModelMapper.ensureInitialized() - .stringifyValue(this as BoxSetModel); - } } extension BoxSetModelValueCopy<$R, $Out> diff --git a/lib/models/collection_types.dart b/lib/models/collection_types.dart index 10867df..282f430 100644 --- a/lib/models/collection_types.dart +++ b/lib/models/collection_types.dart @@ -17,6 +17,8 @@ extension CollectionTypeExtension on CollectionType { Set 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, + }; } diff --git a/lib/models/item_base_model.dart b/lib/models/item_base_model.dart index ee16017..54b42d1 100644 --- a/lib/models/item_base_model.dart +++ b/lib/models/item_base_model.dart @@ -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 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, diff --git a/lib/models/item_base_model.mapper.dart b/lib/models/item_base_model.mapper.dart index fe85f01..e3a288f 100644 --- a/lib/models/item_base_model.mapper.dart +++ b/lib/models/item_base_model.mapper.dart @@ -93,35 +93,12 @@ class ItemBaseModelMapper extends ClassMapperBase { @override final Function instantiate = _instantiate; - - static ItemBaseModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static ItemBaseModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin ItemBaseModelMappable { - String toJson() { - return ItemBaseModelMapper.ensureInitialized() - .encodeJson(this as ItemBaseModel); - } - - Map toMap() { - return ItemBaseModelMapper.ensureInitialized() - .encodeMap(this as ItemBaseModel); - } - ItemBaseModelCopyWith get copyWith => _ItemBaseModelCopyWithImpl( this as ItemBaseModel, $identity, $identity); - @override - String toString() { - return ItemBaseModelMapper.ensureInitialized() - .stringifyValue(this as ItemBaseModel); - } } extension ItemBaseModelValueCopy<$R, $Out> diff --git a/lib/models/items/episode_model.dart b/lib/models/items/episode_model.dart index 7f94a18..cb6d7b0 100644 --- a/lib/models/items/episode_model.dart +++ b/lib/models/items/episode_model.dart @@ -199,17 +199,34 @@ extension EpisodeListExtensions on List { } 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 { diff --git a/lib/models/items/episode_model.mapper.dart b/lib/models/items/episode_model.mapper.dart index bebdd8e..f5714d1 100644 --- a/lib/models/items/episode_model.mapper.dart +++ b/lib/models/items/episode_model.mapper.dart @@ -141,34 +141,11 @@ class EpisodeModelMapper extends SubClassMapperBase { @override final Function instantiate = _instantiate; - - static EpisodeModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static EpisodeModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin EpisodeModelMappable { - String toJson() { - return EpisodeModelMapper.ensureInitialized() - .encodeJson(this as EpisodeModel); - } - - Map toMap() { - return EpisodeModelMapper.ensureInitialized() - .encodeMap(this as EpisodeModel); - } - EpisodeModelCopyWith get copyWith => _EpisodeModelCopyWithImpl(this as EpisodeModel, $identity, $identity); - @override - String toString() { - return EpisodeModelMapper.ensureInitialized() - .stringifyValue(this as EpisodeModel); - } } extension EpisodeModelValueCopy<$R, $Out> diff --git a/lib/models/items/folder_model.mapper.dart b/lib/models/items/folder_model.mapper.dart index f36f7fd..8dde469 100644 --- a/lib/models/items/folder_model.mapper.dart +++ b/lib/models/items/folder_model.mapper.dart @@ -108,34 +108,11 @@ class FolderModelMapper extends SubClassMapperBase { @override final Function instantiate = _instantiate; - - static FolderModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static FolderModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin FolderModelMappable { - String toJson() { - return FolderModelMapper.ensureInitialized() - .encodeJson(this as FolderModel); - } - - Map toMap() { - return FolderModelMapper.ensureInitialized() - .encodeMap(this as FolderModel); - } - FolderModelCopyWith get copyWith => _FolderModelCopyWithImpl(this as FolderModel, $identity, $identity); - @override - String toString() { - return FolderModelMapper.ensureInitialized() - .stringifyValue(this as FolderModel); - } } extension FolderModelValueCopy<$R, $Out> diff --git a/lib/models/items/item_properties_model.freezed.dart b/lib/models/items/item_properties_model.freezed.dart index 6531a37..1f775a8 100644 --- a/lib/models/items/item_properties_model.freezed.dart +++ b/lib/models/items/item_properties_model.freezed.dart @@ -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 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; } diff --git a/lib/models/items/item_shared_models.dart b/lib/models/items/item_shared_models.dart index 45cc175..f20acc6 100644 --- a/lib/models/items/item_shared_models.dart +++ b/lib/models/items/item_shared_models.dart @@ -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; diff --git a/lib/models/items/item_shared_models.mapper.dart b/lib/models/items/item_shared_models.mapper.dart index 9a17f2b..34cde3e 100644 --- a/lib/models/items/item_shared_models.mapper.dart +++ b/lib/models/items/item_shared_models.mapper.dart @@ -92,10 +92,6 @@ mixin UserDataMappable { UserDataCopyWith 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> { diff --git a/lib/models/items/item_stream_model.mapper.dart b/lib/models/items/item_stream_model.mapper.dart index d83984c..550275a 100644 --- a/lib/models/items/item_stream_model.mapper.dart +++ b/lib/models/items/item_stream_model.mapper.dart @@ -112,35 +112,12 @@ class ItemStreamModelMapper extends SubClassMapperBase { @override final Function instantiate = _instantiate; - - static ItemStreamModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static ItemStreamModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin ItemStreamModelMappable { - String toJson() { - return ItemStreamModelMapper.ensureInitialized() - .encodeJson(this as ItemStreamModel); - } - - Map toMap() { - return ItemStreamModelMapper.ensureInitialized() - .encodeMap(this as ItemStreamModel); - } - ItemStreamModelCopyWith get copyWith => _ItemStreamModelCopyWithImpl( this as ItemStreamModel, $identity, $identity); - @override - String toString() { - return ItemStreamModelMapper.ensureInitialized() - .stringifyValue(this as ItemStreamModel); - } } extension ItemStreamModelValueCopy<$R, $Out> diff --git a/lib/models/items/media_segments_model.dart b/lib/models/items/media_segments_model.dart index 971b883..b3e9284 100644 --- a/lib/models/items/media_segments_model.dart +++ b/lib/models/items/media_segments_model.dart @@ -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 defaultSegmentSkipValues = { diff --git a/lib/models/items/media_segments_model.freezed.dart b/lib/models/items/media_segments_model.freezed.dart index f2c6bf9..cae9d70 100644 --- a/lib/models/items/media_segments_model.freezed.dart +++ b/lib/models/items/media_segments_model.freezed.dart @@ -24,82 +24,6 @@ mixin _$MediaSegmentsModel { /// Serializes this MediaSegmentsModel to a JSON map. Map 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 get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $MediaSegmentsModelCopyWith<$Res> { - factory $MediaSegmentsModelCopyWith( - MediaSegmentsModel value, $Res Function(MediaSegmentsModel) then) = - _$MediaSegmentsModelCopyWithImpl<$Res, MediaSegmentsModel>; - @useResult - $Res call({List 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, - ) 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 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, - )); - } } /// @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 toJson() { return _$$MediaSegmentsModelImplToJson( @@ -166,13 +68,6 @@ abstract class _MediaSegmentsModel extends MediaSegmentsModel { @override List 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 json) { @@ -187,102 +82,6 @@ mixin _$MediaSegment { /// Serializes this MediaSegment to a JSON map. Map 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 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 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; } diff --git a/lib/models/items/media_streams_model.dart b/lib/models/items/media_streams_model.dart index 711fd62..547c912 100644 --- a/lib/models/items/media_streams_model.dart +++ b/lib/models/items/media_streams_model.dart @@ -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 on Iterable { } } -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, diff --git a/lib/models/items/movie_model.mapper.dart b/lib/models/items/movie_model.mapper.dart index 56b8f4c..122798c 100644 --- a/lib/models/items/movie_model.mapper.dart +++ b/lib/models/items/movie_model.mapper.dart @@ -147,34 +147,11 @@ class MovieModelMapper extends SubClassMapperBase { @override final Function instantiate = _instantiate; - - static MovieModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static MovieModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin MovieModelMappable { - String toJson() { - return MovieModelMapper.ensureInitialized() - .encodeJson(this as MovieModel); - } - - Map toMap() { - return MovieModelMapper.ensureInitialized() - .encodeMap(this as MovieModel); - } - MovieModelCopyWith get copyWith => _MovieModelCopyWithImpl(this as MovieModel, $identity, $identity); - @override - String toString() { - return MovieModelMapper.ensureInitialized() - .stringifyValue(this as MovieModel); - } } extension MovieModelValueCopy<$R, $Out> diff --git a/lib/models/items/overview_model.dart b/lib/models/items/overview_model.dart index 6747026..76e21fc 100644 --- a/lib/models/items/overview_model.dart +++ b/lib/models/items/overview_model.dart @@ -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 map) => OverviewModelMapper.fromMap(map); - factory OverviewModel.fromJson(String json) => OverviewModelMapper.fromJson(json); } diff --git a/lib/models/items/overview_model.mapper.dart b/lib/models/items/overview_model.mapper.dart index 9afaf5b..c86fcdc 100644 --- a/lib/models/items/overview_model.mapper.dart +++ b/lib/models/items/overview_model.mapper.dart @@ -114,35 +114,12 @@ class OverviewModelMapper extends ClassMapperBase { @override final Function instantiate = _instantiate; - - static OverviewModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static OverviewModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin OverviewModelMappable { - String toJson() { - return OverviewModelMapper.ensureInitialized() - .encodeJson(this as OverviewModel); - } - - Map toMap() { - return OverviewModelMapper.ensureInitialized() - .encodeMap(this as OverviewModel); - } - OverviewModelCopyWith get copyWith => _OverviewModelCopyWithImpl( this as OverviewModel, $identity, $identity); - @override - String toString() { - return OverviewModelMapper.ensureInitialized() - .stringifyValue(this as OverviewModel); - } } extension OverviewModelValueCopy<$R, $Out> diff --git a/lib/models/items/person_model.mapper.dart b/lib/models/items/person_model.mapper.dart index 2577548..8429fa2 100644 --- a/lib/models/items/person_model.mapper.dart +++ b/lib/models/items/person_model.mapper.dart @@ -124,34 +124,11 @@ class PersonModelMapper extends SubClassMapperBase { @override final Function instantiate = _instantiate; - - static PersonModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static PersonModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin PersonModelMappable { - String toJson() { - return PersonModelMapper.ensureInitialized() - .encodeJson(this as PersonModel); - } - - Map toMap() { - return PersonModelMapper.ensureInitialized() - .encodeMap(this as PersonModel); - } - PersonModelCopyWith get copyWith => _PersonModelCopyWithImpl(this as PersonModel, $identity, $identity); - @override - String toString() { - return PersonModelMapper.ensureInitialized() - .stringifyValue(this as PersonModel); - } } extension PersonModelValueCopy<$R, $Out> diff --git a/lib/models/items/photos_model.mapper.dart b/lib/models/items/photos_model.mapper.dart index b19c827..5810dd5 100644 --- a/lib/models/items/photos_model.mapper.dart +++ b/lib/models/items/photos_model.mapper.dart @@ -108,35 +108,12 @@ class PhotoAlbumModelMapper extends SubClassMapperBase { @override final Function instantiate = _instantiate; - - static PhotoAlbumModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static PhotoAlbumModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin PhotoAlbumModelMappable { - String toJson() { - return PhotoAlbumModelMapper.ensureInitialized() - .encodeJson(this as PhotoAlbumModel); - } - - Map toMap() { - return PhotoAlbumModelMapper.ensureInitialized() - .encodeMap(this as PhotoAlbumModel); - } - PhotoAlbumModelCopyWith 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 { @override final Function instantiate = _instantiate; - - static PhotoModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static PhotoModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin PhotoModelMappable { - String toJson() { - return PhotoModelMapper.ensureInitialized() - .encodeJson(this as PhotoModel); - } - - Map toMap() { - return PhotoModelMapper.ensureInitialized() - .encodeMap(this as PhotoModel); - } - PhotoModelCopyWith get copyWith => _PhotoModelCopyWithImpl(this as PhotoModel, $identity, $identity); - @override - String toString() { - return PhotoModelMapper.ensureInitialized() - .stringifyValue(this as PhotoModel); - } } extension PhotoModelValueCopy<$R, $Out> diff --git a/lib/models/items/season_model.mapper.dart b/lib/models/items/season_model.mapper.dart index 4c5e5d3..733b10d 100644 --- a/lib/models/items/season_model.mapper.dart +++ b/lib/models/items/season_model.mapper.dart @@ -137,34 +137,11 @@ class SeasonModelMapper extends SubClassMapperBase { @override final Function instantiate = _instantiate; - - static SeasonModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static SeasonModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin SeasonModelMappable { - String toJson() { - return SeasonModelMapper.ensureInitialized() - .encodeJson(this as SeasonModel); - } - - Map toMap() { - return SeasonModelMapper.ensureInitialized() - .encodeMap(this as SeasonModel); - } - SeasonModelCopyWith get copyWith => _SeasonModelCopyWithImpl(this as SeasonModel, $identity, $identity); - @override - String toString() { - return SeasonModelMapper.ensureInitialized() - .stringifyValue(this as SeasonModel); - } } extension SeasonModelValueCopy<$R, $Out> diff --git a/lib/models/items/series_model.dart b/lib/models/items/series_model.dart index c1f16f7..1a22ef9 100644 --- a/lib/models/items/series_model.dart +++ b/lib/models/items/series_model.dart @@ -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'; diff --git a/lib/models/items/series_model.mapper.dart b/lib/models/items/series_model.mapper.dart index 7f9b7cf..44f2c80 100644 --- a/lib/models/items/series_model.mapper.dart +++ b/lib/models/items/series_model.mapper.dart @@ -135,34 +135,11 @@ class SeriesModelMapper extends SubClassMapperBase { @override final Function instantiate = _instantiate; - - static SeriesModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static SeriesModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin SeriesModelMappable { - String toJson() { - return SeriesModelMapper.ensureInitialized() - .encodeJson(this as SeriesModel); - } - - Map toMap() { - return SeriesModelMapper.ensureInitialized() - .encodeMap(this as SeriesModel); - } - SeriesModelCopyWith get copyWith => _SeriesModelCopyWithImpl(this as SeriesModel, $identity, $identity); - @override - String toString() { - return SeriesModelMapper.ensureInitialized() - .stringifyValue(this as SeriesModel); - } } extension SeriesModelValueCopy<$R, $Out> diff --git a/lib/models/items/trick_play_model.dart b/lib/models/items/trick_play_model.dart index 0015f94..adc5ee4 100644 --- a/lib/models/items/trick_play_model.dart +++ b/lib/models/items/trick_play_model.dart @@ -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, diff --git a/lib/models/items/trick_play_model.freezed.dart b/lib/models/items/trick_play_model.freezed.dart index 8e5fe57..1b77683 100644 --- a/lib/models/items/trick_play_model.freezed.dart +++ b/lib/models/items/trick_play_model.freezed.dart @@ -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) diff --git a/lib/models/library_filters_model.dart b/lib/models/library_filters_model.dart index 9224010..165cbc2 100644 --- a/lib/models/library_filters_model.dart +++ b/lib/models/library_filters_model.dart @@ -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 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, diff --git a/lib/models/library_filters_model.freezed.dart b/lib/models/library_filters_model.freezed.dart index 0a37d13..e7a72a7 100644 --- a/lib/models/library_filters_model.freezed.dart +++ b/lib/models/library_filters_model.freezed.dart @@ -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) diff --git a/lib/models/library_filters_model.g.dart b/lib/models/library_filters_model.g.dart index 6956352..4f6e79a 100644 --- a/lib/models/library_filters_model.g.dart +++ b/lib/models/library_filters_model.g.dart @@ -93,7 +93,7 @@ const _$FladderItemTypeEnumMap = { }; const _$SortingOptionsEnumMap = { - SortingOptions.name: 'name', + SortingOptions.sortName: 'sortName', SortingOptions.communityRating: 'communityRating', SortingOptions.parentalRating: 'parentalRating', SortingOptions.dateAdded: 'dateAdded', diff --git a/lib/models/library_search/library_search_model.dart b/lib/models/library_search/library_search_model.dart index adf7fbe..3e9df53 100644 --- a/lib/models/library_search/library_search_model.dart +++ b/lib/models/library_search/library_search_model.dart @@ -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, diff --git a/lib/models/library_search/library_search_model.mapper.dart b/lib/models/library_search/library_search_model.mapper.dart index 601c4df..33b6dac 100644 --- a/lib/models/library_search/library_search_model.mapper.dart +++ b/lib/models/library_search/library_search_model.mapper.dart @@ -94,7 +94,7 @@ class LibrarySearchModelMapper extends ClassMapperBase { v.sortingOption; static const Field _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 _f$sortOrder = Field('sortOrder', _$sortOrder, opt: true, def: SortingOrder.ascending); @@ -177,36 +177,13 @@ class LibrarySearchModelMapper extends ClassMapperBase { @override final Function instantiate = _instantiate; - - static LibrarySearchModel fromMap(Map map) { - return ensureInitialized().decodeMap(map); - } - - static LibrarySearchModel fromJson(String json) { - return ensureInitialized().decodeJson(json); - } } mixin LibrarySearchModelMappable { - String toJson() { - return LibrarySearchModelMapper.ensureInitialized() - .encodeJson(this as LibrarySearchModel); - } - - Map toMap() { - return LibrarySearchModelMapper.ensureInitialized() - .encodeMap(this as LibrarySearchModel); - } - LibrarySearchModelCopyWith get copyWith => _LibrarySearchModelCopyWithImpl( this as LibrarySearchModel, $identity, $identity); - @override - String toString() { - return LibrarySearchModelMapper.ensureInitialized() - .stringifyValue(this as LibrarySearchModel); - } } extension LibrarySearchModelValueCopy<$R, $Out> diff --git a/lib/models/library_search/library_search_options.dart b/lib/models/library_search/library_search_options.dart index adcd681..aa81c67 100644 --- a/lib/models/library_search/library_search_options.dart +++ b/lib/models/library_search/library_search_options.dart @@ -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 value; - List get toSortBy => [...value, ItemSortBy.name]; + List 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, diff --git a/lib/models/playback/playback_model.dart b/lib/models/playback/playback_model.dart index 902afe7..befe0f9 100644 --- a/lib/models/playback/playback_model.dart +++ b/lib/models/playback/playback_model.dart @@ -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 createOfflinePlaybackModel( + Future _createOfflinePlaybackModel( ItemBaseModel item, + MediaStreamsModel? streamModel, SyncedItem? syncedItem, { PlaybackModel? oldModel, }) async { @@ -156,53 +162,121 @@ class PlaybackModelHelper { ); } - Future 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 createPlaybackModel( + BuildContext? context, + ItemBaseModel? item, { + PlaybackModel? oldModel, + List? 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().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 createServerPlaybackModel( - ItemBaseModel? item, + Future _createServerPlaybackModel( + ItemBaseModel item, + MediaStreamsModel? streamModel, PlaybackType? type, { PlaybackModel? oldModel, - List? libraryQueue, + required List 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 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 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 response = await api.itemsItemIdPlaybackInfoPost( itemId: item.id, diff --git a/lib/models/playback/playback_options_dialogue.dart b/lib/models/playback/playback_options_dialogue.dart new file mode 100644 index 0000000..7be861e --- /dev/null +++ b/lib/models/playback/playback_options_dialogue.dart @@ -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 showPlaybackTypeSelection({ + required BuildContext context, + required Set 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 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); + }, + )) + ], + ); + } +} diff --git a/lib/models/recommended_model.dart b/lib/models/recommended_model.dart index 16f65dd..733a245 100644 --- a/lib/models/recommended_model.dart +++ b/lib/models/recommended_model.dart @@ -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 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? 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, + ); + } } diff --git a/lib/models/settings/arguments_model.dart b/lib/models/settings/arguments_model.dart new file mode 100644 index 0000000..d0cbe64 --- /dev/null +++ b/lib/models/settings/arguments_model.dart @@ -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 arguments) { + arguments = arguments.map((e) => e.trim()).toList(); + return ArgumentsModel( + htpcMode: arguments.contains('--htpc'), + ); + } +} diff --git a/lib/models/settings/arguments_model.freezed.dart b/lib/models/settings/arguments_model.freezed.dart new file mode 100644 index 0000000..881c561 --- /dev/null +++ b/lib/models/settings/arguments_model.freezed.dart @@ -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 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; +} diff --git a/lib/models/settings/client_settings_model.dart b/lib/models/settings/client_settings_model.dart index f6a8366..f8b9f35 100644 --- a/lib/models/settings/client_settings_model.dart +++ b/lib/models/settings/client_settings_model.dart @@ -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; diff --git a/lib/models/settings/client_settings_model.freezed.dart b/lib/models/settings/client_settings_model.freezed.dart index 3bb7f64..d506115 100644 --- a/lib/models/settings/client_settings_model.freezed.dart +++ b/lib/models/settings/client_settings_model.freezed.dart @@ -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 diff --git a/lib/models/settings/client_settings_model.g.dart b/lib/models/settings/client_settings_model.g.dart index d870f72..6c1c6b5 100644 --- a/lib/models/settings/client_settings_model.g.dart +++ b/lib/models/settings/client_settings_model.g.dart @@ -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 _$$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, }; diff --git a/lib/models/settings/home_settings_model.dart b/lib/models/settings/home_settings_model.dart index b6102b7..790a6b4 100644 --- a/lib/models/settings/home_settings_model.dart +++ b/lib/models/settings/home_settings_model.dart @@ -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 screenLayouts, @@ -36,42 +37,6 @@ T selectAvailableOrSmaller(T value, Set availableOptions, List 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, diff --git a/lib/models/settings/home_settings_model.freezed.dart b/lib/models/settings/home_settings_model.freezed.dart index c3fde07..35666b9 100644 --- a/lib/models/settings/home_settings_model.freezed.dart +++ b/lib/models/settings/home_settings_model.freezed.dart @@ -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) diff --git a/lib/models/settings/video_player_settings.dart b/lib/models/settings/video_player_settings.dart index 71f1499..38d2f23 100644 --- a/lib/models/settings/video_player_settings.dart +++ b/lib/models/settings/video_player_settings.dart @@ -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 diff --git a/lib/models/syncing/sync_item.dart b/lib/models/syncing/sync_item.dart index 035fdab..d87906c 100644 --- a/lib/models/syncing/sync_item.dart +++ b/lib/models/syncing/sync_item.dart @@ -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._(); diff --git a/lib/models/syncing/sync_item.freezed.dart b/lib/models/syncing/sync_item.freezed.dart index 2b13b60..bf1e235 100644 --- a/lib/models/syncing/sync_item.freezed.dart +++ b/lib/models/syncing/sync_item.freezed.dart @@ -63,7 +63,6 @@ abstract class $SyncedItemCopyWith<$Res> { List 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 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) diff --git a/lib/models/syncing/sync_settings_model.dart b/lib/models/syncing/sync_settings_model.dart index 880c24a..5ce7aa4 100644 --- a/lib/models/syncing/sync_settings_model.dart +++ b/lib/models/syncing/sync_settings_model.dart @@ -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._(); diff --git a/lib/models/syncing/sync_settings_model.freezed.dart b/lib/models/syncing/sync_settings_model.freezed.dart index cf1e3af..7370ac2 100644 --- a/lib/models/syncing/sync_settings_model.freezed.dart +++ b/lib/models/syncing/sync_settings_model.freezed.dart @@ -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) diff --git a/lib/models/video_stream_model.dart b/lib/models/video_stream_model.dart index 4083802..1abb49d 100644 --- a/lib/models/video_stream_model.dart +++ b/lib/models/video_stream_model.dart @@ -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 { diff --git a/lib/models/view_model.dart b/lib/models/view_model.dart index 31e4b0b..53c8d2b 100644 --- a/lib/models/view_model.dart +++ b/lib/models/view_model.dart @@ -1,9 +1,17 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; +import 'package:fladder/models/collection_types.dart'; import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/items/images_models.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart'; +import 'package:fladder/widgets/shared/item_actions.dart'; class ViewModel { final String name; @@ -16,7 +24,9 @@ class ViewModel { final CollectionType collectionType; final dto.PlayAccess playAccess; final List recentlyAdded; + final ImagesData? imageData; final int childCount; + final String? path; ViewModel({ required this.name, required this.id, @@ -28,7 +38,9 @@ class ViewModel { required this.collectionType, required this.playAccess, required this.recentlyAdded, + required this.imageData, required this.childCount, + required this.path, }); ViewModel copyWith({ @@ -42,7 +54,9 @@ class ViewModel { CollectionType? collectionType, dto.PlayAccess? playAccess, List? recentlyAdded, + ImagesData? imageData, int? childCount, + String? path, }) { return ViewModel( name: name ?? this.name, @@ -55,7 +69,9 @@ class ViewModel { collectionType: collectionType ?? this.collectionType, playAccess: playAccess ?? this.playAccess, recentlyAdded: recentlyAdded ?? this.recentlyAdded, + imageData: imageData ?? this.imageData, childCount: childCount ?? this.childCount, + path: path ?? this.path, ); } @@ -69,11 +85,13 @@ class ViewModel { canDownload: item.canDownload ?? false, parentId: item.parentId ?? "", recentlyAdded: [], + imageData: ImagesData.fromBaseItem(item, ref), collectionType: CollectionType.values .firstWhereOrNull((element) => element.name.toLowerCase() == item.collectionType?.value?.toLowerCase()) ?? CollectionType.folders, playAccess: item.playAccess ?? PlayAccess.none, childCount: item.childCount ?? 0, + path: "", ); } @@ -88,6 +106,27 @@ class ViewModel { return id.hashCode ^ serverId.hashCode; } + NavigationButton toNavigationButton( + bool selected, + bool horizontal, + bool expanded, + FutureOr Function() action, { + FutureOr Function()? onLongPress, + List? trailing, + }) { + return NavigationButton( + label: name, + selected: selected, + onPressed: action, + onLongPress: onLongPress, + horizontal: horizontal, + expanded: expanded, + trailing: trailing ?? [], + selectedIcon: Icon(collectionType.icon), + icon: Icon(collectionType.iconOutlined), + ); + } + @override String toString() { return 'ViewModel(name: $name, id: $id, serverId: $serverId, dateCreated: $dateCreated, canDelete: $canDelete, canDownload: $canDownload, parentId: $parentId, collectionType: $collectionType, playAccess: $playAccess, recentlyAdded: $recentlyAdded, childCount: $childCount)'; diff --git a/lib/providers/arguments_provider.dart b/lib/providers/arguments_provider.dart new file mode 100644 index 0000000..c46af5a --- /dev/null +++ b/lib/providers/arguments_provider.dart @@ -0,0 +1,5 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/settings/arguments_model.dart'; + +final argumentsStateProvider = StateProvider((ref) => ArgumentsModel()); diff --git a/lib/providers/dashboard_provider.dart b/lib/providers/dashboard_provider.dart index 9527f56..1c8ca7d 100644 --- a/lib/providers/dashboard_provider.dart +++ b/lib/providers/dashboard_provider.dart @@ -1,3 +1,5 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/models/home_model.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -6,7 +8,6 @@ import 'package:fladder/providers/service_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/util/list_extensions.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; final dashboardProvider = StateNotifierProvider((ref) { return DashboardNotifier(ref); @@ -34,6 +35,7 @@ class DashboardNotifier extends StateNotifier { ItemFields.mediasources, ItemFields.candelete, ItemFields.candownload, + ItemFields.primaryimageaspectratio, ], mediaTypes: [MediaType.video], enableTotalRecordCount: false, @@ -53,6 +55,7 @@ class DashboardNotifier extends StateNotifier { ItemFields.mediasources, ItemFields.candelete, ItemFields.candownload, + ItemFields.primaryimageaspectratio, ], mediaTypes: [MediaType.audio], enableTotalRecordCount: false, @@ -72,6 +75,7 @@ class DashboardNotifier extends StateNotifier { ItemFields.mediasources, ItemFields.candelete, ItemFields.candownload, + ItemFields.primaryimageaspectratio, ], mediaTypes: [MediaType.book], enableTotalRecordCount: false, @@ -84,14 +88,15 @@ class DashboardNotifier extends StateNotifier { final nextResponse = await api.showsNextUpGet( limit: 16, - nextUpDateCutoff: DateTime.now() - .subtract(ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))), + nextUpDateCutoff: DateTime.now().subtract( + ref.read(clientSettingsProvider.select((value) => value.nextUpDateCutoff ?? const Duration(days: 28)))), fields: [ ItemFields.parentid, ItemFields.mediastreams, ItemFields.mediasources, ItemFields.candelete, ItemFields.candownload, + ItemFields.primaryimageaspectratio, ], ); diff --git a/lib/providers/discovery_provider.dart b/lib/providers/discovery_provider.dart index 54fcb93..08243ac 100644 --- a/lib/providers/discovery_provider.dart +++ b/lib/providers/discovery_provider.dart @@ -4,9 +4,10 @@ import 'dart:developer'; import 'dart:io'; import 'package:dart_mappable/dart_mappable.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/providers/service_provider.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'discovery_provider.g.dart'; part 'discovery_provider.mapper.dart'; @@ -81,7 +82,7 @@ class ServerDiscovery extends _$ServerDiscovery { } } -@MappableClass() +@MappableClass(generateMethods: GenerateMethods.encode | GenerateMethods.decode) class DiscoveryInfo with DiscoveryInfoMappable { @MappableField(key: 'Id') final String id; diff --git a/lib/providers/discovery_provider.mapper.dart b/lib/providers/discovery_provider.mapper.dart index aeb8044..eb28f0e 100644 --- a/lib/providers/discovery_provider.mapper.dart +++ b/lib/providers/discovery_provider.mapper.dart @@ -73,58 +73,4 @@ mixin DiscoveryInfoMappable { return DiscoveryInfoMapper.ensureInitialized() .encodeMap(this as DiscoveryInfo); } - - DiscoveryInfoCopyWith - get copyWith => _DiscoveryInfoCopyWithImpl( - this as DiscoveryInfo, $identity, $identity); - @override - String toString() { - return DiscoveryInfoMapper.ensureInitialized() - .stringifyValue(this as DiscoveryInfo); - } -} - -extension DiscoveryInfoValueCopy<$R, $Out> - on ObjectCopyWith<$R, DiscoveryInfo, $Out> { - DiscoveryInfoCopyWith<$R, DiscoveryInfo, $Out> get $asDiscoveryInfo => - $base.as((v, t, t2) => _DiscoveryInfoCopyWithImpl(v, t, t2)); -} - -abstract class DiscoveryInfoCopyWith<$R, $In extends DiscoveryInfo, $Out> - implements ClassCopyWith<$R, $In, $Out> { - $R call({String? id, String? name, String? address, String? endPointAddress}); - DiscoveryInfoCopyWith<$R2, $In, $Out2> $chain<$R2, $Out2>(Then<$Out2, $R2> t); -} - -class _DiscoveryInfoCopyWithImpl<$R, $Out> - extends ClassCopyWithBase<$R, DiscoveryInfo, $Out> - implements DiscoveryInfoCopyWith<$R, DiscoveryInfo, $Out> { - _DiscoveryInfoCopyWithImpl(super.value, super.then, super.then2); - - @override - late final ClassMapperBase $mapper = - DiscoveryInfoMapper.ensureInitialized(); - @override - $R call( - {String? id, - String? name, - String? address, - Object? endPointAddress = $none}) => - $apply(FieldCopyWithData({ - if (id != null) #id: id, - if (name != null) #name: name, - if (address != null) #address: address, - if (endPointAddress != $none) #endPointAddress: endPointAddress - })); - @override - DiscoveryInfo $make(CopyWithData data) => DiscoveryInfo( - id: data.get(#id, or: $value.id), - name: data.get(#name, or: $value.name), - address: data.get(#address, or: $value.address), - endPointAddress: data.get(#endPointAddress, or: $value.endPointAddress)); - - @override - DiscoveryInfoCopyWith<$R2, DiscoveryInfo, $Out2> $chain<$R2, $Out2>( - Then<$Out2, $R2> t) => - _DiscoveryInfoCopyWithImpl($value, $cast, t); } diff --git a/lib/providers/favourites_provider.dart b/lib/providers/favourites_provider.dart index d948396..907bf46 100644 --- a/lib/providers/favourites_provider.dart +++ b/lib/providers/favourites_provider.dart @@ -1,4 +1,6 @@ import 'package:chopper/chopper.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/favourites_model.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -6,7 +8,6 @@ import 'package:fladder/models/view_model.dart'; import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; final favouritesProvider = StateNotifierProvider((ref) { return FavouritesNotifier(ref); @@ -48,7 +49,7 @@ class FavouritesNotifier extends StateNotifier { isFavorite: true, limit: 10, sortOrder: [SortOrder.ascending], - sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname], + sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded], ); final response2 = await api.itemsGet( parentId: viewModel?.id, @@ -57,7 +58,7 @@ class FavouritesNotifier extends StateNotifier { limit: 10, includeItemTypes: [BaseItemKind.photo, BaseItemKind.episode, BaseItemKind.video, BaseItemKind.collectionfolder], sortOrder: [SortOrder.ascending], - sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname], + sortBy: [ItemSortBy.seriessortname, ItemSortBy.sortname, ItemSortBy.datelastcontentadded], ); return [...?response.body?.items, ...?response2.body?.items]; } diff --git a/lib/providers/items/book_details_provider.dart b/lib/providers/items/book_details_provider.dart index 4109879..e3aeda9 100644 --- a/lib/providers/items/book_details_provider.dart +++ b/lib/providers/items/book_details_provider.dart @@ -117,7 +117,7 @@ class BookDetailsProviderNotifier extends StateNotifier { siblingsResponse = await api.itemsGet( parentId: parentModel.id, recursive: true, - sortBy: SortingOptions.name.toSortBy, + sortBy: SortingOptions.sortName.toSortBy, fields: [ ItemFields.genres, ItemFields.parentid, diff --git a/lib/providers/items/series_details_provider.dart b/lib/providers/items/series_details_provider.dart index 3bec5cb..61083b0 100644 --- a/lib/providers/items/series_details_provider.dart +++ b/lib/providers/items/series_details_provider.dart @@ -1,15 +1,15 @@ import 'package:chopper/chopper.dart'; -import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/providers/service_provider.dart'; -import 'package:fladder/providers/sync_provider.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/models/items/episode_model.dart'; import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/series_model.dart'; import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/related_provider.dart'; +import 'package:fladder/providers/service_provider.dart'; +import 'package:fladder/providers/sync_provider.dart'; final seriesDetailsProvider = StateNotifierProvider.autoDispose.family((ref, id) { diff --git a/lib/providers/library_provider.dart b/lib/providers/library_provider.dart deleted file mode 100644 index c32489b..0000000 --- a/lib/providers/library_provider.dart +++ /dev/null @@ -1,174 +0,0 @@ -import 'package:chopper/chopper.dart'; -import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/models/items/photos_model.dart'; -import 'package:fladder/models/library_model.dart'; -import 'package:fladder/models/recommended_model.dart'; -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/api_provider.dart'; -import 'package:fladder/providers/service_provider.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; - -bool _useFolders(ViewModel model) { - switch (model.collectionType) { - case CollectionType.boxsets: - case CollectionType.homevideos: - case CollectionType.folders: - return true; - default: - return false; - } -} - -final libraryProvider = StateNotifierProvider.autoDispose.family((ref, id) { - return LibraryNotifier(ref); -}); - -class LibraryNotifier extends StateNotifier { - LibraryNotifier(this.ref) : super(null); - - final Ref ref; - - late final JellyService api = ref.read(jellyApiProvider); - - set loading(bool value) { - state = state?.copyWith(loading: value); - } - - bool get loading => state?.loading ?? true; - - Future setupLibrary(ViewModel viewModel) async { - state ??= LibraryModel(id: viewModel.id, name: viewModel.name, loading: true, type: BaseItemKind.movie); - } - - Future loadLibrary(ViewModel viewModel) async { - final response = await api.itemsGet( - parentId: viewModel.id, - sortBy: [ItemSortBy.sortname, ItemSortBy.productionyear], - isMissing: false, - excludeItemTypes: !_useFolders(viewModel) ? [BaseItemKind.folder] : [], - fields: [ItemFields.genres, ItemFields.childcount, ItemFields.parentid], - ); - state = state?.copyWith(posters: response.body?.items); - loading = false; - return response; - } - - Future loadRecommendations(ViewModel viewModel) async { - loading = true; - //Clear recommendations because of all the copying - state = state?.copyWith(recommendations: []); - final latest = await api.usersUserIdItemsLatestGet( - parentId: viewModel.id, - limit: 14, - isPlayed: false, - imageTypeLimit: 1, - includeItemTypes: viewModel.collectionType == CollectionType.tvshows ? [BaseItemKind.episode] : null, - ); - state = state?.copyWith( - recommendations: [ - ...?state?.recommendations, - RecommendedModel( - name: "Latest", - posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [], - type: "Latest", - ), - ], - ); - if (viewModel.collectionType == CollectionType.movies) { - final response = await api.moviesRecommendationsGet( - parentId: viewModel.id, - categoryLimit: 6, - itemLimit: 8, - fields: [ItemFields.mediasourcecount], - ); - state = state?.copyWith(recommendations: [ - ...?state?.recommendations, - ...response.body?.map( - (e) => RecommendedModel( - name: e.baselineItemName ?? "", - posters: e.items?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [], - type: e.recommendationType.toString(), - ), - ) ?? - [], - ]); - loading = false; - } else { - final nextUp = await api.showsNextUpGet( - parentId: viewModel.id, - limit: 14, - imageTypeLimit: 1, - fields: [ItemFields.mediasourcecount, ItemFields.primaryimageaspectratio], - ); - state = state?.copyWith(recommendations: [ - ...?state?.recommendations, - ...[ - RecommendedModel( - name: "Next up", - posters: nextUp.body?.items - ?.map( - (e) => ItemBaseModel.fromBaseDto( - e, - ref, - ), - ) - .toList() ?? - [], - type: "Latest series") - ], - ]); - loading = false; - } - } - - Future loadFavourites(ViewModel viewModel) async { - loading = true; - final response = await api.itemsGet( - parentId: viewModel.id, - isFavorite: true, - recursive: true, - ); - - state = state?.copyWith(favourites: response.body?.items); - loading = false; - return response; - } - - Future loadTimeline(ViewModel viewModel) async { - loading = true; - final response = await api.itemsGet( - parentId: viewModel.id, - recursive: true, - fields: [ItemFields.primaryimageaspectratio, ItemFields.datecreated], - sortBy: [ItemSortBy.datecreated], - sortOrder: [SortOrder.descending], - includeItemTypes: [ - BaseItemKind.photo, - BaseItemKind.video, - ], - ); - state = state?.copyWith( - timelinePhotos: response.body?.items.map((e) => e as PhotoModel).toList(), - ); - loading = false; - return response; - } - - Future loadGenres(ViewModel viewModel) async { - final genres = await api.genresGet( - sortBy: [ItemSortBy.sortname], - sortOrder: [SortOrder.ascending], - includeItemTypes: viewModel.collectionType == CollectionType.movies - ? [BaseItemKind.movie] - : [ - BaseItemKind.series, - ], - parentId: viewModel.id, - ); - state = state?.copyWith( - genres: genres.body?.items?.where((element) => element.name?.isNotEmpty ?? false).map((e) => e.name!).toList()); - return null; - } -} diff --git a/lib/providers/library_screen_provider.dart b/lib/providers/library_screen_provider.dart new file mode 100644 index 0000000..e6a0329 --- /dev/null +++ b/lib/providers/library_screen_provider.dart @@ -0,0 +1,219 @@ +import 'package:flutter/material.dart'; + +import 'package:chopper/chopper.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; +import 'package:fladder/models/collection_types.dart'; +import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/items/item_shared_models.dart'; +import 'package:fladder/models/recommended_model.dart'; +import 'package:fladder/models/view_model.dart'; +import 'package:fladder/providers/api_provider.dart'; +import 'package:fladder/providers/service_provider.dart'; +import 'package:fladder/providers/views_provider.dart'; +import 'package:fladder/util/localization_helper.dart'; + +part 'library_screen_provider.freezed.dart'; +part 'library_screen_provider.g.dart'; + +enum LibraryViewType { + recommended, + favourites, + genres; + + const LibraryViewType(); + + String label(BuildContext context) => switch (this) { + LibraryViewType.recommended => context.localized.recommended, + LibraryViewType.favourites => context.localized.favorites, + LibraryViewType.genres => context.localized.genre(2), + }; + + IconData get icon => switch (this) { + LibraryViewType.recommended => IconsaxPlusLinear.star, + LibraryViewType.favourites => IconsaxPlusLinear.heart, + LibraryViewType.genres => IconsaxPlusLinear.hierarchy_3, + }; + + IconData get iconSelected => switch (this) { + LibraryViewType.recommended => IconsaxPlusBold.star, + LibraryViewType.favourites => IconsaxPlusBold.heart, + LibraryViewType.genres => IconsaxPlusBold.hierarchy_3, + }; +} + +@Freezed(fromJson: false, toJson: false, copyWith: true) +class LibraryScreenModel with _$LibraryScreenModel { + factory LibraryScreenModel({ + @Default([]) List views, + ViewModel? selectedViewModel, + @Default({LibraryViewType.recommended, LibraryViewType.favourites}) Set viewType, + @Default([]) List recommendations, + @Default([]) List genres, + @Default([]) List favourites, + }) = _LibraryScreenModel; +} + +@Riverpod(keepAlive: true) +class LibraryScreen extends _$LibraryScreen { + late final JellyService api = ref.read(jellyApiProvider); + + @override + LibraryScreenModel build() => LibraryScreenModel(); + + Future fetchAllLibraries() async { + final views = await ref.read(viewsProvider.notifier).fetchViews(); + state = state.copyWith(views: views?.views ?? []); + if (state.views.isEmpty) return; + final viewModel = state.selectedViewModel ?? state.views.firstOrNull; + if (viewModel == null) return; + selectLibrary(viewModel); + await loadLibrary(viewModel); + } + + Future selectLibrary(ViewModel viewModel) async { + state = state.copyWith(selectedViewModel: viewModel); + } + + Future setViewType(Set type) async { + state = state.copyWith(viewType: type); + } + + Future loadLibrary(ViewModel viewModel) async { + await loadRecommendations(viewModel); + await loadGenres(viewModel); + await loadFavourites(viewModel); + return null; + } + + Future loadRecommendations(ViewModel viewModel) async { + List newRecommendations = []; + final latest = await api.usersUserIdItemsLatestGet( + parentId: viewModel.id, + limit: 14, + isPlayed: false, + imageTypeLimit: 1, + includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(), + ); + newRecommendations = [ + ...newRecommendations, + RecommendedModel( + name: const Latest(), + posters: latest.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList() ?? [], + type: null, + ), + ]; + if (viewModel.collectionType == CollectionType.movies) { + final response = await api.moviesRecommendationsGet( + parentId: viewModel.id, + categoryLimit: 6, + itemLimit: 14, + fields: [ItemFields.mediasourcecount], + ); + newRecommendations = [ + ...newRecommendations, + ...(response.body?.map( + (e) => RecommendedModel.fromBaseDto(e, ref), + ) ?? + []) + ]; + } else { + final nextUp = await api.showsNextUpGet( + parentId: viewModel.id, + limit: 14, + imageTypeLimit: 1, + fields: [ItemFields.mediasourcecount, ItemFields.primaryimageaspectratio], + ); + newRecommendations = [ + ...newRecommendations, + RecommendedModel( + name: const NextUp(), + posters: nextUp.body?.items + ?.map( + (e) => ItemBaseModel.fromBaseDto( + e, + ref, + ), + ) + .toList() ?? + [], + type: null, + ) + ]; + } + + state = state.copyWith( + recommendations: newRecommendations, + ); + } + + Future loadFavourites(ViewModel viewModel) async { + final response = await api.itemsGet( + parentId: viewModel.id, + isFavorite: true, + recursive: true, + includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(), + enableImageTypes: [ImageType.primary], + fields: [ + ItemFields.primaryimageaspectratio, + ItemFields.mediasourcecount, + ], + enableTotalRecordCount: false, + ); + + state = state.copyWith(favourites: response.body?.items ?? []); + return response; + } + + Future loadGenres(ViewModel viewModel) async { + final genres = await api.genresGet( + sortBy: [ItemSortBy.sortname], + sortOrder: [SortOrder.ascending], + includeItemTypes: + viewModel.collectionType == CollectionType.movies ? [BaseItemKind.movie] : [BaseItemKind.series], + parentId: viewModel.id, + ); + + final filteredGenres = (genres.body?.items?.map( + (item) => GenreItems(id: item.id ?? "", name: item.name ?? ""), + ) ?? + []) + .toList(); + + if (filteredGenres.isEmpty) return null; + + final results = await Future.wait(filteredGenres.map((genre) async { + final response = await api.itemsGet( + parentId: viewModel.id, + genreIds: [genre.id], + limit: 9, + recursive: true, + includeItemTypes: viewModel.collectionType.itemKinds.map((e) => e.dtoKind).toList(), + enableImageTypes: [ImageType.primary], + fields: [ + ItemFields.primaryimageaspectratio, + ItemFields.mediasourcecount, + ], + sortBy: [ItemSortBy.random], + enableTotalRecordCount: false, + imageTypeLimit: 1, + sortOrder: [SortOrder.ascending], + ); + + final items = response.body?.items; + if (items != null && items.isNotEmpty) { + return RecommendedModel(name: Other(genre.name), posters: items); + } + return null; + })); + + state = state.copyWith( + genres: results.whereType().toList(), + ); + + return null; + } +} diff --git a/lib/providers/library_screen_provider.freezed.dart b/lib/providers/library_screen_provider.freezed.dart new file mode 100644 index 0000000..6fdf317 --- /dev/null +++ b/lib/providers/library_screen_provider.freezed.dart @@ -0,0 +1,275 @@ +// 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 'library_screen_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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 _$LibraryScreenModel { + List get views => throw _privateConstructorUsedError; + ViewModel? get selectedViewModel => throw _privateConstructorUsedError; + Set get viewType => throw _privateConstructorUsedError; + List get recommendations => + throw _privateConstructorUsedError; + List get genres => throw _privateConstructorUsedError; + List get favourites => throw _privateConstructorUsedError; + + /// Create a copy of LibraryScreenModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LibraryScreenModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LibraryScreenModelCopyWith<$Res> { + factory $LibraryScreenModelCopyWith( + LibraryScreenModel value, $Res Function(LibraryScreenModel) then) = + _$LibraryScreenModelCopyWithImpl<$Res, LibraryScreenModel>; + @useResult + $Res call( + {List views, + ViewModel? selectedViewModel, + Set viewType, + List recommendations, + List genres, + List favourites}); +} + +/// @nodoc +class _$LibraryScreenModelCopyWithImpl<$Res, $Val extends LibraryScreenModel> + implements $LibraryScreenModelCopyWith<$Res> { + _$LibraryScreenModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LibraryScreenModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? views = null, + Object? selectedViewModel = freezed, + Object? viewType = null, + Object? recommendations = null, + Object? genres = null, + Object? favourites = null, + }) { + return _then(_value.copyWith( + views: null == views + ? _value.views + : views // ignore: cast_nullable_to_non_nullable + as List, + selectedViewModel: freezed == selectedViewModel + ? _value.selectedViewModel + : selectedViewModel // ignore: cast_nullable_to_non_nullable + as ViewModel?, + viewType: null == viewType + ? _value.viewType + : viewType // ignore: cast_nullable_to_non_nullable + as Set, + recommendations: null == recommendations + ? _value.recommendations + : recommendations // ignore: cast_nullable_to_non_nullable + as List, + genres: null == genres + ? _value.genres + : genres // ignore: cast_nullable_to_non_nullable + as List, + favourites: null == favourites + ? _value.favourites + : favourites // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LibraryScreenModelImplCopyWith<$Res> + implements $LibraryScreenModelCopyWith<$Res> { + factory _$$LibraryScreenModelImplCopyWith(_$LibraryScreenModelImpl value, + $Res Function(_$LibraryScreenModelImpl) then) = + __$$LibraryScreenModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {List views, + ViewModel? selectedViewModel, + Set viewType, + List recommendations, + List genres, + List favourites}); +} + +/// @nodoc +class __$$LibraryScreenModelImplCopyWithImpl<$Res> + extends _$LibraryScreenModelCopyWithImpl<$Res, _$LibraryScreenModelImpl> + implements _$$LibraryScreenModelImplCopyWith<$Res> { + __$$LibraryScreenModelImplCopyWithImpl(_$LibraryScreenModelImpl _value, + $Res Function(_$LibraryScreenModelImpl) _then) + : super(_value, _then); + + /// Create a copy of LibraryScreenModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? views = null, + Object? selectedViewModel = freezed, + Object? viewType = null, + Object? recommendations = null, + Object? genres = null, + Object? favourites = null, + }) { + return _then(_$LibraryScreenModelImpl( + views: null == views + ? _value._views + : views // ignore: cast_nullable_to_non_nullable + as List, + selectedViewModel: freezed == selectedViewModel + ? _value.selectedViewModel + : selectedViewModel // ignore: cast_nullable_to_non_nullable + as ViewModel?, + viewType: null == viewType + ? _value._viewType + : viewType // ignore: cast_nullable_to_non_nullable + as Set, + recommendations: null == recommendations + ? _value._recommendations + : recommendations // ignore: cast_nullable_to_non_nullable + as List, + genres: null == genres + ? _value._genres + : genres // ignore: cast_nullable_to_non_nullable + as List, + favourites: null == favourites + ? _value._favourites + : favourites // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$LibraryScreenModelImpl implements _LibraryScreenModel { + _$LibraryScreenModelImpl( + {final List views = const [], + this.selectedViewModel, + final Set viewType = const { + LibraryViewType.recommended, + LibraryViewType.favourites + }, + final List recommendations = const [], + final List genres = const [], + final List favourites = const []}) + : _views = views, + _viewType = viewType, + _recommendations = recommendations, + _genres = genres, + _favourites = favourites; + + final List _views; + @override + @JsonKey() + List get views { + if (_views is EqualUnmodifiableListView) return _views; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_views); + } + + @override + final ViewModel? selectedViewModel; + final Set _viewType; + @override + @JsonKey() + Set get viewType { + if (_viewType is EqualUnmodifiableSetView) return _viewType; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_viewType); + } + + final List _recommendations; + @override + @JsonKey() + List get recommendations { + if (_recommendations is EqualUnmodifiableListView) return _recommendations; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_recommendations); + } + + final List _genres; + @override + @JsonKey() + List get genres { + if (_genres is EqualUnmodifiableListView) return _genres; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_genres); + } + + final List _favourites; + @override + @JsonKey() + List get favourites { + if (_favourites is EqualUnmodifiableListView) return _favourites; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_favourites); + } + + @override + String toString() { + return 'LibraryScreenModel(views: $views, selectedViewModel: $selectedViewModel, viewType: $viewType, recommendations: $recommendations, genres: $genres, favourites: $favourites)'; + } + + /// Create a copy of LibraryScreenModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LibraryScreenModelImplCopyWith<_$LibraryScreenModelImpl> get copyWith => + __$$LibraryScreenModelImplCopyWithImpl<_$LibraryScreenModelImpl>( + this, _$identity); +} + +abstract class _LibraryScreenModel implements LibraryScreenModel { + factory _LibraryScreenModel( + {final List views, + final ViewModel? selectedViewModel, + final Set viewType, + final List recommendations, + final List genres, + final List favourites}) = _$LibraryScreenModelImpl; + + @override + List get views; + @override + ViewModel? get selectedViewModel; + @override + Set get viewType; + @override + List get recommendations; + @override + List get genres; + @override + List get favourites; + + /// Create a copy of LibraryScreenModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LibraryScreenModelImplCopyWith<_$LibraryScreenModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/providers/library_screen_provider.g.dart b/lib/providers/library_screen_provider.g.dart new file mode 100644 index 0000000..7017b4e --- /dev/null +++ b/lib/providers/library_screen_provider.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'library_screen_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$libraryScreenHash() => r'ff8b8514461c3e5da1aaf0933d6d49b014c3c05c'; + +/// See also [LibraryScreen]. +@ProviderFor(LibraryScreen) +final libraryScreenProvider = + NotifierProvider.internal( + LibraryScreen.new, + name: r'libraryScreenProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$libraryScreenHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$LibraryScreen = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/providers/library_search_provider.dart b/lib/providers/library_search_provider.dart index 934c5e9..3ec2ba3 100644 --- a/lib/providers/library_search_provider.dart +++ b/lib/providers/library_search_provider.dart @@ -218,16 +218,16 @@ class LibrarySearchNotifier extends StateNotifier { .toSet() .toList(); var tempState = state.copyWith(); - final genres = mappedList - .expand((element) => element?.genres ?? []) - .nonNulls - .sorted((a, b) => a.name!.toLowerCase().compareTo(b.name!.toLowerCase())); + final genres = (await Future.wait(state.views.included.map((viewModel) => _loadGenres(viewModel)))) + .expand((element) => element) + .toSet() + .toList(); final tags = mappedList .expand((element) => element?.tags ?? []) .sorted((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); tempState = tempState.copyWith( types: state.types.setAll(false).setKeys(enabledCollections, true), - genres: {for (var element in genres) element.name!: false}.replaceMap(tempState.genres), + genres: {for (var element in genres) element.name: false}.replaceMap(tempState.genres), studios: {for (var element in studios) element: false}.replaceMap(tempState.studios), tags: {for (var element in tags) element: false}.replaceMap(tempState.tags), ); @@ -244,6 +244,11 @@ class LibrarySearchNotifier extends StateNotifier { return response.body?.items?.map((e) => Studio(id: e.id ?? "", name: e.name ?? "")).toList() ?? []; } + Future> _loadGenres(ViewModel viewModel) async { + final response = await api.genresGet(parentId: viewModel.id); + return response.body?.items?.map((e) => GenreItems(id: e.id ?? "", name: e.name ?? "")).toList() ?? []; + } + Future _loadLibrary( {ViewModel? viewModel, bool? recursive, @@ -574,8 +579,11 @@ class LibrarySearchNotifier extends StateNotifier { state = state.copyWith(fetchingItems: false); + //Only try to load video items + itemsToPlay = itemsToPlay.where((element) => FladderItemType.playable.contains(element.type)).toList(); + if (itemsToPlay.isNotEmpty) { - await itemsToPlay.playLibraryItems(context, ref); + await itemsToPlay.playLibraryItems(context, ref, shuffle: shuffle); } else { fladderSnackbar(context, title: context.localized.libraryFetchNoItemsFound); } @@ -648,11 +656,12 @@ class LibrarySearchNotifier extends StateNotifier { if (allItems.isNotEmpty) { if (state.fetchingItems == true) { state = state.copyWith(fetchingItems: false); + final newItemList = shuffle ? allItems.shuffled() : allItems; await Navigator.of(context, rootNavigator: true).push( PageTransition( child: PhotoViewerScreen( - items: allItems, - indexOfSelected: selected != null ? allItems.indexOf(selected) : 0, + items: newItemList, + indexOfSelected: selected != null ? newItemList.indexOf(selected) : 0, ), type: PageTransitionType.fade), ); @@ -700,10 +709,14 @@ class LibrarySearchNotifier extends StateNotifier { ref.read(filterProvider.notifier).saveFilter(LibraryFiltersModel.fromLibrarySearch(newName, state)); void updateFilter(LibraryFiltersModel model) { - ref.read(filterProvider.notifier).saveFilter(LibraryFiltersModel.fromLibrarySearch(model.name, state).copyWith( - isFavourite: model.isFavourite, - id: model.id, - )); + ref.read(filterProvider.notifier).saveFilter( + LibraryFiltersModel.fromLibrarySearch( + model.name, + state, + isFavourite: model.isFavourite, + id: model.id, + ), + ); } } diff --git a/lib/providers/service_provider.dart b/lib/providers/service_provider.dart index c395d9c..60636da 100644 --- a/lib/providers/service_provider.dart +++ b/lib/providers/service_provider.dart @@ -902,4 +902,38 @@ class JellyService { Future> quickConnectEnabled() async => api.quickConnectEnabledGet(); Future> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId); + +Future _updateUserConfiguration(UserConfiguration newUserConfiguration) async { + if (account?.id == null) return null; + + final response = await api.usersConfigurationPost( + userId: account!.id, + body: newUserConfiguration, + ); + + if (response.isSuccessful) { + return newUserConfiguration; + } + return null; +} + +Future updateRememberAudioSelections() { + final currentUserConfiguration = account?.userConfiguration; + if (currentUserConfiguration == null) return Future.value(null); + + final updated = currentUserConfiguration.copyWith( + rememberAudioSelections: !(currentUserConfiguration.rememberAudioSelections ?? false), + ); + return _updateUserConfiguration(updated); +} + +Future updateRememberSubtitleSelections() { + final current = account?.userConfiguration; + if (current == null) return Future.value(null); + + final updated = current.copyWith( + rememberSubtitleSelections: !(current.rememberSubtitleSelections ?? false), + ); + return _updateUserConfiguration(updated); +} } diff --git a/lib/providers/session_info_provider.freezed.dart b/lib/providers/session_info_provider.freezed.dart index 67dada9..1f91c0c 100644 --- a/lib/providers/session_info_provider.freezed.dart +++ b/lib/providers/session_info_provider.freezed.dart @@ -25,92 +25,6 @@ mixin _$SessionInfoModel { /// Serializes this SessionInfoModel to a JSON map. Map toJson() => throw _privateConstructorUsedError; - - /// Create a copy of SessionInfoModel - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SessionInfoModelCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $SessionInfoModelCopyWith<$Res> { - factory $SessionInfoModelCopyWith( - SessionInfoModel value, $Res Function(SessionInfoModel) then) = - _$SessionInfoModelCopyWithImpl<$Res, SessionInfoModel>; - @useResult - $Res call({String? playbackModel, TranscodingInfo? transCodeInfo}); -} - -/// @nodoc -class _$SessionInfoModelCopyWithImpl<$Res, $Val extends SessionInfoModel> - implements $SessionInfoModelCopyWith<$Res> { - _$SessionInfoModelCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of SessionInfoModel - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? playbackModel = freezed, - Object? transCodeInfo = freezed, - }) { - return _then(_value.copyWith( - playbackModel: freezed == playbackModel - ? _value.playbackModel - : playbackModel // ignore: cast_nullable_to_non_nullable - as String?, - transCodeInfo: freezed == transCodeInfo - ? _value.transCodeInfo - : transCodeInfo // ignore: cast_nullable_to_non_nullable - as TranscodingInfo?, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$SessionInfoModelImplCopyWith<$Res> - implements $SessionInfoModelCopyWith<$Res> { - factory _$$SessionInfoModelImplCopyWith(_$SessionInfoModelImpl value, - $Res Function(_$SessionInfoModelImpl) then) = - __$$SessionInfoModelImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({String? playbackModel, TranscodingInfo? transCodeInfo}); -} - -/// @nodoc -class __$$SessionInfoModelImplCopyWithImpl<$Res> - extends _$SessionInfoModelCopyWithImpl<$Res, _$SessionInfoModelImpl> - implements _$$SessionInfoModelImplCopyWith<$Res> { - __$$SessionInfoModelImplCopyWithImpl(_$SessionInfoModelImpl _value, - $Res Function(_$SessionInfoModelImpl) _then) - : super(_value, _then); - - /// Create a copy of SessionInfoModel - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? playbackModel = freezed, - Object? transCodeInfo = freezed, - }) { - return _then(_$SessionInfoModelImpl( - playbackModel: freezed == playbackModel - ? _value.playbackModel - : playbackModel // ignore: cast_nullable_to_non_nullable - as String?, - transCodeInfo: freezed == transCodeInfo - ? _value.transCodeInfo - : transCodeInfo // ignore: cast_nullable_to_non_nullable - as TranscodingInfo?, - )); - } } /// @nodoc @@ -131,30 +45,6 @@ class _$SessionInfoModelImpl extends _SessionInfoModel { return 'SessionInfoModel(playbackModel: $playbackModel, transCodeInfo: $transCodeInfo)'; } - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SessionInfoModelImpl && - (identical(other.playbackModel, playbackModel) || - other.playbackModel == playbackModel) && - (identical(other.transCodeInfo, transCodeInfo) || - other.transCodeInfo == transCodeInfo)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, playbackModel, transCodeInfo); - - /// Create a copy of SessionInfoModel - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SessionInfoModelImplCopyWith<_$SessionInfoModelImpl> get copyWith => - __$$SessionInfoModelImplCopyWithImpl<_$SessionInfoModelImpl>( - this, _$identity); - @override Map toJson() { return _$$SessionInfoModelImplToJson( @@ -176,11 +66,4 @@ abstract class _SessionInfoModel extends SessionInfoModel { String? get playbackModel; @override TranscodingInfo? get transCodeInfo; - - /// Create a copy of SessionInfoModel - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SessionInfoModelImplCopyWith<_$SessionInfoModelImpl> get copyWith => - throw _privateConstructorUsedError; } diff --git a/lib/providers/settings/home_settings_provider.dart b/lib/providers/settings/home_settings_provider.dart index 0d20f50..335c2ce 100644 --- a/lib/providers/settings/home_settings_provider.dart +++ b/lib/providers/settings/home_settings_provider.dart @@ -2,6 +2,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/shared_provider.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; final homeSettingsProvider = StateNotifierProvider((ref) { return HomeSettingsNotifier(ref); diff --git a/lib/providers/sync/background_download_provider.dart b/lib/providers/sync/background_download_provider.dart index fc5fc79..9b30cd6 100644 --- a/lib/providers/sync/background_download_provider.dart +++ b/lib/providers/sync/background_download_provider.dart @@ -1,17 +1,45 @@ import 'package:background_downloader/background_downloader.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; + part 'background_download_provider.g.dart'; @Riverpod(keepAlive: true) -FileDownloader backgroundDownloader(Ref ref) { - return FileDownloader() - ..trackTasks() - ..configureNotification( - running: const TaskNotification('Downloading', 'file: {filename}'), - complete: const TaskNotification('Download finished', 'file: {filename}'), - paused: const TaskNotification('Download paused', 'file: {filename}'), - progressBar: true, +class BackgroundDownloader extends _$BackgroundDownloader { + @override + FileDownloader build() { + final maxDownloads = ref.read(clientSettingsProvider.select((value) => value.maxConcurrentDownloads)); + return FileDownloader() + ..configure( + globalConfig: globalConfig(maxDownloads), + ) + ..trackTasks() + ..configureNotification( + running: const TaskNotification('Downloading', 'file: {filename}'), + complete: const TaskNotification('Download finished', 'file: {filename}'), + paused: const TaskNotification('Download paused', 'file: {filename}'), + progressBar: true, + ); + } + + void setMaxConcurrent(int value) { + state.configure( + globalConfig: globalConfig(value), ); + } + + (String, dynamic) globalConfig(int value) => value == 0 + ? ("", "") + : ( + Config.holdingQueue, + ( + //maxConcurrent + value, + //maxConcurrentByHost + value, + //maxConcurrentByGroup + value, + ), + ); } diff --git a/lib/providers/sync/background_download_provider.g.dart b/lib/providers/sync/background_download_provider.g.dart index 47ffb02..285aa71 100644 --- a/lib/providers/sync/background_download_provider.g.dart +++ b/lib/providers/sync/background_download_provider.g.dart @@ -7,12 +7,13 @@ part of 'background_download_provider.dart'; // ************************************************************************** String _$backgroundDownloaderHash() => - r'997d9f4ba79dd0d9d30d5f283b36d5280d10dfaa'; + r'dc27f708fc2f1695d37afcb99f8814bc024037af'; -/// See also [backgroundDownloader]. -@ProviderFor(backgroundDownloader) -final backgroundDownloaderProvider = Provider.internal( - backgroundDownloader, +/// See also [BackgroundDownloader]. +@ProviderFor(BackgroundDownloader) +final backgroundDownloaderProvider = + NotifierProvider.internal( + BackgroundDownloader.new, name: r'backgroundDownloaderProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null @@ -21,8 +22,6 @@ final backgroundDownloaderProvider = Provider.internal( allTransitiveDependencies: null, ); -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef BackgroundDownloaderRef = ProviderRef; +typedef _$BackgroundDownloader = Notifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/providers/update_provider.dart b/lib/providers/update_provider.dart new file mode 100644 index 0000000..cc775fa --- /dev/null +++ b/lib/providers/update_provider.dart @@ -0,0 +1,88 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import 'package:collection/collection.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/util/update_checker.dart'; + +part 'update_provider.freezed.dart'; +part 'update_provider.g.dart'; + +final hasNewUpdateProvider = Provider((ref) { + final latestRelease = ref.watch(updateProvider).latestRelease; + final lastViewedVersion = ref.watch(clientSettingsProvider.select((value) => value.lastViewedUpdate)); + + final latestVersion = latestRelease?.version; + + if (latestVersion == null || lastViewedVersion == null) { + return false; + } + + return latestVersion != lastViewedVersion; +}); + +@Riverpod(keepAlive: true) +class Update extends _$Update { + final updateChecker = UpdateChecker(); + + Timer? _timer; + + @override + UpdatesModel build() { + ref.listen( + clientSettingsProvider.select((value) => value.checkForUpdates), (previous, next) => toggleUpdateChecker(next)); + final checkForUpdates = ref.read(clientSettingsProvider.select((value) => value.checkForUpdates)); + + if (!checkForUpdates) { + _timer?.cancel(); + return UpdatesModel(); + } + + ref.onDispose(() { + _timer?.cancel(); + }); + + _timer?.cancel(); + + _timer = Timer.periodic(const Duration(minutes: 30), (timer) { + _fetchLatest(); + }); + + _fetchLatest(); + + return UpdatesModel(); + } + + void toggleUpdateChecker(bool checkForUpdates) { + _timer?.cancel(); + if (checkForUpdates) { + _timer = Timer.periodic(const Duration(minutes: 30), (timer) { + _fetchLatest(); + }); + _fetchLatest(); + } + } + + Future> _fetchLatest() async { + final latest = await updateChecker.fetchRecentReleases(); + state = UpdatesModel( + lastRelease: latest, + ); + return latest; + } +} + +@Freezed(toJson: false, fromJson: false) +class UpdatesModel with _$UpdatesModel { + const UpdatesModel._(); + + factory UpdatesModel({ + @Default([]) List lastRelease, + }) = _UpdatesModel; + + ReleaseInfo? get latestRelease => lastRelease.firstWhereOrNull((value) => value.isNewerThanCurrent); +} diff --git a/lib/providers/update_provider.freezed.dart b/lib/providers/update_provider.freezed.dart new file mode 100644 index 0000000..9b5131a --- /dev/null +++ b/lib/providers/update_provider.freezed.dart @@ -0,0 +1,59 @@ +// 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 'update_provider.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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 _$UpdatesModel { + List get lastRelease => throw _privateConstructorUsedError; +} + +/// @nodoc + +class _$UpdatesModelImpl extends _UpdatesModel with DiagnosticableTreeMixin { + _$UpdatesModelImpl({final List lastRelease = const []}) + : _lastRelease = lastRelease, + super._(); + + final List _lastRelease; + @override + @JsonKey() + List get lastRelease { + if (_lastRelease is EqualUnmodifiableListView) return _lastRelease; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_lastRelease); + } + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'UpdatesModel(lastRelease: $lastRelease)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'UpdatesModel')) + ..add(DiagnosticsProperty('lastRelease', lastRelease)); + } +} + +abstract class _UpdatesModel extends UpdatesModel { + factory _UpdatesModel({final List lastRelease}) = + _$UpdatesModelImpl; + _UpdatesModel._() : super._(); + + @override + List get lastRelease; +} diff --git a/lib/providers/update_provider.g.dart b/lib/providers/update_provider.g.dart new file mode 100644 index 0000000..0dc6849 --- /dev/null +++ b/lib/providers/update_provider.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'update_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$updateHash() => r'e22205cb13e6b43df1296de90e39059f09bb80a8'; + +/// See also [Update]. +@ProviderFor(Update) +final updateProvider = NotifierProvider.internal( + Update.new, + name: r'updateProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$updateHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$Update = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart index 3e7f7d8..a09bd2b 100644 --- a/lib/providers/user_provider.dart +++ b/lib/providers/user_provider.dart @@ -45,6 +45,7 @@ class User extends _$User { name: response.body?.name ?? state?.name ?? "", policy: response.body?.policy, serverConfiguration: systemConfiguration.body, + userConfiguration: response.body?.configuration, quickConnectState: quickConnectStatus.body ?? false, latestItemsExcludes: response.body?.configuration?.latestItemsExcludes ?? [], ); @@ -53,6 +54,20 @@ class User extends _$User { return null; } + void setRememberAudioSelections() async { + final newUserConfiguration = await api.updateRememberAudioSelections(); + if (newUserConfiguration != null) { + userState = state?.copyWith(userConfiguration: newUserConfiguration); + } + } + + void setRememberSubtitleSelections() async { + final newUserConfiguration = await api.updateRememberSubtitleSelections(); + if (newUserConfiguration != null) { + userState = state?.copyWith(userConfiguration: newUserConfiguration); + } + } + Future refreshMetaData( String itemId, { MetadataRefresh? metadataRefreshMode, @@ -163,7 +178,9 @@ class User extends _$User { if (e.id == model.id) { return model; } else { - return e.copyWith(isFavourite: model.isFavourite && model.containsSameIds(e.ids) ? false : e.isFavourite); + return e.copyWith( + isFavourite: model.isFavourite && model.containsSameIds(e.ids) ? false : e.isFavourite, + ); } }, ).toList()); diff --git a/lib/providers/user_provider.g.dart b/lib/providers/user_provider.g.dart index a699761..07e79b3 100644 --- a/lib/providers/user_provider.g.dart +++ b/lib/providers/user_provider.g.dart @@ -24,7 +24,7 @@ final showSyncButtonProviderProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef ShowSyncButtonProviderRef = AutoDisposeProviderRef; -String _$userHash() => r'1ab1579051806f114e3f42873a2e100c14115900'; +String _$userHash() => r'56fca6515c42347fa99dcdcf4f2d8a977335243a'; /// See also [User]. @ProviderFor(User) diff --git a/lib/providers/video_player_provider.dart b/lib/providers/video_player_provider.dart index f5fd9c1..a077035 100644 --- a/lib/providers/video_player_provider.dart +++ b/lib/providers/video_player_provider.dart @@ -37,11 +37,11 @@ class VideoPlayerNotifier extends StateNotifier { } final subscription = state.stateStream?.listen((value) { - mediaState.update((state) => state.copyWith(buffering: value.buffering)); - mediaState.update((state) => state.copyWith(buffer: value.buffer)); + updateBuffering(value.buffering); + updateBuffer(value.buffer); updatePlaying(value.playing); updatePosition(value.position); - mediaState.update((state) => state.copyWith(duration: value.duration)); + updateDuration(value.duration); }); if (subscription != null) { @@ -49,32 +49,58 @@ class VideoPlayerNotifier extends StateNotifier { } } + Future updateBuffering(bool event) async => + mediaState.update((state) => state.buffering == event ? state : state.copyWith(buffering: event)); + + Future updateBuffer(Duration buffer) async { + mediaState.update( + (state) => (state.buffer - buffer).inSeconds.abs() < 1 + ? state + : state.copyWith( + buffer: buffer, + ), + ); + } + + Future updateDuration(Duration duration) async { + mediaState.update((state) { + return (state.duration - duration).inSeconds.abs() < 1 + ? state + : state.copyWith( + duration: duration, + ); + }); + } + Future updatePlaying(bool event) async { if (!state.hasPlayer) return; - mediaState.update((state) => state.copyWith(playing: event)); + mediaState.update( + (state) => state.playing == event ? state : state.copyWith(playing: event), + ); } Future updatePosition(Duration event) async { if (!state.hasPlayer) return; if (playbackState.playing == false) return; + final currentState = playbackState; + final currentPosition = currentState.position; + + if ((currentPosition - event).inSeconds.abs() < 1) return; final position = event; - final lastPosition = ref.read(mediaPlaybackProvider.select((value) => value.lastPosition)); + + final lastPosition = currentState.lastPosition; final diff = (position.inMilliseconds - lastPosition.inMilliseconds).abs(); if (diff > const Duration(seconds: 10).inMilliseconds) { mediaState.update((value) => value.copyWith( position: event, - playing: playbackState.playing, - duration: playbackState.duration, lastPosition: position, )); ref.read(playBackModel)?.updatePlaybackPosition(position, playbackState.playing, ref); } else { mediaState.update((value) => value.copyWith( position: event, - playing: playbackState.playing, - duration: playbackState.duration, )); } } diff --git a/lib/providers/views_provider.dart b/lib/providers/views_provider.dart index 1a0ad02..c04e5a0 100644 --- a/lib/providers/views_provider.dart +++ b/lib/providers/views_provider.dart @@ -32,8 +32,8 @@ class ViewsNotifier extends StateNotifier { late final JellyService api = ref.read(jellyApiProvider); - Future fetchViews() async { - if (state.loading) return; + Future fetchViews() async { + if (state.loading) return null; final showAllCollections = ref.read(clientSettingsProvider.select((value) => value.showAllCollectionTypes)); final response = await api.usersUserIdViewsGet( includeExternalContent: showAllCollections, @@ -64,6 +64,7 @@ class ViewsNotifier extends StateNotifier { ItemFields.mediasources, ItemFields.candelete, ItemFields.candownload, + ItemFields.primaryimageaspectratio, ], ); return e.copyWith(recentlyAdded: recents.body?.map((e) => ItemBaseModel.fromBaseDto(e, ref)).toList()); @@ -76,6 +77,7 @@ class ViewsNotifier extends StateNotifier { .where((element) => !(ref.read(userProvider)?.latestItemsExcludes.contains(element.id) ?? true)) .toList(), loading: false); + return state; } void clear() { diff --git a/lib/routes/auto_router.dart b/lib/routes/auto_router.dart index 2f44c78..a3f885c 100644 --- a/lib/routes/auto_router.dart +++ b/lib/routes/auto_router.dart @@ -21,7 +21,14 @@ class AutoRouter extends RootStackRouter { List get guards => [...super.guards, AuthGuard(ref: ref)]; @override - RouteType get defaultRouteType => kIsWeb ? const RouteType.material() : const RouteType.adaptive(); + RouteType get defaultRouteType => kIsWeb || + { + TargetPlatform.windows, + TargetPlatform.linux, + TargetPlatform.macOS, + }.contains(defaultTargetPlatform) + ? const RouteType.cupertino() + : const RouteType.adaptive(); @override List get routes => [ @@ -51,6 +58,7 @@ final List homeRoutes = [ _dashboardRoute, _favouritesRoute, _syncedRoute, + _librariesRoute, ]; final List _defaultRoutes = [ @@ -63,22 +71,25 @@ final AutoRoute _dashboardRoute = CustomRoute( page: DashboardRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, initial: true, - maintainState: false, path: 'dashboard', ); final AutoRoute _favouritesRoute = CustomRoute( page: FavouritesRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, - maintainState: false, path: 'favourites', ); final AutoRoute _syncedRoute = CustomRoute( page: SyncedRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, - maintainState: false, path: 'synced', ); +final AutoRoute _librariesRoute = CustomRoute( + page: LibraryRoute.page, + transitionsBuilder: TransitionsBuilders.fadeIn, + path: 'libraries', +); + final List _settingsChildren = [ CustomRoute(page: SettingsSelectionRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'list'), CustomRoute(page: ClientSettingsRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, path: 'client'), diff --git a/lib/routes/auto_router.gr.dart b/lib/routes/auto_router.gr.dart index 3b79be3..5ebe950 100644 --- a/lib/routes/auto_router.gr.dart +++ b/lib/routes/auto_router.gr.dart @@ -8,35 +8,36 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i16; -import 'package:fladder/models/item_base_model.dart' as _i17; -import 'package:fladder/models/items/photos_model.dart' as _i20; +import 'package:auto_route/auto_route.dart' as _i17; +import 'package:fladder/models/item_base_model.dart' as _i18; +import 'package:fladder/models/items/photos_model.dart' as _i21; import 'package:fladder/models/library_search/library_search_options.dart' - as _i19; + as _i20; import 'package:fladder/routes/nested_details_screen.dart' as _i4; import 'package:fladder/screens/dashboard/dashboard_screen.dart' as _i3; import 'package:fladder/screens/favourites/favourites_screen.dart' as _i5; import 'package:fladder/screens/home_screen.dart' as _i6; +import 'package:fladder/screens/library/library_screen.dart' as _i7; import 'package:fladder/screens/library_search/library_search_screen.dart' - as _i7; -import 'package:fladder/screens/login/lock_screen.dart' as _i8; -import 'package:fladder/screens/login/login_screen.dart' as _i9; + as _i8; +import 'package:fladder/screens/login/lock_screen.dart' as _i9; +import 'package:fladder/screens/login/login_screen.dart' as _i10; import 'package:fladder/screens/settings/about_settings_page.dart' as _i1; import 'package:fladder/screens/settings/client_settings_page.dart' as _i2; -import 'package:fladder/screens/settings/player_settings_page.dart' as _i10; -import 'package:fladder/screens/settings/security_settings_page.dart' as _i11; -import 'package:fladder/screens/settings/settings_screen.dart' as _i12; +import 'package:fladder/screens/settings/player_settings_page.dart' as _i11; +import 'package:fladder/screens/settings/security_settings_page.dart' as _i12; +import 'package:fladder/screens/settings/settings_screen.dart' as _i13; import 'package:fladder/screens/settings/settings_selection_screen.dart' - as _i13; -import 'package:fladder/screens/splash_screen.dart' as _i14; -import 'package:fladder/screens/syncing/synced_screen.dart' as _i15; -import 'package:flutter/foundation.dart' as _i18; -import 'package:flutter/material.dart' as _i21; + as _i14; +import 'package:fladder/screens/splash_screen.dart' as _i15; +import 'package:fladder/screens/syncing/synced_screen.dart' as _i16; +import 'package:flutter/foundation.dart' as _i19; +import 'package:flutter/material.dart' as _i22; /// generated route for /// [_i1.AboutSettingsPage] -class AboutSettingsRoute extends _i16.PageRouteInfo { - const AboutSettingsRoute({List<_i16.PageRouteInfo>? children}) +class AboutSettingsRoute extends _i17.PageRouteInfo { + const AboutSettingsRoute({List<_i17.PageRouteInfo>? children}) : super( AboutSettingsRoute.name, initialChildren: children, @@ -44,7 +45,7 @@ class AboutSettingsRoute extends _i16.PageRouteInfo { static const String name = 'AboutSettingsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { return const _i1.AboutSettingsPage(); @@ -54,8 +55,8 @@ class AboutSettingsRoute extends _i16.PageRouteInfo { /// generated route for /// [_i2.ClientSettingsPage] -class ClientSettingsRoute extends _i16.PageRouteInfo { - const ClientSettingsRoute({List<_i16.PageRouteInfo>? children}) +class ClientSettingsRoute extends _i17.PageRouteInfo { + const ClientSettingsRoute({List<_i17.PageRouteInfo>? children}) : super( ClientSettingsRoute.name, initialChildren: children, @@ -63,7 +64,7 @@ class ClientSettingsRoute extends _i16.PageRouteInfo { static const String name = 'ClientSettingsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { return const _i2.ClientSettingsPage(); @@ -73,8 +74,8 @@ class ClientSettingsRoute extends _i16.PageRouteInfo { /// generated route for /// [_i3.DashboardScreen] -class DashboardRoute extends _i16.PageRouteInfo { - const DashboardRoute({List<_i16.PageRouteInfo>? children}) +class DashboardRoute extends _i17.PageRouteInfo { + const DashboardRoute({List<_i17.PageRouteInfo>? children}) : super( DashboardRoute.name, initialChildren: children, @@ -82,7 +83,7 @@ class DashboardRoute extends _i16.PageRouteInfo { static const String name = 'DashboardRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { return const _i3.DashboardScreen(); @@ -92,12 +93,12 @@ class DashboardRoute extends _i16.PageRouteInfo { /// generated route for /// [_i4.DetailsScreen] -class DetailsRoute extends _i16.PageRouteInfo { +class DetailsRoute extends _i17.PageRouteInfo { DetailsRoute({ String id = '', - _i17.ItemBaseModel? item, - _i18.Key? key, - List<_i16.PageRouteInfo>? children, + _i18.ItemBaseModel? item, + _i19.Key? key, + List<_i17.PageRouteInfo>? children, }) : super( DetailsRoute.name, args: DetailsRouteArgs( @@ -111,7 +112,7 @@ class DetailsRoute extends _i16.PageRouteInfo { static const String name = 'DetailsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final queryParams = data.queryParams; @@ -139,9 +140,9 @@ class DetailsRouteArgs { final String id; - final _i17.ItemBaseModel? item; + final _i18.ItemBaseModel? item; - final _i18.Key? key; + final _i19.Key? key; @override String toString() { @@ -151,8 +152,8 @@ class DetailsRouteArgs { /// generated route for /// [_i5.FavouritesScreen] -class FavouritesRoute extends _i16.PageRouteInfo { - const FavouritesRoute({List<_i16.PageRouteInfo>? children}) +class FavouritesRoute extends _i17.PageRouteInfo { + const FavouritesRoute({List<_i17.PageRouteInfo>? children}) : super( FavouritesRoute.name, initialChildren: children, @@ -160,7 +161,7 @@ class FavouritesRoute extends _i16.PageRouteInfo { static const String name = 'FavouritesRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { return const _i5.FavouritesScreen(); @@ -170,8 +171,8 @@ class FavouritesRoute extends _i16.PageRouteInfo { /// generated route for /// [_i6.HomeScreen] -class HomeRoute extends _i16.PageRouteInfo { - const HomeRoute({List<_i16.PageRouteInfo>? children}) +class HomeRoute extends _i17.PageRouteInfo { + const HomeRoute({List<_i17.PageRouteInfo>? children}) : super( HomeRoute.name, initialChildren: children, @@ -179,7 +180,7 @@ class HomeRoute extends _i16.PageRouteInfo { static const String name = 'HomeRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { return const _i6.HomeScreen(); @@ -188,17 +189,36 @@ class HomeRoute extends _i16.PageRouteInfo { } /// generated route for -/// [_i7.LibrarySearchScreen] -class LibrarySearchRoute extends _i16.PageRouteInfo { +/// [_i7.LibraryScreen] +class LibraryRoute extends _i17.PageRouteInfo { + const LibraryRoute({List<_i17.PageRouteInfo>? children}) + : super( + LibraryRoute.name, + initialChildren: children, + ); + + static const String name = 'LibraryRoute'; + + static _i17.PageInfo page = _i17.PageInfo( + name, + builder: (data) { + return const _i7.LibraryScreen(); + }, + ); +} + +/// generated route for +/// [_i8.LibrarySearchScreen] +class LibrarySearchRoute extends _i17.PageRouteInfo { LibrarySearchRoute({ String? viewModelId, List? folderId, bool? favourites, - _i19.SortingOrder? sortOrder, - _i19.SortingOptions? sortingOptions, - _i20.PhotoModel? photoToView, - _i18.Key? key, - List<_i16.PageRouteInfo>? children, + _i20.SortingOrder? sortOrder, + _i20.SortingOptions? sortingOptions, + _i21.PhotoModel? photoToView, + _i19.Key? key, + List<_i17.PageRouteInfo>? children, }) : super( LibrarySearchRoute.name, args: LibrarySearchRouteArgs( @@ -222,7 +242,7 @@ class LibrarySearchRoute extends _i16.PageRouteInfo { static const String name = 'LibrarySearchRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final queryParams = data.queryParams; @@ -234,7 +254,7 @@ class LibrarySearchRoute extends _i16.PageRouteInfo { sortOrder: queryParams.get('sortOrder'), sortingOptions: queryParams.get('sortOptions'), )); - return _i7.LibrarySearchScreen( + return _i8.LibrarySearchScreen( viewModelId: args.viewModelId, folderId: args.folderId, favourites: args.favourites, @@ -264,13 +284,13 @@ class LibrarySearchRouteArgs { final bool? favourites; - final _i19.SortingOrder? sortOrder; + final _i20.SortingOrder? sortOrder; - final _i19.SortingOptions? sortingOptions; + final _i20.SortingOptions? sortingOptions; - final _i20.PhotoModel? photoToView; + final _i21.PhotoModel? photoToView; - final _i18.Key? key; + final _i19.Key? key; @override String toString() { @@ -279,9 +299,9 @@ class LibrarySearchRouteArgs { } /// generated route for -/// [_i8.LockScreen] -class LockRoute extends _i16.PageRouteInfo { - const LockRoute({List<_i16.PageRouteInfo>? children}) +/// [_i9.LockScreen] +class LockRoute extends _i17.PageRouteInfo { + const LockRoute({List<_i17.PageRouteInfo>? children}) : super( LockRoute.name, initialChildren: children, @@ -289,18 +309,18 @@ class LockRoute extends _i16.PageRouteInfo { static const String name = 'LockRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i8.LockScreen(); + return const _i9.LockScreen(); }, ); } /// generated route for -/// [_i9.LoginScreen] -class LoginRoute extends _i16.PageRouteInfo { - const LoginRoute({List<_i16.PageRouteInfo>? children}) +/// [_i10.LoginScreen] +class LoginRoute extends _i17.PageRouteInfo { + const LoginRoute({List<_i17.PageRouteInfo>? children}) : super( LoginRoute.name, initialChildren: children, @@ -308,18 +328,18 @@ class LoginRoute extends _i16.PageRouteInfo { static const String name = 'LoginRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i9.LoginScreen(); + return const _i10.LoginScreen(); }, ); } /// generated route for -/// [_i10.PlayerSettingsPage] -class PlayerSettingsRoute extends _i16.PageRouteInfo { - const PlayerSettingsRoute({List<_i16.PageRouteInfo>? children}) +/// [_i11.PlayerSettingsPage] +class PlayerSettingsRoute extends _i17.PageRouteInfo { + const PlayerSettingsRoute({List<_i17.PageRouteInfo>? children}) : super( PlayerSettingsRoute.name, initialChildren: children, @@ -327,18 +347,18 @@ class PlayerSettingsRoute extends _i16.PageRouteInfo { static const String name = 'PlayerSettingsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i10.PlayerSettingsPage(); + return const _i11.PlayerSettingsPage(); }, ); } /// generated route for -/// [_i11.SecuritySettingsPage] -class SecuritySettingsRoute extends _i16.PageRouteInfo { - const SecuritySettingsRoute({List<_i16.PageRouteInfo>? children}) +/// [_i12.SecuritySettingsPage] +class SecuritySettingsRoute extends _i17.PageRouteInfo { + const SecuritySettingsRoute({List<_i17.PageRouteInfo>? children}) : super( SecuritySettingsRoute.name, initialChildren: children, @@ -346,18 +366,18 @@ class SecuritySettingsRoute extends _i16.PageRouteInfo { static const String name = 'SecuritySettingsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i11.SecuritySettingsPage(); + return const _i12.SecuritySettingsPage(); }, ); } /// generated route for -/// [_i12.SettingsScreen] -class SettingsRoute extends _i16.PageRouteInfo { - const SettingsRoute({List<_i16.PageRouteInfo>? children}) +/// [_i13.SettingsScreen] +class SettingsRoute extends _i17.PageRouteInfo { + const SettingsRoute({List<_i17.PageRouteInfo>? children}) : super( SettingsRoute.name, initialChildren: children, @@ -365,18 +385,18 @@ class SettingsRoute extends _i16.PageRouteInfo { static const String name = 'SettingsRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i12.SettingsScreen(); + return const _i13.SettingsScreen(); }, ); } /// generated route for -/// [_i13.SettingsSelectionScreen] -class SettingsSelectionRoute extends _i16.PageRouteInfo { - const SettingsSelectionRoute({List<_i16.PageRouteInfo>? children}) +/// [_i14.SettingsSelectionScreen] +class SettingsSelectionRoute extends _i17.PageRouteInfo { + const SettingsSelectionRoute({List<_i17.PageRouteInfo>? children}) : super( SettingsSelectionRoute.name, initialChildren: children, @@ -384,21 +404,21 @@ class SettingsSelectionRoute extends _i16.PageRouteInfo { static const String name = 'SettingsSelectionRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { - return const _i13.SettingsSelectionScreen(); + return const _i14.SettingsSelectionScreen(); }, ); } /// generated route for -/// [_i14.SplashScreen] -class SplashRoute extends _i16.PageRouteInfo { +/// [_i15.SplashScreen] +class SplashRoute extends _i17.PageRouteInfo { SplashRoute({ dynamic Function(bool)? loggedIn, - _i21.Key? key, - List<_i16.PageRouteInfo>? children, + _i22.Key? key, + List<_i17.PageRouteInfo>? children, }) : super( SplashRoute.name, args: SplashRouteArgs( @@ -410,12 +430,12 @@ class SplashRoute extends _i16.PageRouteInfo { static const String name = 'SplashRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final args = data.argsAs(orElse: () => const SplashRouteArgs()); - return _i14.SplashScreen( + return _i15.SplashScreen( loggedIn: args.loggedIn, key: args.key, ); @@ -431,7 +451,7 @@ class SplashRouteArgs { final dynamic Function(bool)? loggedIn; - final _i21.Key? key; + final _i22.Key? key; @override String toString() { @@ -440,12 +460,12 @@ class SplashRouteArgs { } /// generated route for -/// [_i15.SyncedScreen] -class SyncedRoute extends _i16.PageRouteInfo { +/// [_i16.SyncedScreen] +class SyncedRoute extends _i17.PageRouteInfo { SyncedRoute({ - _i21.ScrollController? navigationScrollController, - _i21.Key? key, - List<_i16.PageRouteInfo>? children, + _i22.ScrollController? navigationScrollController, + _i22.Key? key, + List<_i17.PageRouteInfo>? children, }) : super( SyncedRoute.name, args: SyncedRouteArgs( @@ -457,12 +477,12 @@ class SyncedRoute extends _i16.PageRouteInfo { static const String name = 'SyncedRoute'; - static _i16.PageInfo page = _i16.PageInfo( + static _i17.PageInfo page = _i17.PageInfo( name, builder: (data) { final args = data.argsAs(orElse: () => const SyncedRouteArgs()); - return _i15.SyncedScreen( + return _i16.SyncedScreen( navigationScrollController: args.navigationScrollController, key: args.key, ); @@ -476,9 +496,9 @@ class SyncedRouteArgs { this.key, }); - final _i21.ScrollController? navigationScrollController; + final _i22.ScrollController? navigationScrollController; - final _i21.Key? key; + final _i22.Key? key; @override String toString() { diff --git a/lib/screens/book_viewer/book_viewer_chapters.dart b/lib/screens/book_viewer/book_viewer_chapters.dart index 57c7a83..dede4b3 100644 --- a/lib/screens/book_viewer/book_viewer_chapters.dart +++ b/lib/screens/book_viewer/book_viewer_chapters.dart @@ -1,7 +1,7 @@ import 'package:fladder/models/book_model.dart'; import 'package:fladder/providers/book_viewer_provider.dart'; import 'package:fladder/providers/items/book_details_provider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/widgets/shared/modal_side_sheet.dart'; import 'package:flutter/material.dart'; diff --git a/lib/screens/book_viewer/book_viewer_controls.dart b/lib/screens/book_viewer/book_viewer_controls.dart index b33bb83..205b257 100644 --- a/lib/screens/book_viewer/book_viewer_controls.dart +++ b/lib/screens/book_viewer/book_viewer_controls.dart @@ -15,7 +15,7 @@ import 'package:fladder/screens/book_viewer/book_viewer_chapters.dart'; import 'package:fladder/screens/book_viewer/book_viewer_settings.dart'; import 'package:fladder/screens/shared/default_title_bar.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/input_handler.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/throttler.dart'; diff --git a/lib/screens/book_viewer/book_viewer_settings.dart b/lib/screens/book_viewer/book_viewer_settings.dart index 738b4ec..38b6301 100644 --- a/lib/screens/book_viewer/book_viewer_settings.dart +++ b/lib/screens/book_viewer/book_viewer_settings.dart @@ -1,5 +1,5 @@ import 'package:fladder/providers/settings/book_viewer_settings_provider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/string_extensions.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart'; diff --git a/lib/screens/dashboard/dashboard_screen.dart b/lib/screens/dashboard/dashboard_screen.dart index 02320a5..af0ea5d 100644 --- a/lib/screens/dashboard/dashboard_screen.dart +++ b/lib/screens/dashboard/dashboard_screen.dart @@ -7,6 +7,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; +import 'package:fladder/models/collection_types.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/dashboard_provider.dart'; @@ -19,10 +20,11 @@ import 'package:fladder/screens/dashboard/home_banner_widget.dart'; import 'package:fladder/screens/shared/media/poster_row.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart'; import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sliver_list_padding.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart'; import 'package:fladder/widgets/shared/pinch_poster_zoom.dart'; import 'package:fladder/widgets/shared/poster_size_slider.dart'; import 'package:fladder/widgets/shared/pull_to_refresh.dart'; @@ -65,6 +67,8 @@ class _DashboardScreenState extends ConsumerState { @override Widget build(BuildContext context) { + final padding = AdaptiveLayout.adaptivePadding(context); + final dashboardData = ref.watch(dashboardProvider); final views = ref.watch(viewsProvider); final homeSettings = ref.watch(homeSettingsProvider); @@ -84,6 +88,7 @@ class _DashboardScreenState extends ConsumerState { return MediaQuery.removeViewInsets( context: context, child: NestedScaffold( + background: BackgroundImage(items: [...homeCarouselItems, ...dashboardData.nextUp, ...allResume]), body: PullToRefresh( refreshKey: _refreshIndicatorKey, displacement: 80 + MediaQuery.of(context).viewPadding.top, @@ -104,7 +109,13 @@ class _DashboardScreenState extends ConsumerState { SliverToBoxAdapter( child: Transform.translate( offset: Offset(0, AdaptiveLayout.layoutOf(context) == ViewSize.phone ? -14 : 0), - child: HomeBannerWidget(posters: homeCarouselItems), + child: Padding( + padding: AdaptiveLayout.adaptivePadding( + context, + horizontalPadding: 0, + ), + child: HomeBannerWidget(posters: homeCarouselItems), + ), ), ), }, @@ -122,6 +133,7 @@ class _DashboardScreenState extends ConsumerState { (homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate)) SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.dashboardContinueWatching, posters: resumeVideo, ), @@ -130,6 +142,7 @@ class _DashboardScreenState extends ConsumerState { (homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate)) SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.dashboardContinueListening, posters: resumeAudio, ), @@ -138,6 +151,7 @@ class _DashboardScreenState extends ConsumerState { (homeSettings.nextUp == HomeNextUp.cont || homeSettings.nextUp == HomeNextUp.separate)) SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.dashboardContinueReading, posters: resumeBooks, ), @@ -146,6 +160,7 @@ class _DashboardScreenState extends ConsumerState { (homeSettings.nextUp == HomeNextUp.nextUp || homeSettings.nextUp == HomeNextUp.separate)) SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.nextUp, posters: dashboardData.nextUp, ), @@ -153,6 +168,7 @@ class _DashboardScreenState extends ConsumerState { if ([...allResume, ...dashboardData.nextUp].isNotEmpty && homeSettings.nextUp == HomeNextUp.combined) SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.dashboardContinue, posters: [...allResume, ...dashboardData.nextUp], ), @@ -161,7 +177,9 @@ class _DashboardScreenState extends ConsumerState { .where((element) => element.recentlyAdded.isNotEmpty) .map((view) => SliverToBoxAdapter( child: PosterRow( + contentPadding: padding, label: context.localized.dashboardRecentlyAdded(view.name), + collectionAspectRatio: view.collectionType.aspectRatio, onLabelClick: () => context.router.push(LibrarySearchRoute( viewModelId: view.id, sortingOptions: switch (view.collectionType) { diff --git a/lib/screens/dashboard/home_banner_widget.dart b/lib/screens/dashboard/home_banner_widget.dart index c793db7..db10520 100644 --- a/lib/screens/dashboard/home_banner_widget.dart +++ b/lib/screens/dashboard/home_banner_widget.dart @@ -27,9 +27,12 @@ class HomeBannerWidget extends ConsumerWidget { const SizedBox(height: 24) ], ), - HomeBanner.banner => MediaBanner( - items: posters, - maxHeight: maxHeight, + HomeBanner.banner => Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: MediaBanner( + items: posters, + maxHeight: maxHeight, + ), ), _ => const SizedBox.shrink(), }; diff --git a/lib/screens/details_screens/components/overview_header.dart b/lib/screens/details_screens/components/overview_header.dart index 90959be..7e9a2a1 100644 --- a/lib/screens/details_screens/components/overview_header.dart +++ b/lib/screens/details_screens/components/overview_header.dart @@ -4,11 +4,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/item_shared_models.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/screens/shared/media/components/chip_button.dart'; import 'package:fladder/screens/shared/media/components/media_header.dart'; import 'package:fladder/screens/shared/media/components/small_detail_widgets.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/humanize_duration.dart'; import 'package:fladder/util/list_padding.dart'; diff --git a/lib/screens/details_screens/empty_item.dart b/lib/screens/details_screens/empty_item.dart index 795a198..7bcea6d 100644 --- a/lib/screens/details_screens/empty_item.dart +++ b/lib/screens/details_screens/empty_item.dart @@ -37,36 +37,42 @@ class EmptyItem extends ConsumerWidget { } }, ), - content: (padding) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 350), - child: AspectRatio( - aspectRatio: 0.67, - child: Card( - elevation: 6, - color: Theme.of(context).colorScheme.secondaryContainer, - shape: RoundedRectangleBorder( - side: BorderSide( - width: 1.0, - color: Colors.white.withValues(alpha: 0.10), + content: (padding) => Center( + child: Padding( + padding: padding, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 350), + child: AspectRatio( + aspectRatio: 0.67, + child: Card( + elevation: 6, + color: Theme.of(context).colorScheme.secondaryContainer, + shape: RoundedRectangleBorder( + side: BorderSide( + width: 1.0, + color: Colors.white.withValues(alpha: 0.10), + ), + borderRadius: FladderTheme.defaultShape.borderRadius, + ), + child: FladderImage( + image: item.getPosters?.primary ?? item.getPosters?.backDrop?.lastOrNull, + placeHolder: PosterPlaceholder(item: item), + ), ), - borderRadius: FladderTheme.defaultShape.borderRadius, - ), - child: FladderImage( - image: item.getPosters?.primary ?? item.getPosters?.backDrop?.lastOrNull, - placeHolder: PosterPlaceholder(item: item), ), ), - ), + Text( + item.title, + style: Theme.of(context).textTheme.titleLarge, + ), + Text("Type of (Jelly.${item.jellyType?.name.capitalize()}) has not been implemented yet."), + ].addInBetween(const SizedBox(height: 32)), ), - Text( - item.title, - style: Theme.of(context).textTheme.titleLarge, - ), - Text("Type of (Jelly.${item.jellyType?.name.capitalize()}) has not been implemented yet."), - ].addInBetween(const SizedBox(height: 32)), + ), ), ); } diff --git a/lib/screens/details_screens/episode_detail_screen.dart b/lib/screens/details_screens/episode_detail_screen.dart index 967d4bd..fcaaa5d 100644 --- a/lib/screens/details_screens/episode_detail_screen.dart +++ b/lib/screens/details_screens/episode_detail_screen.dart @@ -5,7 +5,6 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/items/episode_details_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/details_screens/components/media_stream_information.dart'; @@ -18,7 +17,7 @@ import 'package:fladder/screens/shared/media/episode_posters.dart'; import 'package:fladder/screens/shared/media/expanding_overview.dart'; import 'package:fladder/screens/shared/media/external_urls.dart'; import 'package:fladder/screens/shared/media/people_row.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/util/list_padding.dart'; diff --git a/lib/screens/details_screens/movie_detail_screen.dart b/lib/screens/details_screens/movie_detail_screen.dart index 7c64fe9..d76c65f 100644 --- a/lib/screens/details_screens/movie_detail_screen.dart +++ b/lib/screens/details_screens/movie_detail_screen.dart @@ -5,7 +5,6 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/items/movies_details_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/details_screens/components/media_stream_information.dart'; @@ -17,7 +16,7 @@ import 'package:fladder/screens/shared/media/expanding_overview.dart'; import 'package:fladder/screens/shared/media/external_urls.dart'; import 'package:fladder/screens/shared/media/people_row.dart'; import 'package:fladder/screens/shared/media/poster_row.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/util/list_padding.dart'; diff --git a/lib/screens/details_screens/person_detail_screen.dart b/lib/screens/details_screens/person_detail_screen.dart index 13ff8a5..175b54b 100644 --- a/lib/screens/details_screens/person_detail_screen.dart +++ b/lib/screens/details_screens/person_detail_screen.dart @@ -1,4 +1,3 @@ -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; @@ -11,7 +10,7 @@ import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/shared/detail_scaffold.dart'; import 'package:fladder/screens/shared/media/external_urls.dart'; import 'package:fladder/screens/shared/media/poster_row.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/list_extensions.dart'; import 'package:fladder/util/string_extensions.dart'; @@ -53,7 +52,7 @@ class _PersonDetailScreenState extends ConsumerState { spacing: 32, children: [ Container( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, decoration: BoxDecoration( borderRadius: BorderRadius.circular(15), ), diff --git a/lib/screens/details_screens/series_detail_screen.dart b/lib/screens/details_screens/series_detail_screen.dart index 32b91eb..b2a497f 100644 --- a/lib/screens/details_screens/series_detail_screen.dart +++ b/lib/screens/details_screens/series_detail_screen.dart @@ -6,7 +6,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/series_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/items/series_details_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/details_screens/components/overview_header.dart'; @@ -19,7 +18,7 @@ import 'package:fladder/screens/shared/media/external_urls.dart'; import 'package:fladder/screens/shared/media/people_row.dart'; import 'package:fladder/screens/shared/media/poster_row.dart'; import 'package:fladder/screens/shared/media/season_row.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/util/list_padding.dart'; diff --git a/lib/screens/favourites/favourites_screen.dart b/lib/screens/favourites/favourites_screen.dart index fe78f14..afee5db 100644 --- a/lib/screens/favourites/favourites_screen.dart +++ b/lib/screens/favourites/favourites_screen.dart @@ -1,19 +1,20 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/providers/settings/client_settings_provider.dart'; -import 'package:fladder/routes/auto_router.gr.dart'; -import 'package:fladder/screens/shared/nested_scaffold.dart'; -import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:fladder/util/localization_helper.dart'; -import 'package:fladder/widgets/shared/pinch_poster_zoom.dart'; -import 'package:fladder/widgets/shared/poster_size_slider.dart'; import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/providers/favourites_provider.dart'; -import 'package:fladder/screens/shared/media/poster_grid.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/routes/auto_router.gr.dart'; +import 'package:fladder/screens/shared/media/poster_row.dart'; +import 'package:fladder/screens/shared/nested_scaffold.dart'; +import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sliver_list_padding.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart'; +import 'package:fladder/widgets/shared/pinch_poster_zoom.dart'; +import 'package:fladder/widgets/shared/poster_size_slider.dart'; import 'package:fladder/widgets/shared/pull_to_refresh.dart'; @RoutePage() @@ -23,10 +24,12 @@ class FavouritesScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final favourites = ref.watch(favouritesProvider); + final padding = AdaptiveLayout.adaptivePadding(context); return PullToRefresh( onRefresh: () async => await ref.read(favouritesProvider.notifier).fetchFavourites(), child: NestedScaffold( + background: BackgroundImage(items: favourites.favourites.values.expand((element) => element).toList()), body: PinchPosterZoom( scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2), child: CustomScrollView( @@ -52,25 +55,19 @@ class FavouritesScreen extends ConsumerWidget { ), ...favourites.favourites.entries.where((element) => element.value.isNotEmpty).map( (e) => SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: PosterGrid( - stickyHeader: true, - name: e.key.label(context), - posters: e.value, - ), + child: PosterRow( + contentPadding: padding, + label: e.key.label(context), + posters: e.value, ), ), ), if (favourites.people.isNotEmpty) SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: PosterGrid( - stickyHeader: true, - name: "People", - posters: favourites.people, - ), + child: PosterRow( + contentPadding: padding, + label: context.localized.actor(favourites.people.length), + posters: favourites.people, ), ), const DefautlSliverBottomPadding(), diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 61c0d55..3fb9fff 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.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/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; @@ -14,8 +14,40 @@ import 'package:fladder/widgets/navigation_scaffold/navigation_scaffold.dart'; enum HomeTabs { dashboard, + library, favorites, - sync; + sync, + ; + + const HomeTabs(); + + IconData get icon => switch (this) { + HomeTabs.dashboard => IconsaxPlusLinear.home_1, + HomeTabs.library => IconsaxPlusLinear.book, + HomeTabs.favorites => IconsaxPlusLinear.heart, + HomeTabs.sync => IconsaxPlusLinear.cloud, + }; + + IconData get selectedIcon => switch (this) { + HomeTabs.dashboard => IconsaxPlusBold.home_1, + HomeTabs.library => IconsaxPlusBold.book, + HomeTabs.favorites => IconsaxPlusBold.heart, + HomeTabs.sync => IconsaxPlusBold.cloud, + }; + + Future navigate(BuildContext context) => switch (this) { + HomeTabs.dashboard => context.router.navigate(const DashboardRoute()), + HomeTabs.library => context.router.navigate(const LibraryRoute()), + HomeTabs.favorites => context.router.navigate(const FavouritesRoute()), + HomeTabs.sync => context.router.navigate(SyncedRoute()), + }; + + String label(BuildContext context) => switch (this) { + HomeTabs.dashboard => context.localized.dashboard, + HomeTabs.library => context.localized.library(0), + HomeTabs.favorites => context.localized.favorites, + HomeTabs.sync => context.localized.sync, + }; } @RoutePage() @@ -31,10 +63,10 @@ class HomeScreen extends ConsumerWidget { case HomeTabs.dashboard: return DestinationModel( label: context.localized.navigationDashboard, - icon: const Icon(IconsaxPlusLinear.home), - selectedIcon: const Icon(IconsaxPlusBold.home), + icon: Icon(e.icon), + selectedIcon: Icon(e.selectedIcon), route: const DashboardRoute(), - action: () => context.router.navigate(const DashboardRoute()), + action: () => e.navigate(context), floatingActionButton: AdaptiveFab( context: context, title: context.localized.search, @@ -46,8 +78,8 @@ class HomeScreen extends ConsumerWidget { case HomeTabs.favorites: return DestinationModel( label: context.localized.navigationFavorites, - icon: const Icon(IconsaxPlusLinear.heart), - selectedIcon: const Icon(IconsaxPlusBold.heart), + icon: Icon(e.icon), + selectedIcon: Icon(e.selectedIcon), route: const FavouritesRoute(), floatingActionButton: AdaptiveFab( context: context, @@ -56,19 +88,26 @@ class HomeScreen extends ConsumerWidget { onPressed: () => context.router.navigate(LibrarySearchRoute(favourites: true)), child: const Icon(IconsaxPlusLinear.heart_search), ), - action: () => context.router.navigate(const FavouritesRoute()), + action: () => e.navigate(context), ); case HomeTabs.sync: if (canDownload) { return DestinationModel( label: context.localized.navigationSync, - icon: const Icon(IconsaxPlusLinear.cloud), - selectedIcon: const Icon(IconsaxPlusBold.cloud), + icon: Icon(e.icon), + selectedIcon: Icon(e.selectedIcon), route: SyncedRoute(), - action: () => context.router.navigate(SyncedRoute()), + action: () => e.navigate(context), ); } - return null; + case HomeTabs.library: + return DestinationModel( + label: context.localized.library(0), + icon: Icon(e.icon), + selectedIcon: Icon(e.selectedIcon), + route: const LibraryRoute(), + action: () => e.navigate(context), + ); } }) .nonNulls diff --git a/lib/screens/library/components/library_tabs.dart b/lib/screens/library/components/library_tabs.dart deleted file mode 100644 index e27c1f8..0000000 --- a/lib/screens/library/components/library_tabs.dart +++ /dev/null @@ -1,83 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first - -import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; -import 'package:fladder/screens/library/tabs/favourites_tab.dart'; -import 'package:fladder/screens/library/tabs/library_tab.dart'; -import 'package:fladder/screens/library/tabs/timeline_tab.dart'; -import 'package:flutter/material.dart'; - -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/screens/library/tabs/recommendations_tab.dart'; - -class LibraryTabs { - final String name; - final Icon icon; - final Widget page; - final FloatingActionButton? floatingActionButton; - LibraryTabs({ - required this.name, - required this.icon, - required this.page, - this.floatingActionButton, - }); - - static List getLibraryForType(ViewModel viewModel, CollectionType type) { - LibraryTabs recommendTab() { - return LibraryTabs( - name: "Recommended", - icon: const Icon(Icons.recommend_rounded), - page: RecommendationsTab(viewModel: viewModel), - ); - } - - LibraryTabs timelineTab() { - return LibraryTabs( - name: "Timeline", - icon: const Icon(Icons.timeline), - page: TimelineTab(viewModel: viewModel), - ); - } - - LibraryTabs favouritesTab() { - return LibraryTabs( - name: "Favourites", - icon: const Icon(Icons.favorite_rounded), - page: FavouritesTab(viewModel: viewModel), - ); - } - - LibraryTabs libraryTab() { - return LibraryTabs( - name: "Library", - icon: const Icon(Icons.book_rounded), - page: LibraryTab(viewModel: viewModel), - ); - } - - switch (type) { - case CollectionType.tvshows: - case CollectionType.movies: - return [ - libraryTab(), - recommendTab(), - favouritesTab(), - ]; - case CollectionType.books: - case CollectionType.homevideos: - return [ - libraryTab(), - timelineTab(), - recommendTab(), - favouritesTab(), - ]; - case CollectionType.boxsets: - case CollectionType.playlists: - case CollectionType.folders: - return [ - libraryTab(), - ]; - default: - return []; - } - } -} diff --git a/lib/screens/library/library_screen.dart b/lib/screens/library/library_screen.dart index 896c9f6..b051b07 100644 --- a/lib/screens/library/library_screen.dart +++ b/lib/screens/library/library_screen.dart @@ -1,14 +1,33 @@ -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/library_provider.dart'; -import 'package:fladder/screens/library/components/library_tabs.dart'; -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'dart:async'; +import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; + +import 'package:fladder/models/recommended_model.dart'; +import 'package:fladder/models/view_model.dart'; +import 'package:fladder/providers/library_screen_provider.dart'; +import 'package:fladder/routes/auto_router.gr.dart'; +import 'package:fladder/screens/metadata/refresh_metadata.dart'; +import 'package:fladder/screens/shared/flat_button.dart'; +import 'package:fladder/screens/shared/media/poster_row.dart'; +import 'package:fladder/screens/shared/nested_scaffold.dart'; +import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/fladder_image.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/sliver_list_padding.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart'; +import 'package:fladder/widgets/shared/button_group.dart'; +import 'package:fladder/widgets/shared/horizontal_list.dart'; +import 'package:fladder/widgets/shared/item_actions.dart'; +import 'package:fladder/widgets/shared/pull_to_refresh.dart'; + +@RoutePage() class LibraryScreen extends ConsumerStatefulWidget { - final ViewModel viewModel; const LibraryScreen({ - required this.viewModel, super.key, }); @@ -17,76 +36,273 @@ class LibraryScreen extends ConsumerStatefulWidget { } class _LibraryScreenState extends ConsumerState with SingleTickerProviderStateMixin { - late final List tabs = LibraryTabs.getLibraryForType(widget.viewModel, widget.viewModel.collectionType); - late final TabController tabController = TabController(length: tabs.length, vsync: this); - - @override - void initState() { - super.initState(); - Future.microtask(() { - ref.read(libraryProvider(widget.viewModel.id).notifier).setupLibrary(widget.viewModel); - }); - - tabController.addListener(() { - if (tabController.previousIndex != tabController.index) { - setState(() {}); - } - }); - } - + final GlobalKey? refreshKey = GlobalKey(); @override Widget build(BuildContext context) { - final PreferredSizeWidget tabBar = TabBar( - isScrollable: AdaptiveLayout.of(context).isDesktop ? true : false, - indicatorWeight: 3, - controller: tabController, - tabs: tabs - .map((e) => Tab( - text: e.name, - icon: e.icon, - )) - .toList(), - ); - - return Padding( - padding: AdaptiveLayout.of(context).isDesktop - ? EdgeInsets.only(top: MediaQuery.of(context).padding.top) - : EdgeInsets.zero, - child: ClipRRect( - borderRadius: BorderRadius.circular(AdaptiveLayout.of(context).isDesktop ? 15 : 0), - child: Card( - margin: AdaptiveLayout.of(context).isDesktop ? null : EdgeInsets.zero, - elevation: 2, - child: Scaffold( - backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null, - floatingActionButton: tabs[tabController.index].floatingActionButton, - floatingActionButtonLocation: FloatingActionButtonLocation.endContained, - appBar: AppBar( - centerTitle: true, - backgroundColor: AdaptiveLayout.of(context).isDesktop ? Colors.transparent : null, - title: tabs.length > 1 ? (!AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null, - toolbarHeight: AdaptiveLayout.of(context).isDesktop ? 75 : 40, - bottom: tabs.length > 1 ? (AdaptiveLayout.of(context).isDesktop ? null : tabBar) : null, - ), - extendBody: true, - body: Padding( - padding: !AdaptiveLayout.of(context).isDesktop - ? EdgeInsets.only( - left: MediaQuery.of(context).padding.left, right: MediaQuery.of(context).padding.right) - : EdgeInsets.zero, - child: TabBarView( - controller: tabController, - children: tabs - .map((e) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: e.page, - )) - .toList(), - ), - ), + final libraryScreenState = ref.watch(libraryScreenProvider); + final views = libraryScreenState.views; + final recommendations = libraryScreenState.recommendations; + final favourites = libraryScreenState.favourites; + final selectedView = libraryScreenState.selectedViewModel; + final viewTypes = libraryScreenState.viewType; + final genres = libraryScreenState.genres; + final padding = AdaptiveLayout.adaptivePadding(context); + return NestedScaffold( + background: BackgroundImage( + items: [ + ...recommendations.expand((e) => e.posters), + ...favourites, + ], + ), + body: PullToRefresh( + refreshOnStart: true, + refreshKey: refreshKey, + onRefresh: () => ref.read(libraryScreenProvider.notifier).fetchAllLibraries(), + child: SizedBox.expand( + child: CustomScrollView( + controller: AdaptiveLayout.scrollOf(context), + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + const DefaultSliverTopBadding(), + if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) + NestedSliverAppBar( + route: LibrarySearchRoute(), + parent: context, + ), + if (views.isNotEmpty) + SliverToBoxAdapter( + child: LibraryRow( + padding: padding, + views: views, + selectedView: libraryScreenState.selectedViewModel, + onSelected: (view) { + ref.read(libraryScreenProvider.notifier).selectLibrary(view); + refreshKey?.currentState?.show(); + }, + ), + ), + if (selectedView != null) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 24, bottom: 16), + child: SizedBox( + height: 40, + child: ListView( + padding: padding, + shrinkWrap: true, + scrollDirection: Axis.horizontal, + children: [ + FilledButton.tonalIcon( + onPressed: () => context.pushRoute(LibrarySearchRoute(viewModelId: selectedView.id)), + label: Text("${context.localized.search} ${selectedView.name}..."), + icon: const Icon(IconsaxPlusLinear.search_normal), + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4.0), + child: VerticalDivider(), + ), + ExpressiveButtonGroup( + multiSelection: true, + options: LibraryViewType.values + .map((element) => ButtonGroupOption( + value: element, + icon: Icon(element.icon), + selected: Icon(element.iconSelected), + child: Text( + element.label(context), + ))) + .toList(), + selectedValues: viewTypes, + onSelected: (value) { + ref.read(libraryScreenProvider.notifier).setViewType(value); + }, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 4.0), + child: VerticalDivider(), + ), + ElevatedButton.icon( + onPressed: () => showRefreshPopup(context, selectedView.id, selectedView.name), + label: Text(context.localized.scanLibrary), + icon: const Icon(IconsaxPlusLinear.refresh), + ), + ], + ), + ), + ), + ), + if (viewTypes.isEmpty) + SliverFillRemaining( + child: Center(child: Text(context.localized.noResults)), + ), + if (viewTypes.contains(LibraryViewType.recommended)) ...[ + if (recommendations.isNotEmpty) + ...recommendations.where((element) => element.posters.isNotEmpty).map( + (element) => SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: PosterRow( + contentPadding: padding, + posters: element.posters, + label: element.type != null + ? "${element.type?.label(context)} - ${element.name.label(context)}" + : element.name.label(context), + ), + ), + ), + ), + ], + if (viewTypes.contains(LibraryViewType.favourites)) + if (favourites.isNotEmpty) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: PosterRow( + contentPadding: padding, + posters: favourites, + label: context.localized.favorites, + ), + ), + ), + if (viewTypes.contains(LibraryViewType.genres)) ...[ + if (genres.isNotEmpty) + ...genres.where((element) => element.posters.isNotEmpty).map( + (element) => SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: PosterRow( + contentPadding: padding, + posters: element.posters, + label: element.type != null + ? "${element.type?.label(context)} - ${element.name.label(context)}" + : element.name.label(context), + ), + ), + ), + ) + ], + const DefautlSliverBottomPadding(), + ], ), ), ), ); } } + +class LibraryRow extends ConsumerWidget { + const LibraryRow({ + super.key, + required this.views, + this.selectedView, + required this.padding, + this.onSelected, + }); + + final List views; + final ViewModel? selectedView; + final EdgeInsets padding; + final FutureOr Function(ViewModel selected)? onSelected; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return HorizontalList( + label: context.localized.library(views.length), + items: views, + startIndex: selectedView != null ? views.indexOf(selectedView!) : null, + height: 165, + contentPadding: padding, + itemBuilder: (context, index) { + final view = views[index]; + final isSelected = selectedView == view; + final List viewActions = [ + ItemActionButton( + label: Text(context.localized.search), + icon: const Icon(IconsaxPlusLinear.search_normal), + action: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)), + ), + ItemActionButton( + label: Text(context.localized.scanLibrary), + icon: const Icon(IconsaxPlusLinear.refresh), + action: () => showRefreshPopup(context, view.id, view.name), + ) + ]; + return FlatButton( + onTap: isSelected ? null : () => onSelected?.call(view), + onLongPress: () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)), + onSecondaryTapDown: (details) async { + Offset localPosition = details.globalPosition; + RelativeRect position = + RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); + await showMenu( + context: context, + position: position, + items: viewActions.popupMenuItems(useIcons: true), + ); + }, + child: Card( + color: isSelected ? Theme.of(context).colorScheme.primaryContainer : null, + shadowColor: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 4, + children: [ + SizedBox( + width: 200, + child: Card( + child: AspectRatio( + aspectRatio: 1.60, + child: FladderImage( + image: view.imageData?.primary, + fit: BoxFit.cover, + placeHolder: Center( + child: Text( + view.name, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ), + ), + ), + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Row( + spacing: 8, + children: [ + if (isSelected) + Container( + height: 12, + width: 12, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + ), + Text( + view.name, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.start, + ), + ], + ), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/screens/library/tabs/favourites_tab.dart b/lib/screens/library/tabs/favourites_tab.dart deleted file mode 100644 index ff633b7..0000000 --- a/lib/screens/library/tabs/favourites_tab.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/library_provider.dart'; -import 'package:fladder/screens/shared/media/poster_grid.dart'; -import 'package:fladder/widgets/shared/pull_to_refresh.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class FavouritesTab extends ConsumerStatefulWidget { - final ViewModel viewModel; - const FavouritesTab({required this.viewModel, super.key}); - - @override - ConsumerState createState() => _FavouritesTabState(); -} - -class _FavouritesTabState extends ConsumerState with AutomaticKeepAliveClientMixin { - @override - Widget build(BuildContext context) { - final favourites = ref.watch(libraryProvider(widget.viewModel.id))?.favourites ?? []; - super.build(context); - return PullToRefresh( - onRefresh: () async { - await ref.read(libraryProvider(widget.viewModel.id).notifier).loadFavourites(widget.viewModel); - }, - child: favourites.isNotEmpty - ? ListView( - children: [ - PosterGrid(posters: favourites), - ], - ) - : const Center(child: Text("No favourites, add some using the heart icon.")), - ); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/lib/screens/library/tabs/library_tab.dart b/lib/screens/library/tabs/library_tab.dart deleted file mode 100644 index 8ec1163..0000000 --- a/lib/screens/library/tabs/library_tab.dart +++ /dev/null @@ -1,40 +0,0 @@ -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/library_provider.dart'; -import 'package:fladder/screens/shared/media/poster_grid.dart'; -import 'package:fladder/util/grouping.dart'; -import 'package:fladder/util/keyed_list_view.dart'; -import 'package:fladder/widgets/shared/pull_to_refresh.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class LibraryTab extends ConsumerStatefulWidget { - final ViewModel viewModel; - const LibraryTab({required this.viewModel, super.key}); - - @override - ConsumerState createState() => _LibraryTabState(); -} - -class _LibraryTabState extends ConsumerState with AutomaticKeepAliveClientMixin { - @override - Widget build(BuildContext context) { - super.build(context); - final library = ref.watch(libraryProvider(widget.viewModel.id).select((value) => value?.posters)) ?? []; - final items = groupByName(library); - return PullToRefresh( - onRefresh: () async { - await ref.read(libraryProvider(widget.viewModel.id).notifier).loadLibrary(widget.viewModel); - }, - child: KeyedListView( - map: items, - itemBuilder: (context, index) { - final currentIndex = items.entries.elementAt(index); - return PosterGrid(name: currentIndex.key, posters: currentIndex.value); - }, - ), - ); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/lib/screens/library/tabs/recommendations_tab.dart b/lib/screens/library/tabs/recommendations_tab.dart deleted file mode 100644 index 2de9ca7..0000000 --- a/lib/screens/library/tabs/recommendations_tab.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/library_provider.dart'; -import 'package:fladder/screens/shared/media/poster_grid.dart'; -import 'package:fladder/util/list_padding.dart'; -import 'package:fladder/widgets/shared/pull_to_refresh.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -class RecommendationsTab extends ConsumerStatefulWidget { - final ViewModel viewModel; - - const RecommendationsTab({required this.viewModel, super.key}); - - @override - ConsumerState createState() => _RecommendationsTabState(); -} - -class _RecommendationsTabState extends ConsumerState with AutomaticKeepAliveClientMixin { - @override - Widget build(BuildContext context) { - super.build(context); - final recommendations = ref.watch(libraryProvider(widget.viewModel.id) - .select((value) => value?.recommendations.where((element) => element.posters.isNotEmpty))) ?? - []; - return PullToRefresh( - onRefresh: () async { - await ref.read(libraryProvider(widget.viewModel.id).notifier).loadRecommendations(widget.viewModel); - }, - child: recommendations.isNotEmpty - ? ListView( - children: recommendations - .map( - (e) => PosterGrid(name: e.name, posters: e.posters), - ) - .toList() - .addPadding( - const EdgeInsets.only( - bottom: 32, - ), - ), - ) - : const Center( - child: Text("No recommendations, add more movies and or shows to receive more recomendations")), - ); - } - - @override - bool get wantKeepAlive => true; -} diff --git a/lib/screens/library/tabs/timeline_tab.dart b/lib/screens/library/tabs/timeline_tab.dart deleted file mode 100644 index d4abedf..0000000 --- a/lib/screens/library/tabs/timeline_tab.dart +++ /dev/null @@ -1,133 +0,0 @@ -import 'package:fladder/models/items/photos_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/models/view_model.dart'; -import 'package:fladder/providers/library_provider.dart'; -import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart'; -import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:fladder/util/fladder_image.dart'; -import 'package:fladder/util/sticky_header_text.dart'; -import 'package:fladder/widgets/shared/pull_to_refresh.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:intl/intl.dart'; -import 'package:page_transition/page_transition.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'package:sticky_headers/sticky_headers.dart'; - -class TimelineTab extends ConsumerStatefulWidget { - final ViewModel viewModel; - - const TimelineTab({required this.viewModel, super.key}); - - @override - ConsumerState createState() => _TimelineTabState(); -} - -class _TimelineTabState extends ConsumerState with AutomaticKeepAliveClientMixin { - final itemScrollController = ItemScrollController(); - double get posterCount { - if (AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop) { - return 200; - } - return 125; - } - - @override - Widget build(BuildContext context) { - super.build(context); - final timeLine = ref.watch(libraryProvider(widget.viewModel.id))?.timelinePhotos ?? []; - final items = groupedItems(timeLine); - - return PullToRefresh( - onRefresh: () async { - await ref.read(libraryProvider(widget.viewModel.id).notifier).loadTimeline(widget.viewModel); - }, - child: ScrollablePositionedList.builder( - itemScrollController: itemScrollController, - itemCount: items.length, - itemBuilder: (context, index) { - final item = items.entries.elementAt(index); - return Padding( - padding: const EdgeInsets.only(bottom: 64.0), - child: StickyHeader( - header: StickyHeaderText( - label: item.key.year != DateTime.now().year - ? DateFormat('E dd MMM. y').format(item.key) - : DateFormat('E dd MMM.').format(item.key)), - content: StaggeredGrid.count( - crossAxisCount: MediaQuery.of(context).size.width ~/ posterCount, - mainAxisSpacing: 0, - crossAxisSpacing: 0, - axisDirection: AxisDirection.down, - children: item.value - .map( - (e) => Hero( - tag: e.id, - child: AspectRatio( - aspectRatio: e.primaryRatio ?? 0.0, - child: Card( - margin: const EdgeInsets.all(4), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), - clipBehavior: Clip.antiAlias, - child: Stack( - children: [ - FladderImage(image: e.thumbnail?.primary), - FlatButton( - onLongPress: () {}, - onTap: () async { - final position = await Navigator.of(context, rootNavigator: true).push( - PageTransition( - child: PhotoViewerScreen( - items: timeLine, - indexOfSelected: timeLine.indexOf(e), - ), - type: PageTransitionType.fade), - ); - getParentPosition(items, timeLine, position); - }, - ) - ], - ), - ), - ), - ), - ) - .toList(), - ), - ), - ); - }, - ), - ); - } - - void getParentPosition(Map> items, List timeLine, int position) { - items.forEach( - (key, value) { - if (value.contains(timeLine[position])) { - itemScrollController.scrollTo( - index: items.keys.toList().indexOf(key), duration: const Duration(milliseconds: 250)); - } - }, - ); - } - - Map> groupedItems(List items) { - Map> groupedItems = {}; - for (int i = 0; i < items.length; i++) { - DateTime curretDate = items[i].dateTaken ?? DateTime.now(); - DateTime key = DateTime(curretDate.year, curretDate.month, curretDate.day); - if (!groupedItems.containsKey(key)) { - groupedItems[key] = [items[i]]; - } else { - groupedItems[key]?.add(items[i]); - } - } - return groupedItems; - } - - @override - bool get wantKeepAlive => true; -} diff --git a/lib/screens/library_search/library_search_screen.dart b/lib/screens/library_search/library_search_screen.dart index 2f3dcbe..dbd6352 100644 --- a/lib/screens/library_search/library_search_screen.dart +++ b/lib/screens/library_search/library_search_screen.dart @@ -11,12 +11,9 @@ import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/library_search/library_search_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; -import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/playlist_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/library_search_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; -import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/collections/add_to_collection.dart'; import 'package:fladder/screens/library_search/widgets/library_filter_chips.dart'; import 'package:fladder/screens/library_search/widgets/library_play_options_.dart'; @@ -26,9 +23,9 @@ import 'package:fladder/screens/library_search/widgets/library_views.dart'; import 'package:fladder/screens/library_search/widgets/suggestion_search_bar.dart'; import 'package:fladder/screens/playlists/add_to_playlists.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; -import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/nested_bottom_appbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/screens/shared/nested_scaffold.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/debouncer.dart'; import 'package:fladder/util/fab_extended_anim.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; @@ -37,8 +34,7 @@ import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/map_bool_helper.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/router_extension.dart'; -import 'package:fladder/util/sliver_list_padding.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart'; import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; import 'package:fladder/widgets/shared/fladder_scrollbar.dart'; import 'package:fladder/widgets/shared/hide_on_scroll.dart'; @@ -136,7 +132,6 @@ class _LibrarySearchScreenState extends ConsumerState { final isEmptySearchScreen = widget.viewModelId == null && widget.favourites == null && widget.folderId == null; final librarySearchResults = ref.watch(providerKey); final postersList = librarySearchResults.posters.hideEmptyChildren(librarySearchResults.hideEmptyShows); - final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state)); final libraryViewType = ref.watch(libraryViewTypeProvider); ref.listen( @@ -157,19 +152,14 @@ class _LibrarySearchScreenState extends ConsumerState { libraryProvider.toggleSelectMode(); } }, - child: Scaffold( - extendBody: true, - extendBodyBehindAppBar: true, - floatingActionButtonAnimator: - playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null, - floatingActionButtonLocation: - playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null, - floatingActionButton: switch (playerState) { - VideoPlayerState.minimized => const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: FloatingPlayerBar(), - ), - _ => HideOnScroll( + child: NestedScaffold( + background: BackgroundImage(items: librarySearchResults.activePosters), + body: Padding( + padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth), + child: Scaffold( + extendBody: true, + extendBodyBehindAppBar: true, + floatingActionButton: HideOnScroll( controller: scrollController, visibleBuilder: (visible) => Column( crossAxisAlignment: CrossAxisAlignment.end, @@ -206,29 +196,26 @@ class _LibrarySearchScreenState extends ConsumerState { ].addInBetween(const SizedBox(height: 10)), ), ), - }, - bottomNavigationBar: HideOnScroll( - controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController, - child: IgnorePointer( - ignoring: librarySearchResults.fetchingItems, - child: _LibrarySearchBottomBar( - uniqueKey: uniqueKey, - refreshKey: refreshKey, - scrollController: scrollController, - libraryProvider: libraryProvider, - postersList: postersList, + bottomNavigationBar: HideOnScroll( + controller: AdaptiveLayout.of(context).isDesktop ? null : scrollController, + child: IgnorePointer( + ignoring: librarySearchResults.fetchingItems, + child: _LibrarySearchBottomBar( + uniqueKey: uniqueKey, + refreshKey: refreshKey, + scrollController: scrollController, + libraryProvider: libraryProvider, + postersList: postersList, + ), + ), ), - ), - ), - body: Stack( - children: [ - Positioned.fill( - child: Card( - elevation: 1, - child: PinchPosterZoom( - scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference), - child: MediaQuery.removeViewInsets( - context: context, + body: Stack( + fit: StackFit.expand, + children: [ + Positioned.fill( + child: PinchPosterZoom( + scaleDifference: (difference) => + ref.read(clientSettingsProvider.notifier).addPosterSize(difference), child: ClipRRect( borderRadius: AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop ? BorderRadius.circular(15) @@ -253,8 +240,8 @@ class _LibrarySearchScreenState extends ConsumerState { }, refreshOnStart: false, child: CustomScrollView( - physics: const AlwaysScrollableNoImplicitScrollPhysics(), controller: scrollController, + physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverAppBar( floating: !AdaptiveLayout.of(context).isDesktop, @@ -370,7 +357,7 @@ class _LibrarySearchScreenState extends ConsumerState { onTapUp: (details) async { if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) { double left = details.globalPosition.dx; - double top = details.globalPosition.dy + 20; + double top = details.globalPosition.dy; await showMenu( context: context, position: RelativeRect.fromLTRB(left, top, 40, 100), @@ -463,16 +450,10 @@ class _LibrarySearchScreenState extends ConsumerState { padding: const EdgeInsets.all(8), scrollDirection: Axis.horizontal, child: LibraryFilterChips( - controller: scrollController, - libraryProvider: libraryProvider, - librarySearchResults: librarySearchResults, - uniqueKey: uniqueKey, - postersList: postersList, - libraryViewType: libraryViewType, + key: uniqueKey, ), ), ), - const Row(), ], ), ), @@ -500,13 +481,12 @@ class _LibrarySearchScreenState extends ConsumerState { ), ) else - SliverToBoxAdapter( + SliverFillRemaining( child: Center( child: Text(context.localized.noItemsToShow), ), ), - const DefautlSliverBottomPadding(), - const SliverPadding(padding: EdgeInsets.only(bottom: 80)) + SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height * 0.20)) ], ), ), @@ -514,61 +494,42 @@ class _LibrarySearchScreenState extends ConsumerState { ), ), ), - ), - ), - if (librarySearchResults.fetchingItems) ...[ - Container( - color: Colors.black.withValues(alpha: 0.1), - ), - Center( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(16), + if (librarySearchResults.fetchingItems) ...[ + Container( + color: Colors.black.withValues(alpha: 0.1), ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator.adaptive(), - Text(context.localized.fetchingLibrary, style: Theme.of(context).textTheme.titleMedium), - IconButton( - onPressed: () => libraryProvider.cancelFetch(), - icon: const Icon(IconsaxPlusLinear.close_square), - ) - ].addInBetween(const SizedBox(width: 16)), + Center( + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator.adaptive(), + Text(context.localized.fetchingLibrary, style: Theme.of(context).textTheme.titleMedium), + IconButton( + onPressed: () => libraryProvider.cancelFetch(), + icon: const Icon(IconsaxPlusLinear.close_square), + ) + ].addInBetween(const SizedBox(width: 16)), + ), + ), ), - ), - ), - ) - ], - ], + ) + ], + ], + ), + ), ), ), ); } } -class AlwaysScrollableNoImplicitScrollPhysics extends ScrollPhysics { - /// Creates scroll physics that always lets the user scroll. - const AlwaysScrollableNoImplicitScrollPhysics({super.parent}); - - @override - AlwaysScrollableNoImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) { - return AlwaysScrollableNoImplicitScrollPhysics(parent: buildParent(ancestor)); - } - - @override - bool get allowImplicitScrolling => false; - - @override - bool shouldAcceptUserOffset(ScrollMetrics position) => true; - - @override - bool recommendDeferredLoading(double velocity, ScrollMetrics metrics, BuildContext context) => false; -} - class _LibrarySearchBottomBar extends ConsumerWidget { final Key uniqueKey; final ScrollController scrollController; @@ -663,173 +624,157 @@ class _LibrarySearchBottomBar extends ConsumerWidget { icon: const Icon(IconsaxPlusLinear.save_add), ), ]; - return NestedBottomAppBar( - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Row( + + final paddingOf = MediaQuery.paddingOf(context); + return Padding( + padding: EdgeInsets.only(left: paddingOf.left, right: paddingOf.right), + child: NestedBottomAppBar( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, children: [ - ScrollStatePosition( - controller: scrollController, - positionBuilder: (state) => AnimatedFadeSize( - child: state != ScrollState.top - ? Tooltip( - message: context.localized.scrollToTop, - child: FlatButton( - clipBehavior: Clip.antiAlias, - elevation: 0, - borderRadiusGeometry: BorderRadius.circular(6), - onTap: () => scrollController.animateTo(0, - duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(8), + Row( + spacing: 6, + children: [ + ScrollStatePosition( + controller: scrollController, + positionBuilder: (state) => AnimatedFadeSize( + child: state != ScrollState.top + ? Tooltip( + message: context.localized.scrollToTop, + child: IconButton.filled( + onPressed: () => scrollController.animateTo(0, + duration: const Duration(milliseconds: 500), curve: Curves.easeInOutCubic), + icon: const Icon( + IconsaxPlusLinear.arrow_up, + ), ), - padding: const EdgeInsets.all(6), - child: Icon( - IconsaxPlusLinear.arrow_up, - color: Theme.of(context).colorScheme.onPrimaryContainer, - ), - ), - ), - ) - : const SizedBox(), - ), - ), - const SizedBox(width: 6), - if (!librarySearchResults.selecteMode) ...{ - const SizedBox(width: 6), - IconButton( - tooltip: context.localized.sortBy, - onPressed: () async { - final newOptions = await openSortByDialogue( - context, - libraryProvider: libraryProvider, - uniqueKey: uniqueKey, - options: (librarySearchResults.sortingOption, librarySearchResults.sortOrder), - ); - if (newOptions != null) { - if (newOptions.$1 != null) { - libraryProvider.setSortBy(newOptions.$1!); - } - if (newOptions.$2 != null) { - libraryProvider.setSortOrder(newOptions.$2!); - } - } - }, - icon: const Icon(IconsaxPlusLinear.sort), - ), - if (librarySearchResults.hasActiveFilters) ...{ - const SizedBox(width: 6), - IconButton( - tooltip: context.localized.disableFilters, - onPressed: disableFilters(librarySearchResults, libraryProvider), - icon: const Icon(IconsaxPlusLinear.filter_remove), - ), - }, - }, - const SizedBox(width: 6), - IconButton( - onPressed: () => libraryProvider.toggleSelectMode(), - color: librarySearchResults.selecteMode ? Theme.of(context).colorScheme.primary : null, - icon: const Icon(IconsaxPlusLinear.category_2), - ), - const SizedBox(width: 6), - AnimatedFadeSize( - child: librarySearchResults.selecteMode - ? Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(16)), - child: Row( - children: [ - Tooltip( - message: context.localized.selectAll, - child: IconButton( - onPressed: () => libraryProvider.selectAll(true), - icon: const Icon(IconsaxPlusLinear.box_add), - ), - ), - const SizedBox(width: 6), - Tooltip( - message: context.localized.clearSelection, - child: IconButton( - onPressed: () => libraryProvider.selectAll(false), - icon: const Icon(IconsaxPlusLinear.box_remove), - ), - ), - const SizedBox(width: 6), - if (librarySearchResults.selectedPosters.isNotEmpty) ...{ - if (AdaptiveLayout.of(context).isDesktop) - PopupMenuButton( - itemBuilder: (context) => actions.popupMenuItems(useIcons: true), - ) - else - IconButton( - onPressed: () { - showBottomSheetPill( - context: context, - content: (context, scrollController) => ListView( - shrinkWrap: true, - controller: scrollController, - children: actions.listTileItems(context, useIcons: true), - ), - ); - }, - icon: const Icon(IconsaxPlusLinear.more)) - }, - ], - ), - ) - : const SizedBox(), - ), - const Spacer(), - if (librarySearchResults.activePosters.isNotEmpty) - IconButton( - tooltip: context.localized.random, - onPressed: () => libraryProvider.openRandom(context), - icon: Card( - color: Theme.of(context).colorScheme.secondary, - child: Padding( - padding: const EdgeInsets.all(2.0), - child: Icon( - IconsaxPlusBold.arrow_up_1, - color: Theme.of(context).colorScheme.onSecondary, - ), + ) + : const SizedBox(), ), ), - ), - if (librarySearchResults.activePosters.isNotEmpty) - IconButton( - tooltip: context.localized.shuffleVideos, - onPressed: () async { - if (librarySearchResults.showGalleryButtons && !librarySearchResults.showPlayButtons) { - libraryProvider.viewGallery(context, shuffle: true); - return; - } else if (!librarySearchResults.showGalleryButtons && librarySearchResults.showPlayButtons) { - libraryProvider.playLibraryItems(context, ref, shuffle: true); - return; - } - - await showLibraryPlayOptions( - context, - context.localized.libraryShuffleAndPlayItems, - playVideos: librarySearchResults.showPlayButtons - ? () => libraryProvider.playLibraryItems(context, ref, shuffle: true) - : null, - viewGallery: librarySearchResults.showGalleryButtons - ? () => libraryProvider.viewGallery(context, shuffle: true) - : null, - ); + if (!librarySearchResults.selecteMode) ...{ + IconButton( + tooltip: context.localized.sortBy, + onPressed: () async { + final newOptions = await openSortByDialogue( + context, + libraryProvider: libraryProvider, + uniqueKey: uniqueKey, + options: (librarySearchResults.sortingOption, librarySearchResults.sortOrder), + ); + if (newOptions != null) { + if (newOptions.$1 != null) { + libraryProvider.setSortBy(newOptions.$1!); + } + if (newOptions.$2 != null) { + libraryProvider.setSortOrder(newOptions.$2!); + } + } + }, + icon: const Icon(IconsaxPlusLinear.sort), + ), + if (librarySearchResults.hasActiveFilters) ...{ + IconButton( + tooltip: context.localized.disableFilters, + onPressed: disableFilters(librarySearchResults, libraryProvider), + icon: const Icon(IconsaxPlusLinear.filter_remove), + ), + }, }, - icon: const Icon(IconsaxPlusLinear.shuffle), - ), + IconButton( + onPressed: () => libraryProvider.toggleSelectMode(), + color: librarySearchResults.selecteMode ? Theme.of(context).colorScheme.primary : null, + icon: const Icon(IconsaxPlusLinear.category_2), + ), + AnimatedFadeSize( + child: librarySearchResults.selecteMode + ? Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(16)), + child: Row( + spacing: 6, + children: [ + Tooltip( + message: context.localized.selectAll, + child: IconButton( + onPressed: () => libraryProvider.selectAll(true), + icon: const Icon(IconsaxPlusLinear.box_add), + ), + ), + Tooltip( + message: context.localized.clearSelection, + child: IconButton( + onPressed: () => libraryProvider.selectAll(false), + icon: const Icon(IconsaxPlusLinear.box_remove), + ), + ), + if (librarySearchResults.selectedPosters.isNotEmpty) ...{ + if (AdaptiveLayout.of(context).isDesktop) + PopupMenuButton( + itemBuilder: (context) => actions.popupMenuItems(useIcons: true), + ) + else + IconButton( + onPressed: () { + showBottomSheetPill( + context: context, + content: (context, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: actions.listTileItems(context, useIcons: true), + ), + ); + }, + icon: const Icon(IconsaxPlusLinear.more)) + }, + ], + ), + ) + : const SizedBox(), + ), + const Spacer(), + if (librarySearchResults.activePosters.isNotEmpty) + IconButton.filledTonal( + tooltip: context.localized.random, + onPressed: () => libraryProvider.openRandom(context), + icon: const Icon( + IconsaxPlusBold.arrow_up_1, + ), + ), + if (librarySearchResults.activePosters.isNotEmpty) + IconButton( + tooltip: context.localized.shuffleVideos, + onPressed: () async { + if (librarySearchResults.showGalleryButtons && !librarySearchResults.showPlayButtons) { + libraryProvider.viewGallery(context, shuffle: true); + return; + } else if (!librarySearchResults.showGalleryButtons && librarySearchResults.showPlayButtons) { + libraryProvider.playLibraryItems(context, ref, shuffle: true); + return; + } + + await showLibraryPlayOptions( + context, + context.localized.libraryShuffleAndPlayItems, + playVideos: librarySearchResults.showPlayButtons + ? () => libraryProvider.playLibraryItems(context, ref, shuffle: true) + : null, + viewGallery: librarySearchResults.showGalleryButtons + ? () => libraryProvider.viewGallery(context, shuffle: true) + : null, + ); + }, + icon: const Icon(IconsaxPlusLinear.shuffle), + ), + ], + ), ], ), - if (AdaptiveLayout.of(context).isDesktop) const SizedBox(height: 8), - ], + ), ), ); } diff --git a/lib/screens/library_search/widgets/library_filter_chips.dart b/lib/screens/library_search/widgets/library_filter_chips.dart index a158673..94cf3bb 100644 --- a/lib/screens/library_search/widgets/library_filter_chips.dart +++ b/lib/screens/library_search/widgets/library_filter_chips.dart @@ -1,221 +1,193 @@ import 'package:flutter/material.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/models/item_base_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; -import 'package:fladder/models/library_search/library_search_model.dart'; import 'package:fladder/models/library_search/library_search_options.dart'; import 'package:fladder/providers/library_search_provider.dart'; -import 'package:fladder/screens/library_search/widgets/library_views.dart'; import 'package:fladder/screens/shared/chips/category_chip.dart'; -import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/map_bool_helper.dart'; import 'package:fladder/util/refresh_state.dart'; -import 'package:fladder/widgets/shared/scroll_position.dart'; -class LibraryFilterChips extends ConsumerWidget { - final Key uniqueKey; - final ScrollController controller; - final LibrarySearchModel librarySearchResults; - final LibrarySearchNotifier libraryProvider; - final List postersList; - final LibraryViewTypes libraryViewType; - const LibraryFilterChips({ - required this.uniqueKey, - required this.controller, - required this.librarySearchResults, - required this.libraryProvider, - required this.postersList, - required this.libraryViewType, - super.key, - }); +class LibraryFilterChips extends ConsumerStatefulWidget { + const LibraryFilterChips({super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - return ScrollStatePosition( - controller: controller, - positionBuilder: (state) { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.center, - children: libraryFilterChips( - context, - ref, - uniqueKey, - librarySearchResults: librarySearchResults, - libraryProvider: libraryProvider, - postersList: postersList, - libraryViewType: libraryViewType, - ).addPadding(const EdgeInsets.symmetric(horizontal: 8)), - ); - }, - ); - } + ConsumerState createState() => _LibraryFilterChipsState(); } -List libraryFilterChips( - BuildContext context, - WidgetRef ref, - Key uniqueKey, { - required LibrarySearchModel librarySearchResults, - required LibrarySearchNotifier libraryProvider, - required List postersList, - required LibraryViewTypes libraryViewType, -}) { - Future openGroupDialogue() { - return showDialog( +class _LibraryFilterChipsState extends ConsumerState { + @override + Widget build(BuildContext context) { + final uniqueKey = widget.key ?? UniqueKey(); + final libraryProvider = ref.watch(librarySearchProvider(uniqueKey).notifier); + final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy)); + final favourites = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.favourites)); + final recursive = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.recursive)); + final hideEmpty = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.hideEmptyShows)); + final librarySearchResults = ref.watch(librarySearchProvider(uniqueKey)); + + return Row( + spacing: 8, + children: [ + if (librarySearchResults.folderOverwrite.isEmpty) + CategoryChip( + label: Text(context.localized.library(2)), + items: librarySearchResults.views, + labelBuilder: (item) => Text(item.name), + onSave: (value) => libraryProvider.setViews(value), + onCancel: () => libraryProvider.setViews(librarySearchResults.views), + onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)), + ), + CategoryChip( + label: Text(context.localized.type(librarySearchResults.types.length)), + items: librarySearchResults.types, + labelBuilder: (item) => Row( + children: [ + Icon(item.icon), + const SizedBox(width: 12), + Text(item.label(context)), + ], + ), + onSave: (value) => libraryProvider.setTypes(value), + onClear: () => libraryProvider.setTypes(librarySearchResults.types.setAll(false)), + ), + FilterChip( + label: Text(context.localized.favorites), + avatar: Icon( + favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart, + color: Theme.of(context).colorScheme.onSurface, + ), + selected: favourites, + showCheckmark: false, + onSelected: (_) { + libraryProvider.toggleFavourite(); + context.refreshData(); + }, + ), + FilterChip( + label: Text(context.localized.recursive), + selected: recursive, + onSelected: (_) { + libraryProvider.toggleRecursive(); + context.refreshData(); + }, + ), + if (librarySearchResults.genres.isNotEmpty) + CategoryChip( + label: Text(context.localized.genre(librarySearchResults.genres.length)), + activeIcon: IconsaxPlusBold.hierarchy_2, + items: librarySearchResults.genres, + labelBuilder: (item) => Text(item), + onSave: (value) => libraryProvider.setGenres(value), + onCancel: () => libraryProvider.setGenres(librarySearchResults.genres), + onClear: () => libraryProvider.setGenres(librarySearchResults.genres.setAll(false)), + ), + if (librarySearchResults.studios.isNotEmpty) + CategoryChip( + label: Text(context.localized.studio(librarySearchResults.studios.length)), + activeIcon: IconsaxPlusBold.airdrop, + items: librarySearchResults.studios, + labelBuilder: (item) => Text(item.name), + onSave: (value) => libraryProvider.setStudios(value), + onCancel: () => libraryProvider.setStudios(librarySearchResults.studios), + onClear: () => libraryProvider.setStudios(librarySearchResults.studios.setAll(false)), + ), + if (librarySearchResults.tags.isNotEmpty) + CategoryChip( + label: Text(context.localized.label(librarySearchResults.tags.length)), + activeIcon: Icons.label_rounded, + items: librarySearchResults.tags, + labelBuilder: (item) => Text(item), + onSave: (value) => libraryProvider.setTags(value), + onCancel: () => libraryProvider.setTags(librarySearchResults.tags), + onClear: () => libraryProvider.setTags(librarySearchResults.tags.setAll(false)), + ), + FilterChip( + label: Text(context.localized.group), + selected: groupBy != GroupBy.none, + onSelected: (_) { + _openGroupDialogue(context, ref, libraryProvider, uniqueKey); + }, + ), + CategoryChip( + label: Text(context.localized.filter(librarySearchResults.filters.length)), + items: librarySearchResults.filters, + labelBuilder: (item) => Text(item.label(context)), + onSave: (value) => libraryProvider.setFilters(value), + onClear: () => libraryProvider.setFilters(librarySearchResults.filters.setAll(false)), + ), + if (librarySearchResults.types[FladderItemType.series] == true) + FilterChip( + avatar: Icon( + hideEmpty ? Icons.visibility_off_rounded : Icons.visibility_rounded, + color: Theme.of(context).colorScheme.onSurface, + ), + selected: hideEmpty, + showCheckmark: false, + label: Text(context.localized.hideEmpty), + onSelected: libraryProvider.setHideEmpty, + ), + if (librarySearchResults.officialRatings.isNotEmpty) + CategoryChip( + label: Text(context.localized.rating(librarySearchResults.officialRatings.length)), + activeIcon: Icons.star_rate_rounded, + items: librarySearchResults.officialRatings, + labelBuilder: (item) => Text(item), + onSave: (value) => libraryProvider.setRatings(value), + onCancel: () => libraryProvider.setRatings(librarySearchResults.officialRatings), + onClear: () => libraryProvider.setRatings(librarySearchResults.officialRatings.setAll(false)), + ), + if (librarySearchResults.years.isNotEmpty) + CategoryChip( + label: Text(context.localized.year(librarySearchResults.years.length)), + items: librarySearchResults.years, + labelBuilder: (item) => Text(item.toString()), + onSave: (value) => libraryProvider.setYears(value), + onCancel: () => libraryProvider.setYears(librarySearchResults.years), + onClear: () => libraryProvider.setYears(librarySearchResults.years.setAll(false)), + ), + ], + ); + } + + void _openGroupDialogue( + BuildContext context, + WidgetRef ref, + LibrarySearchNotifier provider, + Key uniqueKey, + ) { + showDialog( context: context, builder: (context) { - return Consumer( - builder: (context, ref, child) { - return AlertDialog( - content: SizedBox( - width: MediaQuery.of(context).size.width * 0.65, - child: ListView( - shrinkWrap: true, - children: [ - Text(context.localized.groupBy), - ...GroupBy.values.map((groupBy) => RadioListTile.adaptive( - value: groupBy, - title: Text(groupBy.value(context)), - groupValue: ref.watch(librarySearchProvider(uniqueKey).select((value) => value.groupBy)), - onChanged: (value) { - libraryProvider.setGroupBy(groupBy); - Navigator.pop(context); - }, - )), - ], + final groupBy = ref.watch(librarySearchProvider(uniqueKey).select((v) => v.groupBy)); + return AlertDialog( + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.65, + child: ListView( + shrinkWrap: true, + children: [ + Text(context.localized.groupBy), + ...GroupBy.values.map( + (group) => RadioListTile.adaptive( + value: group, + groupValue: groupBy, + title: Text(group.value(context)), + onChanged: (_) { + provider.setGroupBy(group); + Navigator.pop(context); + }, + ), ), - ), - ); - }, + ], + ), + ), ); }, ); } - - return [ - if (librarySearchResults.folderOverwrite.isEmpty) - CategoryChip( - label: Text(context.localized.library(2)), - items: librarySearchResults.views, - labelBuilder: (item) => Text(item.name), - onSave: (value) => libraryProvider.setViews(value), - onCancel: () => libraryProvider.setViews(librarySearchResults.views), - onClear: () => libraryProvider.setViews(librarySearchResults.views.setAll(false)), - ), - CategoryChip( - label: Text(context.localized.type(librarySearchResults.types.length)), - items: librarySearchResults.types, - labelBuilder: (item) => Row( - children: [ - Icon(item.icon), - const SizedBox(width: 12), - Text(item.label(context)), - ], - ), - onSave: (value) => libraryProvider.setTypes(value), - onClear: () => libraryProvider.setTypes(librarySearchResults.types.setAll(false)), - ), - FilterChip( - label: Text(context.localized.favorites), - avatar: Icon( - librarySearchResults.favourites ? IconsaxPlusBold.heart : IconsaxPlusLinear.heart, - color: Theme.of(context).colorScheme.onSurface, - ), - selected: librarySearchResults.favourites, - showCheckmark: false, - onSelected: (value) { - libraryProvider.toggleFavourite(); - context.refreshData(); - }, - ), - FilterChip( - label: Text(context.localized.recursive), - selected: librarySearchResults.recursive, - onSelected: (value) { - libraryProvider.toggleRecursive(); - context.refreshData(); - }, - ), - if (librarySearchResults.genres.isNotEmpty) - CategoryChip( - label: Text(context.localized.genre(librarySearchResults.genres.length)), - activeIcon: IconsaxPlusBold.hierarchy_2, - items: librarySearchResults.genres, - labelBuilder: (item) => Text(item), - onSave: (value) => libraryProvider.setGenres(value), - onCancel: () => libraryProvider.setGenres(librarySearchResults.genres), - onClear: () => libraryProvider.setGenres(librarySearchResults.genres.setAll(false)), - ), - if (librarySearchResults.studios.isNotEmpty) - CategoryChip( - label: Text(context.localized.studio(librarySearchResults.studios.length)), - activeIcon: IconsaxPlusBold.airdrop, - items: librarySearchResults.studios, - labelBuilder: (item) => Text(item.name), - onSave: (value) => libraryProvider.setStudios(value), - onCancel: () => libraryProvider.setStudios(librarySearchResults.studios), - onClear: () => libraryProvider.setStudios(librarySearchResults.studios.setAll(false)), - ), - if (librarySearchResults.tags.isNotEmpty) - CategoryChip( - label: Text(context.localized.label(librarySearchResults.tags.length)), - activeIcon: Icons.label_rounded, - items: librarySearchResults.tags, - labelBuilder: (item) => Text(item), - onSave: (value) => libraryProvider.setTags(value), - onCancel: () => libraryProvider.setTags(librarySearchResults.tags), - onClear: () => libraryProvider.setTags(librarySearchResults.tags.setAll(false)), - ), - FilterChip( - label: Text(context.localized.group), - selected: librarySearchResults.groupBy != GroupBy.none, - onSelected: (value) { - openGroupDialogue(); - }, - ), - CategoryChip( - label: Text(context.localized.filter(librarySearchResults.filters.length)), - items: librarySearchResults.filters, - labelBuilder: (item) => Text(item.label(context)), - onSave: (value) => libraryProvider.setFilters(value), - onClear: () => libraryProvider.setFilters(librarySearchResults.filters.setAll(false)), - ), - if (librarySearchResults.types[FladderItemType.series] == true) - FilterChip( - avatar: Icon( - librarySearchResults.hideEmptyShows ? Icons.visibility_off_rounded : Icons.visibility_rounded, - color: Theme.of(context).colorScheme.onSurface, - ), - selected: librarySearchResults.hideEmptyShows, - showCheckmark: false, - label: Text(context.localized.hideEmpty), - onSelected: libraryProvider.setHideEmpty, - ), - if (librarySearchResults.officialRatings.isNotEmpty) - CategoryChip( - label: Text(context.localized.rating(librarySearchResults.officialRatings.length)), - activeIcon: Icons.star_rate_rounded, - items: librarySearchResults.officialRatings, - labelBuilder: (item) => Text(item), - onSave: (value) => libraryProvider.setRatings(value), - onCancel: () => libraryProvider.setRatings(librarySearchResults.officialRatings), - onClear: () => libraryProvider.setRatings(librarySearchResults.officialRatings.setAll(false)), - ), - if (librarySearchResults.years.isNotEmpty) - CategoryChip( - label: Text(context.localized.year(librarySearchResults.years.length)), - items: librarySearchResults.years, - labelBuilder: (item) => Text(item.toString()), - onSave: (value) => libraryProvider.setYears(value), - onCancel: () => libraryProvider.setYears(librarySearchResults.years), - onClear: () => libraryProvider.setYears(librarySearchResults.years.setAll(false)), - ), - ]; } diff --git a/lib/screens/library_search/widgets/library_saved_filters.dart b/lib/screens/library_search/widgets/library_saved_filters.dart index 93dfc50..f99df2c 100644 --- a/lib/screens/library_search/widgets/library_saved_filters.dart +++ b/lib/screens/library_search/widgets/library_saved_filters.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.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/models/library_search/library_search_model.dart'; import 'package:fladder/providers/library_search_provider.dart'; import 'package:fladder/screens/shared/default_alert_dialog.dart'; -import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/outlined_text_field.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -43,6 +42,7 @@ class LibrarySavedFiltersDialogue extends ConsumerWidget { child: Padding( padding: const EdgeInsets.all(16), child: Column( + spacing: 4, mainAxisSize: MainAxisSize.min, children: [ Text( @@ -60,7 +60,7 @@ class LibrarySavedFiltersDialogue extends ConsumerWidget { return Container( margin: const EdgeInsets.symmetric(vertical: 4), child: Card( - child: FlatButton( + child: InkWell( onTap: () => provider.loadModel(filter), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), @@ -130,22 +130,29 @@ class LibrarySavedFiltersDialogue extends ConsumerWidget { const Divider(), ], if (filters.length < 10) - Row( - children: [ - Flexible( - child: OutlinedTextField( - controller: controller, - label: context.localized.name, - onSubmitted: (value) => provider.saveFiltersNew(value), + StatefulBuilder(builder: (context, setState) { + return Row( + children: [ + Flexible( + child: OutlinedTextField( + controller: controller, + label: context.localized.name, + onChanged: (value) => setState(() {}), + onSubmitted: (value) => provider.saveFiltersNew(value), + ), ), - ), - const SizedBox(width: 6), - FilledButton.tonal( - onPressed: () => provider.saveFiltersNew(controller.text), - child: const Icon(IconsaxPlusLinear.save_2), - ), - ], - ) + const SizedBox(width: 6), + FilledButton.tonal( + onPressed: controller.text.isEmpty + ? null + : () { + provider.saveFiltersNew(controller.text); + }, + child: const Icon(IconsaxPlusLinear.save_2), + ), + ], + ); + }) else Text(context.localized.libraryFiltersLimitReached), ElevatedButton( diff --git a/lib/screens/library_search/widgets/library_views.dart b/lib/screens/library_search/widgets/library_views.dart index 5a2eee0..0c0713a 100644 --- a/lib/screens/library_search/widgets/library_views.dart +++ b/lib/screens/library_search/widgets/library_views.dart @@ -1,7 +1,16 @@ import 'dart:ui'; +import 'package:flutter/material.dart'; + import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; +import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:intl/intl.dart'; +import 'package:page_transition/page_transition.dart'; +import 'package:sliver_tools/sliver_tools.dart'; + import 'package:fladder/models/boxset_model.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/photos_model.dart'; @@ -10,22 +19,15 @@ import 'package:fladder/models/playlist_model.dart'; import 'package:fladder/providers/library_search_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart'; -import 'package:fladder/screens/shared/media/poster_grid.dart'; import 'package:fladder/screens/shared/media/poster_list_item.dart'; import 'package:fladder/screens/shared/media/poster_widget.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/string_extensions.dart'; +import 'package:fladder/util/theme_extensions.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import 'package:intl/intl.dart'; -import 'package:page_transition/page_transition.dart'; -import 'package:sliver_tools/sliver_tools.dart'; -import 'package:sticky_headers/sticky_headers/widget.dart'; final libraryViewTypeProvider = StateProvider((ref) { return LibraryViewTypes.grid; @@ -107,179 +109,139 @@ class LibraryViews extends ConsumerWidget { switch (ref.watch(libraryViewTypeProvider)) { case LibraryViewTypes.grid: - if (groupByType != GroupBy.none) { - final groupedItems = groupItemsBy(context, items, groupByType); - return SliverList.builder( - itemCount: groupedItems.length, + Widget createGrid(List items) { + return SliverGrid.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: posterSize.toInt(), + mainAxisSpacing: (8 * decimal) + 8, + crossAxisSpacing: (8 * decimal) + 8, + childAspectRatio: items.getMostCommonType.aspectRatio, + ), + itemCount: items.length, itemBuilder: (context, index) { - final name = groupedItems.keys.elementAt(index); - final group = groupedItems[name]; - if (group?.isEmpty ?? false || group == null) { - return Text(context.localized.empty); - } - return PosterGrid( - posters: group!, - name: name, - itemBuilder: (context, index) { - final item = group[index]; - return PosterWidget( - key: Key(item.id), - poster: group[index], - maxLines: 2, - heroTag: true, - subTitle: item.subTitle(sortingOptions), - excludeActions: excludeActions, - otherActions: otherActions(item), - selected: selected.contains(item), - onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), - onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), - onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), - onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), - ); - }, + final item = items[index]; + return PosterWidget( + key: Key(item.id), + poster: item, + maxLines: 2, + heroTag: true, + subTitle: item.subTitle(sortingOptions), + excludeActions: excludeActions, + otherActions: otherActions(item), + selected: selected.contains(item), + onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), + onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), + onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ); }, ); + } + + if (groupByType != GroupBy.none) { + final groupedItems = groupItemsBy(context, items, groupByType); + return MultiSliver( + children: groupedItems.entries.map( + (element) { + final name = element.key; + final group = element.value; + return stickyHeaderBuilder( + context, + header: name, + sliver: createGrid(group), + ); + }, + ).toList()); } else { return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 8), - sliver: SliverGrid.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: posterSize.toInt(), - mainAxisSpacing: (8 * decimal) + 8, - crossAxisSpacing: (8 * decimal) + 8, - childAspectRatio: AdaptiveLayout.poster(context).ratio, - ), - itemCount: items.length, - itemBuilder: (context, index) { - final item = items[index]; - return PosterWidget( - key: Key(item.id), - poster: item, - maxLines: 2, - heroTag: true, - subTitle: item.subTitle(sortingOptions), - excludeActions: excludeActions, - otherActions: otherActions(item), - selected: selected.contains(item), - onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), - onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), - onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), - onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), - ); - }, - ), + sliver: createGrid(items), ); } case LibraryViewTypes.list: - if (groupByType != GroupBy.none) { - final groupedItems = groupItemsBy(context, items, groupByType); + Widget listBuilder(List items) { return SliverList.builder( - itemCount: groupedItems.length, + itemCount: items.length, itemBuilder: (context, index) { - final name = groupedItems.keys.elementAt(index); - final group = groupedItems[name]; - if (group?.isEmpty ?? false) { - return Text(context.localized.empty); - } - return StickyHeader( - header: Text(name, style: Theme.of(context).textTheme.headlineSmall), - content: ListView.builder( - shrinkWrap: true, - padding: EdgeInsets.zero, - physics: const NeverScrollableScrollPhysics(), - itemCount: group?.length, - itemBuilder: (context, index) { - final poster = group![index]; - return PosterListItem( - key: Key(poster.id), - poster: poster, - subTitle: poster.subTitle(sortingOptions), - excludeActions: excludeActions, - otherActions: otherActions(poster), - selected: selected.contains(poster), - onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), - onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), - onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), - onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), - ); - }, - ), + final poster = items[index]; + return PosterListItem( + poster: poster, + selected: selected.contains(poster), + excludeActions: excludeActions, + otherActions: otherActions(poster), + subTitle: poster.subTitle(sortingOptions), + onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), + onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), + onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), + onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), ); }, ); } - return SliverList.builder( - itemCount: items.length, - itemBuilder: (context, index) { - final poster = items[index]; - return PosterListItem( - poster: poster, - selected: selected.contains(poster), - excludeActions: excludeActions, - otherActions: otherActions(poster), - subTitle: poster.subTitle(sortingOptions), - onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), - onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), - onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), - onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), - ); - }, - ); + if (groupByType != GroupBy.none) { + final groupedItems = groupItemsBy(context, items, groupByType); + return MultiSliver( + children: groupedItems.entries.map( + (element) { + final name = element.key; + final group = element.value; + return stickyHeaderBuilder( + context, + header: name, + sliver: listBuilder(group), + ); + }, + ).toList()); + } + return listBuilder(items); case LibraryViewTypes.masonry: if (groupByType != GroupBy.none) { final groupedItems = groupItemsBy(context, items, groupByType); - return SliverList.builder( - itemCount: groupedItems.length, - itemBuilder: (context, index) { - final name = groupedItems.keys.elementAt(index); - final group = groupedItems[name]; - if (group?.isEmpty ?? false) { - return Text(context.localized.empty); - } - return Padding( - padding: EdgeInsets.only(top: index == 0 ? 0 : 64.0), - child: StickyHeader( - header: Text(name, style: Theme.of(context).textTheme.headlineMedium), - overlapHeaders: true, - content: Padding( - padding: const EdgeInsets.only(top: 16.0), - child: MasonryGridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - mainAxisSpacing: (8 * decimal) + 8, - crossAxisSpacing: (8 * decimal) + 8, - gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: - (MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0)) - .toDouble() * - 20, - ), - itemCount: group!.length, - itemBuilder: (context, index) { - final item = group[index]; - return PosterWidget( - key: Key(item.id), - poster: item, - aspectRatio: item.primaryRatio, - selected: selected.contains(item), - inlineTitle: true, - heroTag: true, - subTitle: item.subTitle(sortingOptions), - excludeActions: excludeActions, - otherActions: otherActions(group[index]), - onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), - onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), - onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), - onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), - ); - }, - ), - )), + return MultiSliver( + children: groupedItems.entries.map( + (element) { + final name = element.key; + final group = element.value; + return stickyHeaderBuilder( + context, + header: name, + //MasonryGridView because SliverMasonryGrid breaks scrolling + sliver: SliverToBoxAdapter( + child: MasonryGridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: (8 * decimal) + 8, + crossAxisSpacing: (8 * decimal) + 8, + gridDelegate: SliverSimpleGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + (MediaQuery.sizeOf(context).width ~/ (lerpDouble(250, 75, posterSizeMultiplier) ?? 1.0)) + .toDouble() * + 12, + ), + itemCount: group.length, + itemBuilder: (context, index) { + final item = group[index]; + return PosterWidget( + key: Key(item.id), + poster: item, + aspectRatio: item.primaryRatio, + selected: selected.contains(item), + inlineTitle: true, + heroTag: true, + subTitle: item.subTitle(sortingOptions), + excludeActions: excludeActions, + otherActions: otherActions(group[index]), + onUserDataChanged: (id, newData) => libraryProvider.updateUserData(id, newData), + onItemRemoved: (oldItem) => libraryProvider.removeFromPosters([oldItem.id]), + onItemUpdated: (newItem) => libraryProvider.updateItem(newItem), + onPressed: (action, item) async => onItemPressed(action, key, item, ref, context), + ); + }, + ), + ), ); }, - ); + ).toList()); } else { return SliverMasonryGrid.count( mainAxisSpacing: (8 * decimal) + 8, @@ -309,6 +271,36 @@ class LibraryViews extends ConsumerWidget { } } + SliverStickyHeader stickyHeaderBuilder( + BuildContext context, { + required String header, + Widget? sliver, + }) { + return SliverStickyHeader( + header: Container( + height: 50, + alignment: Alignment.centerLeft, + child: Transform.translate( + offset: const Offset(-20, 0), + child: Container( + decoration: BoxDecoration( + color: context.colors.surface.withValues(alpha: 0.9), + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text( + header, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + ), + ), + ), + sliver: sliver, + ); + } + Map> groupItemsBy(BuildContext context, List list, GroupBy groupOption) { switch (groupOption) { case GroupBy.dateAdded: diff --git a/lib/screens/library_search/widgets/suggestion_search_bar.dart b/lib/screens/library_search/widgets/suggestion_search_bar.dart index b52b286..8acb64d 100644 --- a/lib/screens/library_search/widgets/suggestion_search_bar.dart +++ b/lib/screens/library_search/widgets/suggestion_search_bar.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_typeahead/flutter_typeahead.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:page_transition/page_transition.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -65,6 +65,9 @@ class _SearchBarState extends ConsumerState { }); return Card( elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: FladderTheme.largeShape.borderRadius, + ), shadowColor: Colors.transparent, child: TypeAheadField( focusNode: focusNode, @@ -80,7 +83,7 @@ class _SearchBarState extends ConsumerState { decorationBuilder: (context, child) => DecoratedBox( decoration: BoxDecoration( color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: FladderTheme.defaultShape.borderRadius, + borderRadius: FladderTheme.largeShape.borderRadius, ), child: child, ), @@ -133,39 +136,45 @@ class _SearchBarState extends ConsumerState { } }, contentPadding: const EdgeInsets.symmetric(horizontal: 8), - title: SizedBox( - height: 50, - child: Row( - children: [ - Card( - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), - child: AspectRatio( - aspectRatio: 0.8, - child: FladderImage( - image: suggestion.images?.primary, - fit: BoxFit.cover, + title: ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 50, + maxHeight: 65, + ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + child: AspectRatio( + aspectRatio: 0.8, + child: FladderImage( + image: suggestion.images?.primary, + fit: BoxFit.cover, + ), ), ), - ), - const SizedBox(width: 8), - Flexible( - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Flexible( - child: Text( - suggestion.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - )), - if (suggestion.overview.yearAired.toString().isNotEmpty) + const SizedBox(width: 8), + Flexible( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ Flexible( - child: - Opacity(opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))), - ], + child: Text( + suggestion.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + )), + if (suggestion.overview.yearAired.toString().isNotEmpty) + Flexible( + child: Opacity( + opacity: 0.45, child: Text(suggestion.overview.yearAired?.toString() ?? ""))), + ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/screens/login/login_screen.dart b/lib/screens/login/login_screen.dart index 3b03518..891d814 100644 --- a/lib/screens/login/login_screen.dart +++ b/lib/screens/login/login_screen.dart @@ -20,7 +20,7 @@ import 'package:fladder/screens/shared/fladder_logo.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/outlined_text_field.dart'; import 'package:fladder/screens/shared/passcode_input.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/auth_service.dart'; import 'package:fladder/util/fladder_config.dart'; import 'package:fladder/util/list_padding.dart'; diff --git a/lib/screens/login/login_user_grid.dart b/lib/screens/login/login_user_grid.dart index a2f6684..31c2b9b 100644 --- a/lib/screens/login/login_user_grid.dart +++ b/lib/screens/login/login_user_grid.dart @@ -135,7 +135,7 @@ class _CardHolder extends StatelessWidget { return Card( elevation: 1, shadowColor: Colors.transparent, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, margin: EdgeInsets.zero, child: ConstrainedBox( constraints: const BoxConstraints(maxHeight: 150, maxWidth: 150), diff --git a/lib/screens/login/widgets/login_icon.dart b/lib/screens/login/widgets/login_icon.dart index d0f374e..30114f2 100644 --- a/lib/screens/login/widgets/login_icon.dart +++ b/lib/screens/login/widgets/login_icon.dart @@ -24,7 +24,7 @@ class LoginIcon extends ConsumerWidget { aspectRatio: 1.0, child: Card( elevation: 1, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, margin: EdgeInsets.zero, child: Stack( children: [ diff --git a/lib/screens/metadata/edit_item.dart b/lib/screens/metadata/edit_item.dart index 9ef4a82..c7f8566 100644 --- a/lib/screens/metadata/edit_item.dart +++ b/lib/screens/metadata/edit_item.dart @@ -5,7 +5,7 @@ import 'package:fladder/providers/edit_item_provider.dart'; import 'package:fladder/screens/metadata/edit_screens/edit_fields.dart'; import 'package:fladder/screens/metadata/edit_screens/edit_image_content.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:flutter/material.dart'; diff --git a/lib/screens/metadata/edit_screens/edit_image_content.dart b/lib/screens/metadata/edit_screens/edit_image_content.dart index b5fa60c..6d2abc0 100644 --- a/lib/screens/metadata/edit_screens/edit_image_content.dart +++ b/lib/screens/metadata/edit_screens/edit_image_content.dart @@ -9,7 +9,7 @@ import 'package:fladder/providers/edit_item_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/shared/file_picker.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; class EditImageContent extends ConsumerStatefulWidget { final ImageType type; diff --git a/lib/screens/metadata/identifty_screen.dart b/lib/screens/metadata/identifty_screen.dart index 9c662a2..1672150 100644 --- a/lib/screens/metadata/identifty_screen.dart +++ b/lib/screens/metadata/identifty_screen.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; 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/models/item_base_model.dart'; import 'package:fladder/providers/items/identify_provider.dart'; @@ -51,11 +51,10 @@ class _IdentifyScreenState extends ConsumerState with TickerProv final state = ref.watch(provider); final posters = state.results; final processing = state.processing; - return ActionContent( - showDividers: false, - title: Container( - color: Theme.of(context).colorScheme.surface, - child: Column( + return Card( + child: ActionContent( + showDividers: false, + title: Column( mainAxisSize: MainAxisSize.min, children: [ Row( @@ -89,137 +88,137 @@ class _IdentifyScreenState extends ConsumerState with TickerProv ) ], ), - ), - child: TabBarView( - controller: tabController, - children: [ - inputFields(state), - if (posters.isEmpty) - Center( - child: processing - ? const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round) - : Text(context.localized.noResults), - ) - else - Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text(context.localized.replaceAllImages), - const SizedBox(width: 16), - Switch.adaptive( - value: state.replaceAllImages, - onChanged: (value) { - ref.read(provider.notifier).update((state) => state.copyWith(replaceAllImages: value)); - }, - ), - ], - ), - Flexible( - child: ListView( - shrinkWrap: true, - children: posters - .map((result) => ListTile( - title: Row( - children: [ - SizedBox( - width: 75, - child: Card( - child: CachedNetworkImage( - imageUrl: result.imageUrl ?? "", - errorWidget: (context, url, error) => SizedBox( - height: 75, - child: Card( - child: Center( - child: Text(result.name?.getInitials() ?? ""), + child: TabBarView( + controller: tabController, + children: [ + inputFields(state), + if (posters.isEmpty) + Center( + child: processing + ? const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round) + : Text(context.localized.noResults), + ) + else + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text(context.localized.replaceAllImages), + const SizedBox(width: 16), + Switch.adaptive( + value: state.replaceAllImages, + onChanged: (value) { + ref.read(provider.notifier).update((state) => state.copyWith(replaceAllImages: value)); + }, + ), + ], + ), + Flexible( + child: ListView( + shrinkWrap: true, + children: posters + .map((result) => ListTile( + title: Row( + children: [ + SizedBox( + width: 75, + child: Card( + child: CachedNetworkImage( + imageUrl: result.imageUrl ?? "", + errorWidget: (context, url, error) => SizedBox( + height: 75, + child: Card( + child: Center( + child: Text(result.name?.getInitials() ?? ""), + ), ), ), ), ), ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "${result.name ?? ""}${result.productionYear != null ? "(${result.productionYear})" : ""}"), - Opacity(opacity: 0.65, child: Text(result.providerIds?.keys.join(',') ?? "")) - ], + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${result.name ?? ""}${result.productionYear != null ? "(${result.productionYear})" : ""}"), + Opacity(opacity: 0.65, child: Text(result.providerIds?.keys.join(',') ?? "")) + ], + ), ), - ), - Tooltip( - message: context.localized.openWebLink, - child: IconButton( - onPressed: () { - final providerKeyEntry = result.providerIds?.entries.first; - final providerKey = providerKeyEntry?.key; - final providerValue = providerKeyEntry?.value; + Tooltip( + message: context.localized.openWebLink, + child: IconButton( + onPressed: () { + final providerKeyEntry = result.providerIds?.entries.first; + final providerKey = providerKeyEntry?.key; + final providerValue = providerKeyEntry?.value; - final externalId = state.externalIds - .firstWhereOrNull((element) => element.key == providerKey) - // ignore: deprecated_member_use_from_same_package - ?.urlFormatString; + final externalId = state.externalIds + .firstWhereOrNull((element) => element.key == providerKey) + // ignore: deprecated_member_use_from_same_package + ?.urlFormatString; - final url = externalId?.replaceAll("{0}", providerValue?.toString() ?? ""); + final url = externalId?.replaceAll("{0}", providerValue?.toString() ?? ""); - launchUrl(context, url ?? ""); - }, - icon: const Icon(Icons.launch_rounded)), - ), - Tooltip( - message: "Select result", - child: IconButton( - onPressed: !processing - ? () async { - final response = await ref.read(provider.notifier).setIdentity(result); - if (response?.isSuccessful == true) { - fladderSnackbar(context, - title: context.localized.setIdentityTo(result.name ?? "")); - } else { - fladderSnackbarResponse(context, response, - altTitle: context.localized.somethingWentWrong); + launchUrl(context, url ?? ""); + }, + icon: const Icon(Icons.launch_rounded)), + ), + Tooltip( + message: "Select result", + child: IconButton( + onPressed: !processing + ? () async { + final response = await ref.read(provider.notifier).setIdentity(result); + if (response?.isSuccessful == true) { + fladderSnackbar(context, + title: context.localized.setIdentityTo(result.name ?? "")); + } else { + fladderSnackbarResponse(context, response, + altTitle: context.localized.somethingWentWrong); + } + + Navigator.of(context).pop(); } - - Navigator.of(context).pop(); - } - : null, - icon: const Icon(Icons.save_alt_rounded), - ), - ) - ], - ), - )) - .toList(), + : null, + icon: const Icon(Icons.save_alt_rounded), + ), + ) + ], + ), + )) + .toList(), + ), ), - ), - ], - ) + ], + ) + ], + ), + actions: [ + ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)), + const SizedBox(width: 16), + FilledButton( + onPressed: !processing + ? () async { + await ref.read(provider.notifier).remoteSearch(); + tabController.animateTo(1); + } + : null, + child: processing + ? SizedBox( + width: 21, + height: 21, + child: CircularProgressIndicator.adaptive( + backgroundColor: Theme.of(context).colorScheme.onPrimary, strokeCap: StrokeCap.round), + ) + : Text(context.localized.search), + ), ], ), - actions: [ - ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)), - const SizedBox(width: 16), - FilledButton( - onPressed: !processing - ? () async { - await ref.read(provider.notifier).remoteSearch(); - tabController.animateTo(1); - } - : null, - child: processing - ? SizedBox( - width: 21, - height: 21, - child: CircularProgressIndicator.adaptive( - backgroundColor: Theme.of(context).colorScheme.onPrimary, strokeCap: StrokeCap.round), - ) - : Text(context.localized.search), - ), - ], ); } @@ -248,7 +247,7 @@ class _IdentifyScreenState extends ConsumerState with TickerProv final controller = currentKey == "Name" ? currentController : TextEditingController(text: state.searchString); return FocusedOutlinedTextField( - label: context.localized.userName, + label: context.localized.name, controller: controller, onChanged: (value) { currentController = controller; diff --git a/lib/screens/metadata/info_screen.dart b/lib/screens/metadata/info_screen.dart index 74b4fb1..c5eabed 100644 --- a/lib/screens/metadata/info_screen.dart +++ b/lib/screens/metadata/info_screen.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.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/models/information_model.dart'; import 'package:fladder/models/item_base_model.dart'; diff --git a/lib/screens/metadata/refresh_metadata.dart b/lib/screens/metadata/refresh_metadata.dart index 9494cbc..8fe9a08 100644 --- a/lib/screens/metadata/refresh_metadata.dart +++ b/lib/screens/metadata/refresh_metadata.dart @@ -6,7 +6,7 @@ import 'package:fladder/jellyfin/enum_models.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; diff --git a/lib/screens/photo_viewer/photo_viewer_controls.dart b/lib/screens/photo_viewer/photo_viewer_controls.dart index 60c10c9..5aa10c8 100644 --- a/lib/screens/photo_viewer/photo_viewer_controls.dart +++ b/lib/screens/photo_viewer/photo_viewer_controls.dart @@ -15,14 +15,13 @@ import 'package:fladder/providers/settings/photo_view_settings_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/input_fields.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/input_handler.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/throttler.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; import 'package:fladder/widgets/shared/elevated_icon.dart'; -import 'package:fladder/widgets/shared/full_screen_button.dart' - if (dart.library.html) 'package:fladder/widgets/shared/full_screen_button_web.dart'; import 'package:fladder/widgets/shared/progress_floating_button.dart'; class PhotoViewerControls extends ConsumerStatefulWidget { @@ -131,7 +130,7 @@ class _PhotoViewerControllsState extends ConsumerState with @override void dispose() { timerController.dispose(); - closeFullScreen(); + fullScreenHelper.closeFullScreen(ref); windowManager.removeListener(this); super.dispose(); } @@ -349,11 +348,11 @@ class _PhotoViewerControllsState extends ConsumerState with Future sharePhoto() async { final file = await DefaultCacheManager().getSingleFile(widget.photo.downloadPath(ref)); - await Share.shareXFiles([ + await SharePlus.instance.share(ShareParams(files: [ XFile( file.path, ), - ]); + ])); await file.delete(); } } diff --git a/lib/screens/photo_viewer/photo_viewer_screen.dart b/lib/screens/photo_viewer/photo_viewer_screen.dart index f8b8d6d..a12f9f2 100644 --- a/lib/screens/photo_viewer/photo_viewer_screen.dart +++ b/lib/screens/photo_viewer/photo_viewer_screen.dart @@ -16,7 +16,7 @@ import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/photo_viewer/photo_viewer_controls.dart'; import 'package:fladder/screens/photo_viewer/simple_video_player.dart'; import 'package:fladder/screens/shared/default_title_bar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/custom_cache_manager.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; diff --git a/lib/screens/settings/about_settings_page.dart b/lib/screens/settings/about_settings_page.dart index ac24bf4..ecc46e4 100644 --- a/lib/screens/settings/about_settings_page.dart +++ b/lib/screens/settings/about_settings_page.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/screens/crash_screen/crash_screen.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; +import 'package:fladder/screens/settings/widgets/settings_update_information.dart'; import 'package:fladder/screens/shared/fladder_icon.dart'; import 'package:fladder/screens/shared/fladder_logo.dart'; import 'package:fladder/screens/shared/media/external_urls.dart'; @@ -42,75 +43,82 @@ class AboutSettingsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final applicationInfo = ref.watch(applicationInfoProvider); - return Card( - child: SettingsScaffold( - label: "", - items: [ - const FladderLogo(), - Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)), - Text(context.localized.aboutBuild(applicationInfo.buildNumber)), - const SizedBox(height: 16), - Text(context.localized.aboutCreatedBy), - ], + + return SettingsScaffold( + label: "", + items: [ + const FladderLogo(), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(context.localized.aboutVersion(applicationInfo.versionAndPlatform)), + Text(context.localized.aboutBuild(applicationInfo.buildNumber)), + const SizedBox(height: 16), + Text(context.localized.aboutCreatedBy), + ], + ), + const FractionallySizedBox( + widthFactor: 0.25, + child: Divider( + indent: 16, + endIndent: 16, ), - const Divider(), - Column( - children: [ - Text( - context.localized.aboutSocials, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 6), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: socials - .map( - (e) => IconButton.filledTonal( - onPressed: () => launchUrl(context, e.url), - icon: Column( - children: [ - Icon(e.icon), - Text(e.label), - ], - ), + ), + Column( + children: [ + Text( + context.localized.aboutSocials, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: socials + .map( + (e) => IconButton.filledTonal( + onPressed: () => launchUrl(context, e.url), + icon: Column( + children: [ + Icon(e.icon), + Text(e.label), + ], ), - ) - .toList() - .addInBetween(const SizedBox(width: 16)), - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FilledButton.tonal( - onPressed: () => showLicensePage( - context: context, - applicationIcon: const FladderIcon(size: 55), - applicationVersion: applicationInfo.versionPlatformBuild, - applicationLegalese: "DonutWare", - ), - child: Text(context.localized.aboutLicenses), - ) - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FilledButton.tonal( - onPressed: () => showDialog( - context: context, - builder: (context) => const CrashScreen(), - ), - child: Text(context.localized.errorLogs), - ) - ], - ), - ].addInBetween(const SizedBox(height: 16)), - ), + ), + ) + .toList() + .addInBetween(const SizedBox(width: 16)), + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FilledButton.tonal( + onPressed: () => showLicensePage( + context: context, + applicationIcon: const FladderIcon(size: 55), + applicationVersion: applicationInfo.versionPlatformBuild, + applicationLegalese: "DonutWare", + useRootNavigator: true, + ), + child: Text(context.localized.aboutLicenses), + ) + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FilledButton.tonal( + onPressed: () => showDialog( + context: context, + builder: (context) => const CrashScreen(), + ), + child: Text(context.localized.errorLogs), + ) + ], + ), + const SettingsUpdateInformation(), + ].addInBetween(const SizedBox(height: 16)), ); } } diff --git a/lib/screens/settings/client_sections/client_settings_advanced.dart b/lib/screens/settings/client_sections/client_settings_advanced.dart index a0f1272..a736658 100644 --- a/lib/screens/settings/client_sections/client_settings_advanced.dart +++ b/lib/screens/settings/client_sections/client_settings_advanced.dart @@ -2,79 +2,83 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/home_settings_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/option_dialogue.dart'; List buildClientSettingsAdvanced(BuildContext context, WidgetRef ref) { - return [ + return settingsListGroup( + context, SettingsLabelDivider(label: context.localized.advanced), - SettingsListTile( - label: Text(context.localized.settingsLayoutSizesTitle), - subLabel: Text(context.localized.settingsLayoutSizesDesc), - onTap: () async { - final newItems = await openMultiSelectOptions( - context, - label: context.localized.settingsLayoutSizesTitle, - items: ViewSize.values, - allowMultiSelection: true, - selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())), - itemBuilder: (type, selected, tap) => CheckboxListTile( - contentPadding: EdgeInsets.zero, - value: selected, - onChanged: (value) => tap(), - title: Text(type.label(context)), + [ + SettingsListTile( + label: Text(context.localized.settingsLayoutSizesTitle), + subLabel: Text(context.localized.settingsLayoutSizesDesc), + onTap: () async { + final newItems = await openMultiSelectOptions( + context, + label: context.localized.settingsLayoutSizesTitle, + items: ViewSize.values, + allowMultiSelection: true, + selected: ref.read(homeSettingsProvider.select((value) => value.layoutStates.toList())), + itemBuilder: (type, selected, tap) => CheckboxListTile( + contentPadding: EdgeInsets.zero, + value: selected, + onChanged: (value) => tap(), + title: Text(type.label(context)), + ), + ); + ref.read(homeSettingsProvider.notifier).setViewSize(newItems.toSet()); + }, + trailing: Card( + color: Theme.of(context).colorScheme.primaryContainer, + shadowColor: Colors.transparent, + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(ref + .watch(homeSettingsProvider.select((value) => value.layoutStates.toList())) + .map((e) => e.label(context)) + .join(', ')), ), - ); - ref.read(homeSettingsProvider.notifier).setViewSize(newItems.toSet()); - }, - trailing: Card( - color: Theme.of(context).colorScheme.primaryContainer, - shadowColor: Colors.transparent, - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(ref - .watch(homeSettingsProvider.select((value) => value.layoutStates.toList())) - .map((e) => e.label(context)) - .join(', ')), ), ), - ), - SettingsListTile( - label: Text(context.localized.settingsLayoutModesTitle), - subLabel: Text(context.localized.settingsLayoutModesDesc), - onTap: () async { - final newItems = await openMultiSelectOptions( - context, - label: context.localized.settingsLayoutModesTitle, - items: LayoutMode.values, - allowMultiSelection: true, - selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())), - itemBuilder: (type, selected, tap) => CheckboxListTile( - contentPadding: EdgeInsets.zero, - value: selected, - onChanged: (value) => tap(), - title: Text(type.label(context)), + SettingsListTile( + label: Text(context.localized.settingsLayoutModesTitle), + subLabel: Text(context.localized.settingsLayoutModesDesc), + onTap: () async { + final newItems = await openMultiSelectOptions( + context, + label: context.localized.settingsLayoutModesTitle, + items: LayoutMode.values, + allowMultiSelection: true, + selected: ref.read(homeSettingsProvider.select((value) => value.screenLayouts.toList())), + itemBuilder: (type, selected, tap) => CheckboxListTile( + contentPadding: EdgeInsets.zero, + value: selected, + onChanged: (value) => tap(), + title: Text(type.label(context)), + ), + ); + ref.read(homeSettingsProvider.notifier).setLayoutModes(newItems.toSet()); + }, + trailing: Card( + color: Theme.of(context).colorScheme.primaryContainer, + shadowColor: Colors.transparent, + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text(ref + .watch(homeSettingsProvider.select((value) => value.screenLayouts.toList())) + .map((e) => e.label(context)) + .join(', ')), ), - ); - ref.read(homeSettingsProvider.notifier).setLayoutModes(newItems.toSet()); - }, - trailing: Card( - color: Theme.of(context).colorScheme.primaryContainer, - shadowColor: Colors.transparent, - elevation: 0, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text(ref - .watch(homeSettingsProvider.select((value) => value.screenLayouts.toList())) - .map((e) => e.label(context)) - .join(', ')), ), ), - ), - ]; + ], + ); } diff --git a/lib/screens/settings/client_sections/client_settings_dashboard.dart b/lib/screens/settings/client_sections/client_settings_dashboard.dart index 8377695..fc31f9c 100644 --- a/lib/screens/settings/client_sections/client_settings_dashboard.dart +++ b/lib/screens/settings/client_sections/client_settings_dashboard.dart @@ -7,89 +7,92 @@ import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/home_settings_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; List buildClientSettingsDashboard(BuildContext context, WidgetRef ref) { final clientSettings = ref.watch(clientSettingsProvider); - return [ + return settingsListGroup( + context, SettingsLabelDivider(label: context.localized.dashboard), - SettingsListTile( - label: Text(context.localized.settingsHomeBannerTitle), - subLabel: Text(context.localized.settingsHomeBannerDescription), - trailing: EnumBox( - current: ref.watch( - homeSettingsProvider.select( - (value) => value.homeBanner.label(context), - ), - ), - itemBuilder: (context) => HomeBanner.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => - ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)), - ), - ) - .toList(), - ), - ), - if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide) + [ SettingsListTile( - label: Text(context.localized.settingsHomeBannerInformationTitle), - subLabel: Text(context.localized.settingsHomeBannerInformationDesc), + label: Text(context.localized.settingsHomeBannerTitle), + subLabel: Text(context.localized.settingsHomeBannerDescription), trailing: EnumBox( current: ref.watch( - homeSettingsProvider.select((value) => value.carouselSettings.label(context)), + homeSettingsProvider.select( + (value) => value.homeBanner.label(context), + ), ), - itemBuilder: (context) => HomeCarouselSettings.values + itemBuilder: (context) => HomeBanner.values .map( (entry) => PopupMenuItem( value: entry, child: Text(entry.label(context)), - onTap: () => ref - .read(homeSettingsProvider.notifier) - .update((context) => context.copyWith(carouselSettings: entry)), + onTap: () => + ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(homeBanner: entry)), ), ) .toList(), ), ), - SettingsListTile( - label: Text(context.localized.settingsHomeNextUpTitle), - subLabel: Text(context.localized.settingsHomeNextUpDesc), - trailing: EnumBox( - current: ref.watch( - homeSettingsProvider.select( - (value) => value.nextUp.label(context), + if (ref.watch(homeSettingsProvider.select((value) => value.homeBanner)) != HomeBanner.hide) + SettingsListTile( + label: Text(context.localized.settingsHomeBannerInformationTitle), + subLabel: Text(context.localized.settingsHomeBannerInformationDesc), + trailing: EnumBox( + current: ref.watch( + homeSettingsProvider.select((value) => value.carouselSettings.label(context)), + ), + itemBuilder: (context) => HomeCarouselSettings.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => ref + .read(homeSettingsProvider.notifier) + .update((context) => context.copyWith(carouselSettings: entry)), + ), + ) + .toList(), ), ), - itemBuilder: (context) => HomeNextUp.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => - ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)), - ), - ) - .toList(), + SettingsListTile( + label: Text(context.localized.settingsHomeNextUpTitle), + subLabel: Text(context.localized.settingsHomeNextUpDesc), + trailing: EnumBox( + current: ref.watch( + homeSettingsProvider.select( + (value) => value.nextUp.label(context), + ), + ), + itemBuilder: (context) => HomeNextUp.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => + ref.read(homeSettingsProvider.notifier).update((context) => context.copyWith(nextUp: entry)), + ), + ) + .toList(), + ), ), - ), - SettingsListTile( - label: Text(context.localized.clientSettingsShowAllCollectionsTitle), - subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc), - onTap: () => ref - .read(clientSettingsProvider.notifier) - .update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)), - trailing: Switch( - value: clientSettings.showAllCollectionTypes, - onChanged: (value) => ref + SettingsListTile( + label: Text(context.localized.clientSettingsShowAllCollectionsTitle), + subLabel: Text(context.localized.clientSettingsShowAllCollectionsDesc), + onTap: () => ref .read(clientSettingsProvider.notifier) - .update((current) => current.copyWith(showAllCollectionTypes: value)), + .update((current) => current.copyWith(showAllCollectionTypes: !current.showAllCollectionTypes)), + trailing: Switch( + value: clientSettings.showAllCollectionTypes, + onChanged: (value) => ref + .read(clientSettingsProvider.notifier) + .update((current) => current.copyWith(showAllCollectionTypes: value)), + ), ), - ), - const Divider(), - ]; + ], + ); } diff --git a/lib/screens/settings/client_sections/client_settings_download.dart b/lib/screens/settings/client_sections/client_settings_download.dart index c54ee32..73ddde8 100644 --- a/lib/screens/settings/client_sections/client_settings_download.dart +++ b/lib/screens/settings/client_sections/client_settings_download.dart @@ -1,17 +1,20 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/providers/sync/background_download_provider.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/screens/shared/default_alert_dialog.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/screens/shared/input_fields.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/size_formatting.dart'; @@ -22,101 +25,122 @@ List buildClientSettingsDownload(BuildContext context, WidgetRef ref, Fu return [ if (canSync && !kIsWeb) ...[ - SettingsLabelDivider(label: context.localized.downloadsTitle), - if (AdaptiveLayout.of(context).isDesktop) ...[ - SettingsListTile( - label: Text(context.localized.downloadsPath), - subLabel: Text(currentFolder ?? "-"), - onTap: currentFolder != null - ? () async => await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.localized.pathEditTitle), - content: Text(context.localized.pathEditDesc), - actions: [ - ElevatedButton( - onPressed: () async { - String? selectedDirectory = await FilePicker.platform.getDirectoryPath( - dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); - if (selectedDirectory != null) { - ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); - } - Navigator.of(context).pop(); - }, - child: Text(context.localized.change), - ) - ], + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.downloadsTitle), [ + if (AdaptiveLayout.of(context).isDesktop) ...[ + SettingsListTile( + label: Text(context.localized.downloadsPath), + subLabel: Text(currentFolder ?? "-"), + onTap: currentFolder != null + ? () async => await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.localized.pathEditTitle), + content: Text(context.localized.pathEditDesc), + actions: [ + ElevatedButton( + onPressed: () async { + String? selectedDirectory = await FilePicker.platform.getDirectoryPath( + dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); + if (selectedDirectory != null) { + ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); + } + Navigator.of(context).pop(); + }, + child: Text(context.localized.change), + ) + ], + ), + ) + : () async { + String? selectedDirectory = await FilePicker.platform.getDirectoryPath( + dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); + if (selectedDirectory != null) { + ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); + } + }, + trailing: currentFolder?.isNotEmpty == true + ? IconButton( + color: Theme.of(context).colorScheme.error, + onPressed: () async => await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.localized.pathClearTitle), + content: Text(context.localized.pathEditDesc), + actions: [ + ElevatedButton( + onPressed: () { + ref.read(clientSettingsProvider.notifier).setSyncPath(null); + Navigator.of(context).pop(); + }, + child: Text(context.localized.clear), + ) + ], + ), ), + icon: const Icon(IconsaxPlusLinear.folder_minus), ) - : () async { - String? selectedDirectory = await FilePicker.platform - .getDirectoryPath(dialogTitle: context.localized.pathEditSelect, initialDirectory: currentFolder); - if (selectedDirectory != null) { - ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); + : null, + ), + ], + FutureBuilder( + future: ref.watch(syncProvider.notifier).directorySize, + builder: (context, snapshot) { + final data = snapshot.data ?? 0; + return SettingsListTile( + label: Text(context.localized.downloadsSyncedData), + subLabel: Text(data.byteFormat ?? ""), + trailing: FilledButton( + onPressed: () { + showDefaultAlertDialog( + context, + context.localized.downloadsClearTitle, + context.localized.downloadsClearDesc, + (context) async { + await ref.read(syncProvider.notifier).clear(); + setState(() {}); + Navigator.of(context).pop(); + }, + context.localized.clear, + (context) => Navigator.of(context).pop(), + context.localized.cancel, + ); + }, + child: Text(context.localized.clear), + ), + ); + }, + ), + SettingsListTile( + label: Text(context.localized.clientSettingsRequireWifiTitle), + subLabel: Text(context.localized.clientSettingsRequireWifiDesc), + onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi), + trailing: Switch( + value: clientSettings.requireWifi, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value), + ), + ), + SettingsListTile( + label: Text(context.localized.maxConcurrentDownloadsTitle), + subLabel: Text(context.localized.maxConcurrentDownloadsDesc), + trailing: SizedBox( + width: 100, + child: IntInputField( + controller: TextEditingController(text: clientSettings.maxConcurrentDownloads.toString()), + onSubmitted: (value) { + if (value != null) { + ref.read(clientSettingsProvider.notifier).update( + (current) => current.copyWith( + maxConcurrentDownloads: value, + ), + ); + + ref.read(backgroundDownloaderProvider.notifier).setMaxConcurrent(value); } }, - trailing: currentFolder?.isNotEmpty == true - ? IconButton( - color: Theme.of(context).colorScheme.error, - onPressed: () async => await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.localized.pathClearTitle), - content: Text(context.localized.pathEditDesc), - actions: [ - ElevatedButton( - onPressed: () { - ref.read(clientSettingsProvider.notifier).setSyncPath(null); - Navigator.of(context).pop(); - }, - child: Text(context.localized.clear), - ) - ], - ), - ), - icon: const Icon(IconsaxPlusLinear.folder_minus), - ) - : null, + )), ), - ], - FutureBuilder( - future: ref.watch(syncProvider.notifier).directorySize, - builder: (context, snapshot) { - final data = snapshot.data ?? 0; - return SettingsListTile( - label: Text(context.localized.downloadsSyncedData), - subLabel: Text(data.byteFormat ?? ""), - trailing: FilledButton( - onPressed: () { - showDefaultAlertDialog( - context, - context.localized.downloadsClearTitle, - context.localized.downloadsClearDesc, - (context) async { - await ref.read(syncProvider.notifier).clear(); - setState(() {}); - Navigator.of(context).pop(); - }, - context.localized.clear, - (context) => Navigator.of(context).pop(), - context.localized.cancel, - ); - }, - child: Text(context.localized.clear), - ), - ); - }, - ), - SettingsListTile( - label: Text(context.localized.clientSettingsRequireWifiTitle), - subLabel: Text(context.localized.clientSettingsRequireWifiDesc), - onTap: () => ref.read(clientSettingsProvider.notifier).setRequireWifi(!clientSettings.requireWifi), - trailing: Switch( - value: clientSettings.requireWifi, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setRequireWifi(value), - ), - ), - const Divider(), + ]), + const SizedBox(height: 12), ], ]; } diff --git a/lib/screens/settings/client_sections/client_settings_theme.dart b/lib/screens/settings/client_sections/client_settings_theme.dart index de3b055..cbef7c3 100644 --- a/lib/screens/settings/client_sections/client_settings_theme.dart +++ b/lib/screens/settings/client_sections/client_settings_theme.dart @@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/util/color_extensions.dart'; import 'package:fladder/util/custom_color_themes.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -13,8 +14,7 @@ import 'package:fladder/util/theme_mode_extension.dart'; List buildClientSettingsTheme(BuildContext context, WidgetRef ref) { final clientSettings = ref.watch(clientSettingsProvider); - return [ - SettingsLabelDivider(label: context.localized.theme), + return settingsListGroup(context, SettingsLabelDivider(label: context.localized.theme), [ SettingsListTile( label: Text(context.localized.mode), subLabel: Text(clientSettings.themeMode.label(context)), @@ -107,6 +107,5 @@ List buildClientSettingsTheme(BuildContext context, WidgetRef ref) { onChanged: (value) => ref.read(clientSettingsProvider.notifier).setAmoledBlack(value), ), ), - const Divider(), - ]; + ]); } diff --git a/lib/screens/settings/client_sections/client_settings_visual.dart b/lib/screens/settings/client_sections/client_settings_visual.dart index b5578df..5666573 100644 --- a/lib/screens/settings/client_sections/client_settings_visual.dart +++ b/lib/screens/settings/client_sections/client_settings_visual.dart @@ -1,15 +1,14 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/l10n/generated/app_localizations.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/screens/shared/input_fields.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; import 'package:fladder/widgets/shared/fladder_slider.dart'; @@ -22,136 +21,149 @@ List buildClientSettingsVisual( ) { final clientSettings = ref.watch(clientSettingsProvider); Locale currentLocale = WidgetsBinding.instance.platformDispatcher.locale; - return [ + return settingsListGroup( + context, SettingsLabelDivider(label: context.localized.settingsVisual), - SettingsListTile( - label: Text(context.localized.displayLanguage), - trailing: Localizations.override( - context: context, - locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))), - child: Builder(builder: (context) { - String language = "Unknown"; - try { - language = context.localized.nativeName; - } catch (e) { - log(e.toString()); - } - return EnumBox( - current: language, - itemBuilder: (context) { - return [ - ...AppLocalizations.supportedLocales.map( - (entry) => PopupMenuItem( - value: entry, - child: Localizations.override( - context: context, - locale: entry, - child: Builder(builder: (context) { - return Text("${context.localized.nativeName} (${entry.languageCode.toUpperCase()})"); - }), + [ + SettingsListTile( + label: Text(context.localized.displayLanguage), + trailing: Localizations.override( + context: context, + locale: ref.watch(clientSettingsProvider.select((value) => (value.selectedLocale ?? currentLocale))), + child: Builder(builder: (context) { + String language = "English"; + try { + language = context.localized.nativeName; + } catch (_) {} + return EnumBox( + current: language, + itemBuilder: (context) { + return [ + ...AppLocalizations.supportedLocales.map( + (entry) => PopupMenuItem( + value: entry, + child: Localizations.override( + context: context, + locale: entry, + child: Builder(builder: (context) { + return Text("${context.localized.nativeName} (${entry.toDisplayCode()})"); + }), + ), + onTap: () => ref + .read(clientSettingsProvider.notifier) + .update((state) => state.copyWith(selectedLocale: entry)), ), - onTap: () => ref - .read(clientSettingsProvider.notifier) - .update((state) => state.copyWith(selectedLocale: entry)), + ) + ]; + }, + ); + }), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsBlurredPlaceholderTitle), + subLabel: Text(context.localized.settingsBlurredPlaceholderDesc), + onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders), + trailing: Switch( + value: clientSettings.blurPlaceHolders, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsBlurEpisodesTitle), + subLabel: Text(context.localized.settingsBlurEpisodesDesc), + onTap: () => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes), + trailing: Switch( + value: clientSettings.blurUpcomingEpisodes, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsEnableOsMediaControls), + subLabel: Text(context.localized.settingsEnableOsMediaControlsDesc), + onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys), + trailing: Switch( + value: clientSettings.enableMediaKeys, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value), + ), + ), + SettingsListTile( + label: Text(context.localized.enableBackgroundPostersTitle), + subLabel: Text(context.localized.enableBackgroundPostersDesc), + onTap: () => ref + .read(clientSettingsProvider.notifier) + .update((cb) => cb.copyWith(backgroundPosters: !clientSettings.backgroundPosters)), + trailing: Switch( + value: clientSettings.backgroundPosters, + onChanged: (value) => + ref.read(clientSettingsProvider.notifier).update((cb) => cb.copyWith(backgroundPosters: value)), + ), + ), + SettingsListTile( + label: Text(context.localized.settingsNextUpCutoffDays), + trailing: SizedBox( + width: 100, + child: IntInputField( + suffix: context.localized.days, + controller: nextUpDaysEditor, + onSubmitted: (value) { + if (value != null) { + ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith( + nextUpDateCutoff: Duration(days: value), + )); + } + }, + )), + ), + SettingsListTile( + label: Text(context.localized.libraryPageSizeTitle), + subLabel: Text(context.localized.libraryPageSizeDesc), + trailing: SizedBox( + width: 100, + child: IntInputField( + controller: libraryPageSizeController, + placeHolder: "500", + onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update( + (current) => current.copyWith(libraryPageSize: value), ), - ) - ]; - }, - ); - }), + )), ), - ), - SettingsListTile( - label: Text(context.localized.settingsBlurredPlaceholderTitle), - subLabel: Text(context.localized.settingsBlurredPlaceholderDesc), - onTap: () => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(!clientSettings.blurPlaceHolders), - trailing: Switch( - value: clientSettings.blurPlaceHolders, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurPlaceholders(value), - ), - ), - SettingsListTile( - label: Text(context.localized.settingsBlurEpisodesTitle), - subLabel: Text(context.localized.settingsBlurEpisodesDesc), - onTap: () => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(!clientSettings.blurUpcomingEpisodes), - trailing: Switch( - value: clientSettings.blurUpcomingEpisodes, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setBlurEpisodes(value), - ), - ), - SettingsListTile( - label: Text(context.localized.settingsEnableOsMediaControls), - onTap: () => ref.read(clientSettingsProvider.notifier).setMediaKeys(!clientSettings.enableMediaKeys), - trailing: Switch( - value: clientSettings.enableMediaKeys, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).setMediaKeys(value), - ), - ), - SettingsListTile( - label: Text(context.localized.settingsNextUpCutoffDays), - trailing: SizedBox( - width: 100, - child: IntInputField( - suffix: context.localized.days, - controller: nextUpDaysEditor, - onSubmitted: (value) { - if (value != null) { - ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith( - nextUpDateCutoff: Duration(days: value), - )); - } - }, - )), - ), - SettingsListTile( - label: Text(context.localized.libraryPageSizeTitle), - subLabel: Text(context.localized.libraryPageSizeDesc), - trailing: SizedBox( - width: 100, - child: IntInputField( - controller: libraryPageSizeController, - placeHolder: "500", - onSubmitted: (value) => ref.read(clientSettingsProvider.notifier).update( - (current) => current.copyWith(libraryPageSize: value), - ), - )), - ), - SettingsListTile( - label: Text(AdaptiveLayout.of(context).isDesktop - ? context.localized.settingsShowScaleSlider - : context.localized.settingsPosterPinch), - onTap: () => ref.read(clientSettingsProvider.notifier).update( - (current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom), - ), - trailing: Switch( - value: clientSettings.pinchPosterZoom, - onChanged: (value) => ref.read(clientSettingsProvider.notifier).update( - (current) => current.copyWith(pinchPosterZoom: value), + SettingsListTile( + label: Text(AdaptiveLayout.of(context).isDesktop + ? context.localized.settingsShowScaleSlider + : context.localized.settingsPosterPinch), + onTap: () => ref.read(clientSettingsProvider.notifier).update( + (current) => current.copyWith(pinchPosterZoom: !current.pinchPosterZoom), ), + trailing: Switch( + value: clientSettings.pinchPosterZoom, + onChanged: (value) => ref.read(clientSettingsProvider.notifier).update( + (current) => current.copyWith(pinchPosterZoom: value), + ), + ), ), - ), - Column( - children: [ - SettingsListTile( - label: Text(context.localized.settingsPosterSize), - trailing: Text( - clientSettings.posterSize.toString(), - style: Theme.of(context).textTheme.bodyLarge, + Column( + children: [ + SettingsListTile( + label: Text(context.localized.settingsPosterSize), + trailing: Text( + clientSettings.posterSize.toString(), + style: Theme.of(context).textTheme.bodyLarge, + ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: FladderSlider( - min: 0.5, - max: 1.5, - value: clientSettings.posterSize, - divisions: 20, - onChanged: (value) => - ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: FladderSlider( + min: 0.5, + max: 1.5, + value: clientSettings.posterSize, + divisions: 20, + onChanged: (value) => + ref.read(clientSettingsProvider.notifier).update((current) => current.copyWith(posterSize: value)), + ), ), - ), - ], - ), - const Divider(), - ]; + ], + ), + ], + ); } diff --git a/lib/screens/settings/client_settings_page.dart b/lib/screens/settings/client_settings_page.dart index e594717..1282115 100644 --- a/lib/screens/settings/client_settings_page.dart +++ b/lib/screens/settings/client_settings_page.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/shared_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; @@ -16,7 +15,8 @@ import 'package:fladder/screens/settings/client_sections/client_settings_visual. import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/simple_duration_picker.dart'; @@ -38,16 +38,12 @@ class _ClientSettingsPageState extends ConsumerState { @override Widget build(BuildContext context) { final clientSettings = ref.watch(clientSettingsProvider); - final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone && - AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; - return Card( - elevation: showBackground ? 2 : 0, - child: SettingsScaffold( - label: "Fladder", - items: [ - ...buildClientSettingsDownload(context, ref, setState), - SettingsLabelDivider(label: context.localized.lockscreen), + return SettingsScaffold( + label: "Fladder", + items: [ + ...buildClientSettingsDownload(context, ref, setState), + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.lockscreen), [ SettingsListTile( label: Text(context.localized.timeOut), subLabel: Text(timePickerString(context, clientSettings.timeOut)), @@ -64,12 +60,16 @@ class _ClientSettingsPageState extends ConsumerState { : null); }, ), - const Divider(), - ...buildClientSettingsDashboard(context, ref), - ...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController), - ...buildClientSettingsTheme(context, ref), - if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[ - SettingsLabelDivider(label: context.localized.controls), + ]), + const SizedBox(height: 12), + ...buildClientSettingsDashboard(context, ref), + const SizedBox(height: 12), + ...buildClientSettingsVisual(context, ref, nextUpDaysEditor, libraryPageSizeController), + const SizedBox(height: 12), + ...buildClientSettingsTheme(context, ref), + const SizedBox(height: 12), + if (AdaptiveLayout.inputDeviceOf(context) == InputDevice.pointer) ...[ + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.controls), [ SettingsListTile( label: Text(context.localized.mouseDragSupport), subLabel: Text(clientSettings.mouseDragSupport ? context.localized.enabled : context.localized.disabled), @@ -83,61 +83,61 @@ class _ClientSettingsPageState extends ConsumerState { .update((current) => current.copyWith(mouseDragSupport: !clientSettings.mouseDragSupport)), ), ), - const Divider(), - ], - ...buildClientSettingsAdvanced(context, ref), - if (kDebugMode) ...[ - const SizedBox(height: 64), - SettingsListTile( - label: Text( - context.localized.clearAllSettings, - ), - contentColor: Theme.of(context).colorScheme.error, - onTap: () { - showDialog( - context: context, - builder: (context) => Dialog( - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - context.localized.clearAllSettingsQuestion, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 16), - Text( - context.localized.unableToReverseAction, - ), - const SizedBox(height: 16), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - FilledButton( - onPressed: () => Navigator.of(context).pop(), - child: Text(context.localized.cancel), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: () async { - await ref.read(sharedPreferencesProvider).clear(); - context.router.push(const LoginRoute()); - }, - child: Text(context.localized.clear), - ) - ], - ), - ], - ), + ]), + const SizedBox(height: 12), + ], + ...buildClientSettingsAdvanced(context, ref), + if (kDebugMode) ...[ + const SizedBox(height: 64), + SettingsListTile( + label: Text( + context.localized.clearAllSettings, + ), + contentColor: Theme.of(context).colorScheme.error, + onTap: () { + showDialog( + context: context, + builder: (context) => Dialog( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + context.localized.clearAllSettingsQuestion, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox(height: 16), + Text( + context.localized.unableToReverseAction, + ), + const SizedBox(height: 16), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + FilledButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.localized.cancel), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () async { + await ref.read(sharedPreferencesProvider).clear(); + context.router.push(const LoginRoute()); + }, + child: Text(context.localized.clear), + ) + ], + ), + ], ), ), - ); - }, - ), - ], + ), + ); + }, + ), ], - ), + ], ); } } diff --git a/lib/screens/settings/player_settings_page.dart b/lib/screens/settings/player_settings_page.dart index 2e5eb22..51512ca 100644 --- a/lib/screens/settings/player_settings_page.dart +++ b/lib/screens/settings/player_settings_page.dart @@ -8,23 +8,23 @@ import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/media_segments_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/settings/video_player_settings.dart'; import 'package:fladder/providers/connectivity_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; +import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/screens/settings/widgets/settings_message_box.dart'; import 'package:fladder/screens/settings/widgets/subtitle_editor.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/input_fields.dart'; import 'package:fladder/screens/video_player/components/video_player_options_sheet.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/bitrate_helper.dart'; import 'package:fladder/util/box_fit_extension.dart'; import 'package:fladder/util/localization_helper.dart'; -import 'package:fladder/util/option_dialogue.dart'; import 'package:fladder/widgets/shared/enum_selection.dart'; @RoutePage() @@ -40,100 +40,101 @@ class _PlayerSettingsPageState extends ConsumerState { Widget build(BuildContext context) { final videoSettings = ref.watch(videoPlayerSettingsProvider); final provider = ref.read(videoPlayerSettingsProvider.notifier); - final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone && - AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; final connectionState = ref.watch(connectivityStatusProvider); - return Card( - elevation: showBackground ? 2 : 0, - child: SettingsScaffold( - label: context.localized.settingsPlayerTitle, - items: [ + return SettingsScaffold( + label: context.localized.settingsPlayerTitle, + items: [ + ...settingsListGroup( + context, SettingsLabelDivider(label: context.localized.video), - if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb) + [ + if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb) + Column( + children: [ + SettingsListTile( + label: Text(context.localized.videoScalingFillScreenTitle), + subLabel: Text(context.localized.videoScalingFillScreenDesc), + onTap: () => provider.setFillScreen(!videoSettings.fillScreen), + trailing: Switch( + value: videoSettings.fillScreen, + onChanged: (value) => provider.setFillScreen(value), + ), + ), + AnimatedFadeSize( + child: videoSettings.fillScreen + ? SettingsMessageBox( + context.localized.videoScalingFillScreenNotif, + messageType: MessageType.warning, + ) + : Container(), + ), + ], + ), SettingsListTile( - label: Text(context.localized.videoScalingFillScreenTitle), - subLabel: Text(context.localized.videoScalingFillScreenDesc), - onTap: () => provider.setFillScreen(!videoSettings.fillScreen), - trailing: Switch( - value: videoSettings.fillScreen, - onChanged: (value) => provider.setFillScreen(value), + label: Text(context.localized.videoScaling), + trailing: EnumBox( + current: videoSettings.videoFit.label(context), + itemBuilder: (context) => BoxFit.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => ref.read(videoPlayerSettingsProvider.notifier).setFitType(entry), + ), + ) + .toList(), ), ), - AnimatedFadeSize( - child: videoSettings.fillScreen - ? SettingsMessageBox( - context.localized.videoScalingFillScreenNotif, - messageType: MessageType.warning, - ) - : Container(), - ), - SettingsListTile( - label: Text(context.localized.videoScalingFillScreenTitle), - subLabel: Text(videoSettings.videoFit.label(context)), - onTap: () => openMultiSelectOptions( - context, - label: context.localized.videoScalingFillScreenTitle, - items: BoxFit.values, - selected: [ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit))], - onChanged: (values) => ref.read(videoPlayerSettingsProvider.notifier).setFitType(values.first), - itemBuilder: (type, selected, tap) => RadioListTile( - groupValue: ref.read(videoPlayerSettingsProvider.select((value) => value.videoFit)), - title: Text(type.label(context)), - value: type, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - contentPadding: EdgeInsets.zero, - onChanged: (value) => tap(), + SettingsListTile( + label: _StatusIndicator( + homeInternet: connectionState.homeInternet, + label: Text(context.localized.homeStreamingQualityTitle), + ), + subLabel: Text(context.localized.homeStreamingQualityDesc), + trailing: EnumBox( + current: ref.watch( + videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)), + ), + itemBuilder: (context) => Bitrate.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = + ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry), + ), + ) + .toList(), ), ), - ), - SettingsListTile( - label: _StatusIndicator( - homeInternet: connectionState.homeInternet, - label: Text(context.localized.homeStreamingQualityTitle), - ), - subLabel: Text(context.localized.homeStreamingQualityDesc), - trailing: EnumBox( - current: ref.watch( - videoPlayerSettingsProvider.select((value) => value.maxHomeBitrate.label(context)), + SettingsListTile( + label: _StatusIndicator( + homeInternet: !connectionState.homeInternet, + label: Text(context.localized.internetStreamingQualityTitle), ), - itemBuilder: (context) => Bitrate.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = - ref.read(videoPlayerSettingsProvider).copyWith(maxHomeBitrate: entry), - ), - ) - .toList(), - ), - ), - SettingsListTile( - label: _StatusIndicator( - homeInternet: !connectionState.homeInternet, - label: Text(context.localized.internetStreamingQualityTitle), - ), - subLabel: Text(context.localized.internetStreamingQualityDesc), - trailing: EnumBox( - current: ref.watch( - videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)), + subLabel: Text(context.localized.internetStreamingQualityDesc), + trailing: EnumBox( + current: ref.watch( + videoPlayerSettingsProvider.select((value) => value.maxInternetBitrate.label(context)), + ), + itemBuilder: (context) => Bitrate.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = + ref.read(videoPlayerSettingsProvider).copyWith(maxInternetBitrate: entry), + ), + ) + .toList(), ), - itemBuilder: (context) => Bitrate.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = - ref.read(videoPlayerSettingsProvider).copyWith(maxInternetBitrate: entry), - ), - ) - .toList(), ), - ), - const Divider(), - SettingsLabelDivider(label: context.localized.mediaSegmentActions), + ], + ), + const SizedBox(height: 12), + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.mediaSegmentActions), [ ...videoSettings.segmentSkipSettings.entries.sorted((a, b) => b.key.index.compareTo(a.key.index)).map( (entry) => Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), @@ -166,8 +167,34 @@ class _PlayerSettingsPageState extends ConsumerState { ), ), ), - const Divider(), - SettingsLabelDivider(label: context.localized.advanced), + ]), + const SizedBox(height: 12), + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.playbackTrackSelection), [ + SettingsListTile( + label: Text(context.localized.rememberAudioSelections), + subLabel: Text(context.localized.rememberAudioSelectionsDesc), + onTap: () => ref.read(userProvider.notifier).setRememberAudioSelections(), + trailing: Switch( + value: ref.watch(userProvider.select( + (value) => value?.userConfiguration?.rememberAudioSelections ?? true, + )), + onChanged: (_) => ref.read(userProvider.notifier).setRememberAudioSelections(), + ), + ), + SettingsListTile( + label: Text(context.localized.rememberSubtitleSelections), + subLabel: Text(context.localized.rememberSubtitleSelectionsDesc), + onTap: () => ref.read(userProvider.notifier).setRememberSubtitleSelections(), + trailing: Switch( + value: ref.watch(userProvider.select( + (value) => value?.userConfiguration?.rememberSubtitleSelections ?? true, + )), + onChanged: (_) => ref.read(userProvider.notifier).setRememberSubtitleSelections(), + ), + ), + ]), + const SizedBox(height: 12), + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.advanced), [ if (PlayerOptions.available.length != 1) SettingsListTile( label: Text(context.localized.playerSettingsBackendTitle), @@ -212,7 +239,7 @@ class _PlayerSettingsPageState extends ConsumerState { onChanged: (value) => provider.setHardwareAccel(value), ), ), - if (!kIsWeb) ...[ + if (!kIsWeb) SettingsListTile( label: Text(context.localized.settingsPlayerNativeLibassAccelTitle), subLabel: Text(context.localized.settingsPlayerNativeLibassAccelDesc), @@ -222,29 +249,28 @@ class _PlayerSettingsPageState extends ConsumerState { onChanged: (value) => provider.setUseLibass(value), ), ), - AnimatedFadeSize( - child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid - ? SettingsMessageBox( - context.localized.settingsPlayerMobileWarning, - messageType: MessageType.warning, - ) - : Container(), - ), - ], + AnimatedFadeSize( + child: videoSettings.useLibass && videoSettings.hardwareAccel && Platform.isAndroid + ? SettingsMessageBox( + context.localized.settingsPlayerMobileWarning, + messageType: MessageType.warning, + ) + : Container(), + ), SettingsListTile( label: Text(context.localized.settingsPlayerBufferSizeTitle), subLabel: Text(context.localized.settingsPlayerBufferSizeDesc), trailing: SizedBox( - width: 70, - child: IntInputField( - suffix: 'MB', - controller: TextEditingController(text: videoSettings.bufferSize.toString()), - onSubmitted: (value) { - if (value != null) { - provider.setBufferSize(value); - } - }, - )), + width: 70, + child: IntInputField( + suffix: 'MB', + controller: TextEditingController(text: videoSettings.bufferSize.toString()), + onSubmitted: (value) { + if (value != null) { + provider.setBufferSize(value); + } + }, + )), ), SettingsListTile( label: Text(context.localized.settingsPlayerCustomSubtitlesTitle), @@ -267,33 +293,37 @@ class _PlayerSettingsPageState extends ConsumerState { "${context.localized.noVideoPlayerOptions}\n${context.localized.mdkExperimental}") }, ), - SettingsListTile( - label: Text(context.localized.settingsAutoNextTitle), - subLabel: Text(context.localized.settingsAutoNextDesc), - trailing: EnumBox( - current: ref.watch( - videoPlayerSettingsProvider.select( - (value) => value.nextVideoType.label(context), + Column( + children: [ + SettingsListTile( + label: Text(context.localized.settingsAutoNextTitle), + subLabel: Text(context.localized.settingsAutoNextDesc), + trailing: EnumBox( + current: ref.watch( + videoPlayerSettingsProvider.select( + (value) => value.nextVideoType.label(context), + ), + ), + itemBuilder: (context) => AutoNextType.values + .map( + (entry) => PopupMenuItem( + value: entry, + child: Text(entry.label(context)), + onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = + ref.read(videoPlayerSettingsProvider).copyWith(nextVideoType: entry), + ), + ) + .toList(), ), ), - itemBuilder: (context) => AutoNextType.values - .map( - (entry) => PopupMenuItem( - value: entry, - child: Text(entry.label(context)), - onTap: () => ref.read(videoPlayerSettingsProvider.notifier).state = - ref.read(videoPlayerSettingsProvider).copyWith(nextVideoType: entry), - ), - ) - .toList(), - ), - ), - AnimatedFadeSize( - child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) { - AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)), - AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)), - _ => const SizedBox.shrink(), - }, + AnimatedFadeSize( + child: switch (ref.watch(videoPlayerSettingsProvider.select((value) => value.nextVideoType))) { + AutoNextType.smart => SettingsMessageBox(AutoNextType.smart.desc(context)), + AutoNextType.static => SettingsMessageBox(AutoNextType.static.desc(context)), + _ => const SizedBox.shrink(), + }, + ), + ], ), if (!AdaptiveLayout.of(context).isDesktop && !kIsWeb) SettingsListTile( @@ -301,8 +331,8 @@ class _PlayerSettingsPageState extends ConsumerState { subLabel: Text(context.localized.playerSettingsOrientationDesc), onTap: () => showOrientationOptions(context, ref), ), - ], - ), + ]), + ], ); } } @@ -328,7 +358,7 @@ class _StatusIndicator extends StatelessWidget { ), const SizedBox(width: 6), ], - label, + Flexible(child: label), ], ); } diff --git a/lib/screens/settings/security_settings_page.dart b/lib/screens/settings/security_settings_page.dart index bafaafa..6fbfed5 100644 --- a/lib/screens/settings/security_settings_page.dart +++ b/lib/screens/settings/security_settings_page.dart @@ -1,14 +1,15 @@ +import 'package:flutter/material.dart'; + import 'package:auto_route/auto_route.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/settings/widgets/settings_label_divider.dart'; +import 'package:fladder/screens/settings/widgets/settings_list_group.dart'; import 'package:fladder/screens/shared/authenticate_button_options.dart'; -import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; @RoutePage() class SecuritySettingsPage extends ConsumerStatefulWidget { @@ -22,14 +23,10 @@ class _UserSettingsPageState extends ConsumerState { @override Widget build(BuildContext context) { final user = ref.watch(userProvider); - final showBackground = AdaptiveLayout.viewSizeOf(context) != ViewSize.phone && - AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; - return Card( - elevation: showBackground ? 2 : 0, - child: SettingsScaffold( - label: context.localized.settingsProfileTitle, - items: [ - SettingsLabelDivider(label: context.localized.settingSecurityApplockTitle), + return SettingsScaffold( + label: context.localized.settingsProfileTitle, + items: [ + ...settingsListGroup(context, SettingsLabelDivider(label: context.localized.settingSecurityApplockTitle), [ SettingsListTile( label: Text(context.localized.settingSecurityApplockTitle), subLabel: Text(user?.authMethod.name(context) ?? ""), @@ -37,8 +34,8 @@ class _UserSettingsPageState extends ConsumerState { ref.read(userProvider.notifier).updateUser(newUser); }), ), - ], - ), + ]), + ], ); } } diff --git a/lib/screens/settings/settings_list_tile.dart b/lib/screens/settings/settings_list_tile.dart index c8d3c74..8e2ecba 100644 --- a/lib/screens/settings/settings_list_tile.dart +++ b/lib/screens/settings/settings_list_tile.dart @@ -8,7 +8,7 @@ class SettingsListTile extends StatelessWidget { final Widget? trailing; final bool selected; final IconData? icon; - final Widget? suffix; + final Widget? leading; final Color? contentColor; final Function()? onTap; const SettingsListTile({ @@ -16,7 +16,7 @@ class SettingsListTile extends StatelessWidget { this.subLabel, this.trailing, this.selected = false, - this.suffix, + this.leading, this.icon, this.contentColor, this.onTap, @@ -27,7 +27,7 @@ class SettingsListTile extends StatelessWidget { Widget build(BuildContext context) { final iconWidget = icon != null ? Icon(icon) : null; - final leadingWidget = (suffix ?? iconWidget) != null + final leadingWidget = (leading ?? iconWidget) != null ? Padding( padding: const EdgeInsets.only(left: 8, right: 16.0), child: AnimatedContainer( @@ -38,11 +38,11 @@ class SettingsListTile extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 12), - child: (suffix ?? iconWidget), + child: (leading ?? iconWidget), ), ), ) - : suffix ?? const SizedBox(); + : leading ?? const SizedBox(); return Card( elevation: selected ? 2 : 0, color: selected ? Theme.of(context).colorScheme.surfaceContainerLow : Colors.transparent, @@ -57,7 +57,7 @@ class SettingsListTile extends StatelessWidget { horizontal: 16, vertical: 12, ).copyWith( - left: (suffix ?? iconWidget) != null ? 0 : null, + left: (leading ?? iconWidget) != null ? 0 : null, ), child: ConstrainedBox( constraints: const BoxConstraints( @@ -90,7 +90,7 @@ class SettingsListTile extends StatelessWidget { ), if (subLabel != null) Opacity( - opacity: 0.75, + opacity: 0.65, child: Material( color: Colors.transparent, textStyle: Theme.of(context).textTheme.labelLarge, diff --git a/lib/screens/settings/settings_scaffold.dart b/lib/screens/settings/settings_scaffold.dart index 0121d67..0b17395 100644 --- a/lib/screens/settings/settings_scaffold.dart +++ b/lib/screens/settings/settings_scaffold.dart @@ -4,10 +4,9 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/shared/user_icon.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/router_extension.dart'; class SettingsScaffold extends ConsumerWidget { @@ -34,7 +33,6 @@ class SettingsScaffold extends ConsumerWidget { final padding = MediaQuery.of(context).padding; final singleLayout = AdaptiveLayout.layoutModeOf(context) == LayoutMode.single; return Scaffold( - backgroundColor: AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual ? Colors.transparent : null, floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: floatingActionButton, body: Column( @@ -87,9 +85,10 @@ class SettingsScaffold extends ConsumerWidget { ), ), SliverPadding( - padding: MediaQuery.paddingOf(context).copyWith(top: AdaptiveLayout.of(context).isDesktop ? 0 : 8), - sliver: SliverList( - delegate: SliverChildListDelegate(items), + padding: MediaQuery.paddingOf(context).copyWith(top: 0), + sliver: SliverList.builder( + itemBuilder: (context, index) => items[index], + itemCount: items.length, ), ), if (bottomActions.isEmpty) diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart index f9945e0..dc0a70f 100644 --- a/lib/screens/settings/settings_screen.dart +++ b/lib/screens/settings/settings_screen.dart @@ -1,18 +1,21 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:window_manager/window_manager.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; +import 'package:fladder/providers/arguments_provider.dart'; import 'package:fladder/providers/auth_provider.dart'; +import 'package:fladder/providers/update_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/settings/quick_connect_window.dart'; import 'package:fladder/screens/settings/settings_list_tile.dart'; import 'package:fladder/screens/settings/settings_scaffold.dart'; import 'package:fladder/screens/shared/fladder_icon.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/screens/shared/fladder_snackbar.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/theme_extensions.dart'; @@ -97,119 +100,152 @@ class _SettingsScreenState extends ConsumerState { final quickConnectAvailable = ref.watch(userProvider.select((value) => value?.serverConfiguration?.quickConnectAvailable ?? false)); - return Container( - color: context.colors.surface, - child: SettingsScaffold( - label: context.localized.settings, - scrollController: scrollController, - showBackButtonNested: true, - showUserIcon: true, - items: [ - SettingsListTile( - label: Text(context.localized.settingsClientTitle), - subLabel: Text(context.localized.settingsClientDesc), - selected: containsRoute(const ClientSettingsRoute()), - icon: deviceIcon, - onTap: () => navigateTo(const ClientSettingsRoute()), - ), - if (quickConnectAvailable) - SettingsListTile( - label: Text(context.localized.settingsQuickConnectTitle), - icon: IconsaxPlusLinear.password_check, - onTap: () => openQuickConnectDialog(context), - ), - SettingsListTile( - label: Text(context.localized.settingsProfileTitle), - subLabel: Text(context.localized.settingsProfileDesc), - selected: containsRoute(const SecuritySettingsRoute()), - icon: IconsaxPlusLinear.security_user, - onTap: () => navigateTo(const SecuritySettingsRoute()), - ), - SettingsListTile( - label: Text(context.localized.settingsPlayerTitle), - subLabel: Text(context.localized.settingsPlayerDesc), - selected: containsRoute(const PlayerSettingsRoute()), - icon: IconsaxPlusLinear.video_play, - onTap: () => navigateTo(const PlayerSettingsRoute()), - ), - SettingsListTile( - label: Text(context.localized.about), - subLabel: const Text("Fladder"), - selected: containsRoute(const AboutSettingsRoute()), - suffix: Opacity( - opacity: 1, - child: FladderIconOutlined( - size: 24, - color: context.colors.onSurfaceVariant, + final newRelease = ref.watch(updateProvider.select((value) => value.latestRelease)); + + final hasNewUpdate = ref.watch(hasNewUpdateProvider); + + return Padding( + padding: EdgeInsets.only(left: AdaptiveLayout.of(context).sideBarWidth), + child: Container( + color: context.colors.surface, + child: SettingsScaffold( + label: context.localized.settings, + scrollController: scrollController, + showBackButtonNested: true, + showUserIcon: true, + items: [ + if (hasNewUpdate && newRelease != null) ...[ + Card( + color: context.colors.secondaryContainer, + child: SettingsListTile( + label: Text(context.localized.newReleaseFoundTitle(newRelease.version)), + subLabel: Text(context.localized.newUpdateFoundOnGithub), + icon: IconsaxPlusLinear.information, + onTap: () => navigateTo(const AboutSettingsRoute()), + ), ), + const SizedBox(height: 8), + ], + SettingsListTile( + label: Text(context.localized.settingsClientTitle), + subLabel: Text(context.localized.settingsClientDesc), + selected: containsRoute(const ClientSettingsRoute()), + icon: deviceIcon, + onTap: () => navigateTo(const ClientSettingsRoute()), ), - onTap: () => navigateTo(const AboutSettingsRoute()), - ), - ], - floatingActionButton: Padding( - padding: EdgeInsets.symmetric(horizontal: MediaQuery.paddingOf(context).horizontal), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Spacer(), - FloatingActionButton( - key: Key(context.localized.switchUser), - tooltip: context.localized.switchUser, - onPressed: () async { - await ref.read(userProvider.notifier).logoutUser(); - context.router.replaceAll([const LoginRoute()]); - }, - child: const Icon( - IconsaxPlusLinear.arrow_swap_horizontal, - ), + if (quickConnectAvailable) + SettingsListTile( + label: Text(context.localized.settingsQuickConnectTitle), + icon: IconsaxPlusLinear.password_check, + onTap: () => openQuickConnectDialog(context), + ), + SettingsListTile( + label: Text(context.localized.settingsProfileTitle), + subLabel: Text(context.localized.settingsProfileDesc), + selected: containsRoute(const SecuritySettingsRoute()), + icon: IconsaxPlusLinear.security_user, + onTap: () => navigateTo(const SecuritySettingsRoute()), + ), + SettingsListTile( + label: Text(context.localized.settingsPlayerTitle), + subLabel: Text(context.localized.settingsPlayerDesc), + selected: containsRoute(const PlayerSettingsRoute()), + icon: IconsaxPlusLinear.video_play, + onTap: () => navigateTo(const PlayerSettingsRoute()), + ), + SettingsListTile( + label: Text(context.localized.about), + subLabel: Text("Fladder, ${context.localized.latestReleases}"), + selected: containsRoute(const AboutSettingsRoute()), + leading: Opacity( + opacity: 1, + child: FladderIconOutlined( + size: 24, + color: context.colors.onSurfaceVariant, ), - const SizedBox(width: 16), - FloatingActionButton( - heroTag: context.localized.logout, - key: Key(context.localized.logout), - tooltip: context.localized.logout, - backgroundColor: Theme.of(context).colorScheme.errorContainer, - onPressed: () { - final user = ref.read(userProvider); - showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")), - scrollable: true, - content: Text( - context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""), - ), - actions: [ - ElevatedButton( - onPressed: () => Navigator.pop(context), - child: Text(context.localized.cancel), + ), + onTap: () => navigateTo(const AboutSettingsRoute()), + ), + if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) ...[ + SettingsListTile( + label: Text(context.localized.exitFladderTitle), + icon: IconsaxPlusLinear.close_square, + onTap: () async { + final manager = WindowManager.instance; + if (await manager.isClosable()) { + manager.close(); + } else { + fladderSnackbar(context, title: context.localized.somethingWentWrong); + } + }, + ), + ], + ], + floatingActionButton: Padding( + padding: EdgeInsets.symmetric(horizontal: MediaQuery.paddingOf(context).horizontal), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Spacer(), + FloatingActionButton( + key: Key(context.localized.switchUser), + tooltip: context.localized.switchUser, + onPressed: () async { + await ref.read(userProvider.notifier).logoutUser(); + context.router.replaceAll([const LoginRoute()]); + }, + child: const Icon( + IconsaxPlusLinear.arrow_swap_horizontal, + ), + ), + const SizedBox(width: 16), + FloatingActionButton( + heroTag: context.localized.logout, + key: Key(context.localized.logout), + tooltip: context.localized.logout, + backgroundColor: Theme.of(context).colorScheme.errorContainer, + onPressed: () { + final user = ref.read(userProvider); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(context.localized.logoutUserPopupTitle(user?.name ?? "")), + scrollable: true, + content: Text( + context.localized.logoutUserPopupContent(user?.name ?? "", user?.server ?? ""), ), - ElevatedButton( - style: ElevatedButton.styleFrom().copyWith( - iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), - foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), - backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer), + actions: [ + ElevatedButton( + onPressed: () => Navigator.pop(context), + child: Text(context.localized.cancel), ), - onPressed: () async { - await ref.read(authProvider.notifier).logOutUser(); - if (context.mounted) { - context.router.replaceAll([const LoginRoute()]); - } - }, - child: Text(context.localized.logout), - ), - ], - ), - ); - }, - child: Icon( - IconsaxPlusLinear.logout, - color: Theme.of(context).colorScheme.onErrorContainer, + ElevatedButton( + style: ElevatedButton.styleFrom().copyWith( + iconColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), + foregroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.onErrorContainer), + backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.errorContainer), + ), + onPressed: () async { + await ref.read(authProvider.notifier).logOutUser(); + if (context.mounted) { + context.router.replaceAll([const LoginRoute()]); + } + }, + child: Text(context.localized.logout), + ), + ], + ), + ); + }, + child: Icon( + IconsaxPlusLinear.logout, + color: Theme.of(context).colorScheme.onErrorContainer, + ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/screens/settings/widgets/settings_list_group.dart b/lib/screens/settings/widgets/settings_list_group.dart new file mode 100644 index 0000000..ca8bf59 --- /dev/null +++ b/lib/screens/settings/widgets/settings_list_group.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import 'package:dynamic_color/dynamic_color.dart'; + +List settingsListGroup(BuildContext context, Widget label, List children) { + final radius = BorderRadius.circular(24); + final radiusSmall = const Radius.circular(6); + final color = Theme.of(context).colorScheme.surfaceContainerLow.harmonizeWith(Colors.red); + return [ + Card( + elevation: 0, + margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + color: color, + shape: RoundedRectangleBorder( + borderRadius: radius.copyWith( + bottomLeft: radiusSmall, + bottomRight: radiusSmall, + ), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: label, + ), + ), + ...children.map( + (e) { + return Card( + elevation: 0, + color: color, + margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + shape: RoundedRectangleBorder( + borderRadius: radius.copyWith( + topLeft: radiusSmall, + topRight: radiusSmall, + bottomLeft: e != children.last ? radiusSmall : null, + bottomRight: e != children.last ? radiusSmall : null, + )), + child: e, + ); + }, + ) + ]; +} diff --git a/lib/screens/settings/widgets/settings_message_box.dart b/lib/screens/settings/widgets/settings_message_box.dart index 9298076..f304fb5 100644 --- a/lib/screens/settings/widgets/settings_message_box.dart +++ b/lib/screens/settings/widgets/settings_message_box.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.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/util/list_padding.dart'; diff --git a/lib/screens/settings/widgets/settings_update_information.dart b/lib/screens/settings/widgets/settings_update_information.dart new file mode 100644 index 0000000..4db8175 --- /dev/null +++ b/lib/screens/settings/widgets/settings_update_information.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:markdown_widget/widget/markdown.dart'; + +import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/providers/update_provider.dart'; +import 'package:fladder/screens/settings/settings_list_tile.dart'; +import 'package:fladder/screens/shared/media/external_urls.dart'; +import 'package:fladder/util/list_padding.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/theme_extensions.dart'; +import 'package:fladder/util/update_checker.dart'; + +class SettingsUpdateInformation extends ConsumerStatefulWidget { + const SettingsUpdateInformation({super.key}); + + @override + ConsumerState createState() => _SettingsUpdateInformationState(); +} + +class _SettingsUpdateInformationState extends ConsumerState { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((value) { + final latestRelease = ref.read(updateProvider.select((value) => value.latestRelease)); + if (latestRelease == null) return; + final lastViewedUpdate = ref.read(clientSettingsProvider.select((value) => value.lastViewedUpdate)); + if (lastViewedUpdate != latestRelease.version) { + ref + .read(clientSettingsProvider.notifier) + .update((value) => value.copyWith(lastViewedUpdate: latestRelease.version)); + } + }); + } + + @override + Widget build(BuildContext context) { + final updates = ref.watch(updateProvider); + final latestRelease = updates.latestRelease; + final otherReleases = updates.lastRelease; + final checkForUpdate = ref.watch(clientSettingsProvider.select((value) => value.checkForUpdates)); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: ListView( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: [ + const Divider(), + SettingsListTile( + label: Text(context.localized.latestReleases), + subLabel: Text(context.localized.autoCheckForUpdates), + onTap: () => ref + .read(clientSettingsProvider.notifier) + .update((value) => value.copyWith(checkForUpdates: !checkForUpdate)), + trailing: Switch( + value: checkForUpdate, + onChanged: (value) => ref + .read(clientSettingsProvider.notifier) + .update((value) => value.copyWith(checkForUpdates: !checkForUpdate)), + ), + ), + if (latestRelease != null) + UpdateInformation( + releaseInfo: latestRelease, + expanded: true, + ), + ...otherReleases.where((element) => element != latestRelease).map( + (value) => UpdateInformation(releaseInfo: value), + ), + ], + ), + ); + } +} + +class UpdateInformation extends StatelessWidget { + final ReleaseInfo releaseInfo; + final bool expanded; + const UpdateInformation({ + required this.releaseInfo, + this.expanded = false, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ExpansionTile( + backgroundColor: + releaseInfo.isNewerThanCurrent ? context.colors.primaryContainer : context.colors.surfaceContainer, + collapsedBackgroundColor: releaseInfo.isNewerThanCurrent ? context.colors.primaryContainer : null, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + collapsedShape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + title: Text(releaseInfo.version), + initiallyExpanded: expanded, + childrenPadding: const EdgeInsets.all(16), + expandedCrossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Card( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: MarkdownWidget( + data: releaseInfo.changelog, + shrinkWrap: true, + ), + ), + ), + ...releaseInfo.preferredDownloads.entries.map( + (entry) { + return FilledButton( + onPressed: () => launchUrl(context, entry.value), + child: Text( + entry.key.prettifyKey(), + ), + ); + }, + ), + const Divider(), + ...releaseInfo.otherDownloads.entries.map( + (entry) { + return ElevatedButton( + onPressed: () => launchUrl(context, entry.value), + child: Text( + entry.key.prettifyKey(), + ), + ); + }, + ) + ].addInBetween(const SizedBox(height: 12)), + ); + } +} diff --git a/lib/screens/settings/widgets/subtitle_editor.dart b/lib/screens/settings/widgets/subtitle_editor.dart index 9b0ebef..0d80071 100644 --- a/lib/screens/settings/widgets/subtitle_editor.dart +++ b/lib/screens/settings/widgets/subtitle_editor.dart @@ -7,7 +7,7 @@ import 'package:fladder/models/settings/subtitle_settings_model.dart'; import 'package:fladder/providers/settings/subtitle_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart'; diff --git a/lib/screens/shared/adaptive_dialog.dart b/lib/screens/shared/adaptive_dialog.dart index c3867fc..49fdef8 100644 --- a/lib/screens/shared/adaptive_dialog.dart +++ b/lib/screens/shared/adaptive_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; Future showDialogAdaptive( {required BuildContext context, required Widget Function(BuildContext context) builder}) { @@ -16,8 +16,11 @@ Future showDialogAdaptive( return showDialog( context: context, useSafeArea: false, - builder: (context) => Dialog.fullscreen( - child: builder(context), + builder: (context) => Padding( + padding: MediaQuery.paddingOf(context), + child: Dialog.fullscreen( + child: builder(context), + ), ), ); } diff --git a/lib/screens/shared/animated_fade_size.dart b/lib/screens/shared/animated_fade_size.dart index 11e860a..eadbf7e 100644 --- a/lib/screens/shared/animated_fade_size.dart +++ b/lib/screens/shared/animated_fade_size.dart @@ -1,12 +1,15 @@ import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; class AnimatedFadeSize extends ConsumerWidget { final Duration duration; final Widget child; + final Alignment alignment; const AnimatedFadeSize({ this.duration = const Duration(milliseconds: 125), required this.child, + this.alignment = Alignment.center, super.key, }); @@ -14,6 +17,7 @@ class AnimatedFadeSize extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return AnimatedSize( duration: duration, + alignment: alignment, curve: Curves.easeInOutCubic, child: AnimatedSwitcher( duration: duration, diff --git a/lib/screens/shared/chips/category_chip.dart b/lib/screens/shared/chips/category_chip.dart index 0921c29..0d07d83 100644 --- a/lib/screens/shared/chips/category_chip.dart +++ b/lib/screens/shared/chips/category_chip.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/map_bool_helper.dart'; @@ -100,33 +99,37 @@ class CategoryChip extends StatelessWidget { label: Text(context.localized.clear), ) ].addInBetween(const SizedBox(width: 6)); - Widget header() => Row( + Widget header(BuildContext context) => Column( + crossAxisAlignment: CrossAxisAlignment.center, children: [ Material( color: Colors.transparent, textStyle: Theme.of(context).textTheme.titleLarge, child: dialogueTitle ?? label, ), - const Spacer(), - FilledButton.tonal( - onPressed: () { - Navigator.of(context).pop(); - newEntry = null; - onCancel?.call(); - }, - child: Text(context.localized.cancel), + Row( + children: [ + FilledButton.tonal( + onPressed: () { + Navigator.of(context).pop(); + newEntry = null; + onCancel?.call(); + }, + child: Text(context.localized.cancel), + ), + if (onClear != null) + ElevatedButton.icon( + onPressed: () { + Navigator.of(context).pop(); + newEntry = null; + onClear!(); + }, + icon: const Icon(IconsaxPlusLinear.back_square), + label: Text(context.localized.clear), + ) + ].addInBetween(const SizedBox(width: 6)), ), - if (onClear != null) - ElevatedButton.icon( - onPressed: () { - Navigator.of(context).pop(); - newEntry = null; - onClear!(); - }, - icon: const Icon(IconsaxPlusLinear.back_square), - label: Text(context.localized.clear), - ) - ].addInBetween(const SizedBox(width: 6)), + ], ); if (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone) { @@ -156,7 +159,7 @@ class CategoryChip extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: header(), + child: header(context), ), const Divider(), CategoryChipEditor( diff --git a/lib/screens/shared/default_title_bar.dart b/lib/screens/shared/default_title_bar.dart index 4fa1f2c..d12ff14 100644 --- a/lib/screens/shared/default_title_bar.dart +++ b/lib/screens/shared/default_title_bar.dart @@ -4,7 +4,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/providers/arguments_provider.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; class DefaultTitleBar extends ConsumerStatefulWidget { final String? label; @@ -33,6 +35,7 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi @override Widget build(BuildContext context) { + if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return const SizedBox.shrink(); final brightness = widget.brightness ?? Theme.of(context).brightness; final iconColor = Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65); return MouseRegion( @@ -77,11 +80,9 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi children: [ FutureBuilder>(future: Future.microtask(() async { final isMinimized = await windowManager.isMinimized(); - final isFullScreen = await windowManager.isFullScreen(); - return [isMinimized, isFullScreen]; + return [isMinimized]; }), builder: (context, snapshot) { final isMinimized = snapshot.data?.firstOrNull ?? false; - final fullScreen = snapshot.data?.lastOrNull ?? false; return IconButton( style: IconButton.styleFrom( hoverColor: brightness == Brightness.light @@ -89,9 +90,7 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi : Colors.white.withValues(alpha: 0.2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2))), onPressed: () async { - if (fullScreen) { - await windowManager.setFullScreen(false); - } + fullScreenHelper.closeFullScreen(ref); if (isMinimized) { windowManager.restore(); } else { @@ -111,12 +110,10 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi FutureBuilder>( future: Future.microtask(() async { final isMaximized = await windowManager.isMaximized(); - final isFullScreen = await windowManager.isFullScreen(); - return [isMaximized, isFullScreen]; + return [isMaximized]; }), builder: (BuildContext context, AsyncSnapshot> snapshot) { final maximized = snapshot.data?.firstOrNull ?? false; - final fullScreen = snapshot.data?.lastOrNull ?? false; return IconButton( style: IconButton.styleFrom( hoverColor: brightness == Brightness.light @@ -125,15 +122,12 @@ class _DefaultTitleBarState extends ConsumerState with WindowLi shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(2)), ), onPressed: () async { - if (fullScreen && maximized) { - await windowManager.setFullScreen(false); + fullScreenHelper.closeFullScreen(ref); + if (maximized) { await windowManager.unmaximize(); return; } - - if (fullScreen) { - await windowManager.setFullScreen(false); - } else if (!maximized) { + if (!maximized) { await windowManager.maximize(); } else { await windowManager.unmaximize(); diff --git a/lib/screens/shared/detail_scaffold.dart b/lib/screens/shared/detail_scaffold.dart index 63d6b24..dadf07d 100644 --- a/lib/screens/shared/detail_scaffold.dart +++ b/lib/screens/shared/detail_scaffold.dart @@ -1,22 +1,18 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.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/models/item_base_model.dart'; import 'package:fladder/models/items/images_models.dart'; -import 'package:fladder/models/media_playback_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/routes/auto_router.gr.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/fladder_image.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/router_extension.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; @@ -64,9 +60,9 @@ class _DetailScaffoldState extends ConsumerState { Widget build(BuildContext context) { final padding = EdgeInsets.symmetric(horizontal: MediaQuery.sizeOf(context).width / 25); final backGroundColor = Theme.of(context).colorScheme.surface.withValues(alpha: 0.8); - final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state)); final minHeight = 450.0.clamp(0, MediaQuery.sizeOf(context).height).toDouble(); final maxHeight = MediaQuery.sizeOf(context).height - 10; + final sideBarPadding = AdaptiveLayout.of(context).sideBarWidth; return PullToRefresh( onRefresh: () async { await widget.onRefresh?.call(); @@ -78,16 +74,6 @@ class _DetailScaffoldState extends ConsumerState { }, refreshOnStart: true, child: Scaffold( - floatingActionButtonAnimator: - playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null, - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: switch (playerState) { - VideoPlayerState.minimized => const Padding( - padding: EdgeInsets.all(8.0), - child: FloatingPlayerBar(), - ), - _ => null, - }, backgroundColor: Theme.of(context).colorScheme.surface, extendBodyBehindAppBar: true, body: Stack( @@ -164,7 +150,6 @@ class _DetailScaffoldState extends ConsumerState { Padding( padding: EdgeInsets.only( bottom: 0, - left: MediaQuery.of(context).padding.left, top: MediaQuery.of(context).padding.top, ), child: ConstrainedBox( @@ -172,7 +157,9 @@ class _DetailScaffoldState extends ConsumerState { minHeight: MediaQuery.sizeOf(context).height, maxWidth: MediaQuery.sizeOf(context).width, ), - child: widget.content(padding), + child: widget.content(padding.copyWith( + left: sideBarPadding + 25 + MediaQuery.paddingOf(context).left, + )), ), ), ], @@ -182,9 +169,11 @@ class _DetailScaffoldState extends ConsumerState { IconTheme( data: IconThemeData(color: Theme.of(context).colorScheme.onSurface), child: Padding( - padding: MediaQuery.paddingOf(context).add( - const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - ), + padding: MediaQuery.paddingOf(context) + .copyWith(left: sideBarPadding + MediaQuery.paddingOf(context).left) + .add( + const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), child: Row( children: [ IconButton.filledTonal( @@ -255,13 +244,13 @@ class _DetailScaffoldState extends ConsumerState { child: SettingsUserIcon(), ), ), - Tooltip( - message: context.localized.home, - child: IconButton( - onPressed: () => context.router.navigate(const DashboardRoute()), - icon: const Icon(IconsaxPlusLinear.home), - ), - ), + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) + Tooltip( + message: context.localized.home, + child: IconButton( + onPressed: () => context.navigateTo(const DashboardRoute()), + icon: const Icon(IconsaxPlusLinear.home), + )), ], ), ), diff --git a/lib/screens/shared/file_picker.dart b/lib/screens/shared/file_picker.dart index 89f6ad9..a63a0c3 100644 --- a/lib/screens/shared/file_picker.dart +++ b/lib/screens/shared/file_picker.dart @@ -1,7 +1,7 @@ import 'package:desktop_drop/desktop_drop.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/screens/shared/outlined_text_field.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:flutter/foundation.dart'; // ignore: depend_on_referenced_packages import 'package:path/path.dart' as p; diff --git a/lib/screens/shared/floating_search_bar.dart b/lib/screens/shared/floating_search_bar.dart index f4fe13d..448ba79 100644 --- a/lib/screens/shared/floating_search_bar.dart +++ b/lib/screens/shared/floating_search_bar.dart @@ -47,7 +47,7 @@ class _FloatingSearchBarState extends ConsumerState { closedColor: Colors.transparent, closedElevation: 0, closedBuilder: (context, openAction) => Card( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, shadowColor: Colors.transparent, elevation: 5, margin: EdgeInsets.zero, diff --git a/lib/screens/shared/media/carousel_banner.dart b/lib/screens/shared/media/carousel_banner.dart index 2294425..5db17c5 100644 --- a/lib/screens/shared/media/carousel_banner.dart +++ b/lib/screens/shared/media/carousel_banner.dart @@ -7,7 +7,7 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/media/banner_play_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; @@ -51,16 +51,15 @@ class _CarouselBannerState extends ConsumerState { final itemExtent = widget.items.length == 1 ? MediaQuery.sizeOf(context).width : maxExtent; return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4) - .copyWith(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10), + padding: EdgeInsets.only(top: AdaptiveLayout.of(context).isDesktop ? 6 : 10), child: Stack( children: [ CarouselView( elevation: 3, shrinkExtent: 0, controller: carouselController, - shape: RoundedRectangleBorder(borderRadius: border), padding: const EdgeInsets.symmetric(horizontal: 6), + shape: RoundedRectangleBorder(borderRadius: border), enableSplash: false, itemExtent: itemExtent, children: [ @@ -146,8 +145,8 @@ class _CarouselBannerState extends ConsumerState { ? null : (details) async { Offset localPosition = details.globalPosition; - RelativeRect position = RelativeRect.fromLTRB(localPosition.dx - 320, - localPosition.dy, localPosition.dx, localPosition.dy); + RelativeRect position = RelativeRect.fromLTRB( + localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); final poster = widget.items[index]; await showMenu( diff --git a/lib/screens/shared/media/chapter_row.dart b/lib/screens/shared/media/chapter_row.dart index e92d7ad..c5badcd 100644 --- a/lib/screens/shared/media/chapter_row.dart +++ b/lib/screens/shared/media/chapter_row.dart @@ -5,7 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/chapters_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/humanize_duration.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -67,8 +67,8 @@ class ChapterRow extends ConsumerWidget { FlatButton( onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; - RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx - 80, localPosition.dy, localPosition.dx, localPosition.dy); + RelativeRect position = + RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); await showMenu( context: context, position: position, diff --git a/lib/screens/shared/media/components/next_up_episode.dart b/lib/screens/shared/media/components/next_up_episode.dart index fabdd75..9736e25 100644 --- a/lib/screens/shared/media/components/next_up_episode.dart +++ b/lib/screens/shared/media/components/next_up_episode.dart @@ -7,7 +7,7 @@ import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/screens/details_screens/components/media_stream_information.dart'; import 'package:fladder/screens/shared/media/episode_posters.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sticky_header_text.dart'; import 'package:fladder/util/string_extensions.dart'; diff --git a/lib/screens/shared/media/components/poster_image.dart b/lib/screens/shared/media/components/poster_image.dart index 882f76b..93773fb 100644 --- a/lib/screens/shared/media/components/poster_image.dart +++ b/lib/screens/shared/media/components/poster_image.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.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/models/book_model.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -11,7 +11,7 @@ import 'package:fladder/models/items/series_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/media/components/poster_placeholder.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/disable_keypad_focus.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/humanize_duration.dart'; @@ -137,7 +137,7 @@ class _PosterImageState extends ConsumerState { border: Border.all(width: 3, color: Theme.of(context).colorScheme.primary), borderRadius: FladderTheme.defaultShape.borderRadius, ), - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: Stack( alignment: Alignment.topCenter, children: [ @@ -225,7 +225,7 @@ class _PosterImageState extends ConsumerState { onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy); + localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); await showMenu( context: context, position: position, diff --git a/lib/screens/shared/media/episode_details_list.dart b/lib/screens/shared/media/episode_details_list.dart index 3dfd80f..f2bc2b6 100644 --- a/lib/screens/shared/media/episode_details_list.dart +++ b/lib/screens/shared/media/episode_details_list.dart @@ -2,7 +2,7 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/screens/shared/media/episode_posters.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; diff --git a/lib/screens/shared/media/episode_posters.dart b/lib/screens/shared/media/episode_posters.dart index 33d8df1..653a034 100644 --- a/lib/screens/shared/media/episode_posters.dart +++ b/lib/screens/shared/media/episode_posters.dart @@ -9,7 +9,7 @@ import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/syncing/sync_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; @@ -241,12 +241,13 @@ class EpisodePoster extends ConsumerWidget { LayoutBuilder( builder: (context, constraints) { return FlatButton( - onSecondaryTapDown: (details) { + onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx - 260, localPosition.dy, localPosition.dx, localPosition.dy); + localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); - showMenu(context: context, position: position, items: actions.popupMenuItems(useIcons: true)); + await showMenu( + context: context, position: position, items: actions.popupMenuItems(useIcons: true)); }, onTap: onTap, onLongPress: onLongPress, diff --git a/lib/screens/shared/media/expanding_overview.dart b/lib/screens/shared/media/expanding_overview.dart index d0508a9..4c2137c 100644 --- a/lib/screens/shared/media/expanding_overview.dart +++ b/lib/screens/shared/media/expanding_overview.dart @@ -1,9 +1,11 @@ -import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/util/localization_helper.dart'; -import 'package:fladder/util/sticky_header_text.dart'; import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; + +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/sticky_header_text.dart'; class ExpandingOverview extends ConsumerStatefulWidget { final String text; @@ -68,11 +70,11 @@ class _ExpandingOverviewState extends ConsumerState { child: expanded ? IconButton.filledTonal( onPressed: toggleState, - icon: const Icon(IconsaxPlusLinear.arrow_up_2), + icon: const Icon(IconsaxPlusLinear.arrow_up_1), ) : IconButton.filledTonal( onPressed: toggleState, - icon: const Icon(IconsaxPlusLinear.arrow_down_1), + icon: const Icon(IconsaxPlusLinear.arrow_down), ), ), ), diff --git a/lib/screens/shared/media/external_urls.dart b/lib/screens/shared/media/external_urls.dart index b796bcc..e0d2ff0 100644 --- a/lib/screens/shared/media/external_urls.dart +++ b/lib/screens/shared/media/external_urls.dart @@ -6,7 +6,7 @@ import 'package:url_launcher/url_launcher.dart' as urilauncher; import 'package:url_launcher/url_launcher_string.dart'; import 'package:fladder/models/items/item_shared_models.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sticky_header_text.dart'; diff --git a/lib/screens/shared/media/item_detail_list_widget.dart b/lib/screens/shared/media/item_detail_list_widget.dart index 9b5e4f6..525a6b5 100644 --- a/lib/screens/shared/media/item_detail_list_widget.dart +++ b/lib/screens/shared/media/item_detail_list_widget.dart @@ -25,7 +25,7 @@ class _ItemDetailListWidgetState extends ConsumerState { return Card( elevation: widget.elevation, margin: EdgeInsets.zero, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: Stack( children: [ FlatButton( diff --git a/lib/screens/shared/media/media_banner.dart b/lib/screens/shared/media/media_banner.dart index 302de4e..ae4929a 100644 --- a/lib/screens/shared/media/media_banner.dart +++ b/lib/screens/shared/media/media_banner.dart @@ -1,13 +1,13 @@ import 'package:flutter/material.dart'; import 'package:async/async.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/models/item_base_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/media/banner_play_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; @@ -87,7 +87,7 @@ class _MediaBannerState extends ConsumerState { final double dragOpacity = (1 - dragOffset.abs()).clamp(0, 1); return Padding( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + padding: const EdgeInsets.symmetric(vertical: 6), child: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/screens/shared/media/people_row.dart b/lib/screens/shared/media/people_row.dart index f7b89d0..c8bf543 100644 --- a/lib/screens/shared/media/people_row.dart +++ b/lib/screens/shared/media/people_row.dart @@ -7,7 +7,7 @@ import 'package:fladder/jellyfin/jellyfin_open_api.enums.swagger.dart'; import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/screens/details_screens/person_detail_screen.dart'; import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/string_extensions.dart'; @@ -22,12 +22,14 @@ class PeopleRow extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { Widget placeHolder(String name) { - return Card( - child: FractionallySizedBox( - widthFactor: 0.4, + return Center( + child: SizedBox( + height: 75, + width: 75, child: Card( elevation: 5, - shape: const CircleBorder(), + shadowColor: Colors.transparent, + color: Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.50), child: Center( child: Text( name.getInitials(), diff --git a/lib/screens/shared/media/poster_grid.dart b/lib/screens/shared/media/poster_grid.dart index cce4cd1..fd5d553 100644 --- a/lib/screens/shared/media/poster_grid.dart +++ b/lib/screens/shared/media/poster_grid.dart @@ -1,11 +1,13 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:sticky_headers/sticky_headers.dart'; + import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/screens/shared/media/poster_widget.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/sticky_header_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:sticky_headers/sticky_headers.dart'; class PosterGrid extends ConsumerWidget { final String? name; diff --git a/lib/screens/shared/media/poster_list_item.dart b/lib/screens/shared/media/poster_list_item.dart index 51fa691..035082f 100644 --- a/lib/screens/shared/media/poster_list_item.dart +++ b/lib/screens/shared/media/poster_list_item.dart @@ -1,10 +1,13 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; + import 'package:fladder/models/book_model.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; -import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; @@ -12,8 +15,6 @@ import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/shared/clickable_text.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class PosterListItem extends ConsumerWidget { final ItemBaseModel poster; @@ -64,12 +65,12 @@ class PosterListItem extends ConsumerWidget { color: Theme.of(context).colorScheme.primary.withValues(alpha: selected == true ? 0.25 : 0), borderRadius: BorderRadius.circular(6), ), - child: FlatButton( + child: InkWell( onTap: () => pressedWidget(context), onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; RelativeRect position = - RelativeRect.fromLTRB(localPosition.dx - 320, localPosition.dy, localPosition.dx, localPosition.dy); + RelativeRect.fromLTRB(localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); await showMenu( context: context, position: position, diff --git a/lib/screens/shared/media/poster_row.dart b/lib/screens/shared/media/poster_row.dart index 41a907e..a6f38f2 100644 --- a/lib/screens/shared/media/poster_row.dart +++ b/lib/screens/shared/media/poster_row.dart @@ -1,18 +1,23 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/screens/shared/media/poster_widget.dart'; +import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/widgets/shared/horizontal_list.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class PosterRow extends ConsumerStatefulWidget { final List posters; final String label; + final double? collectionAspectRatio; final Function()? onLabelClick; final EdgeInsets contentPadding; const PosterRow({ required this.posters, this.contentPadding = const EdgeInsets.symmetric(horizontal: 16), required this.label, + this.collectionAspectRatio, this.onLabelClick, super.key, }); @@ -32,6 +37,7 @@ class _PosterRowState extends ConsumerState { @override Widget build(BuildContext context) { + final dominantRatio = widget.collectionAspectRatio ?? widget.posters.getMostCommonType.aspectRatio; return HorizontalList( contentPadding: widget.contentPadding, label: widget.label, @@ -41,6 +47,7 @@ class _PosterRowState extends ConsumerState { final poster = widget.posters[index]; return PosterWidget( poster: poster, + aspectRatio: dominantRatio, key: Key(poster.id), ); }, diff --git a/lib/screens/shared/media/poster_widget.dart b/lib/screens/shared/media/poster_widget.dart index f128ad5..4fd4981 100644 --- a/lib/screens/shared/media/poster_widget.dart +++ b/lib/screens/shared/media/poster_widget.dart @@ -4,9 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/screens/shared/media/components/poster_image.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/item_base_model/play_item_helpers.dart'; import 'package:fladder/widgets/shared/clickable_text.dart'; diff --git a/lib/screens/shared/media/season_row.dart b/lib/screens/shared/media/season_row.dart index 73c817f..375692f 100644 --- a/lib/screens/shared/media/season_row.dart +++ b/lib/screens/shared/media/season_row.dart @@ -4,7 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; @@ -124,11 +124,11 @@ class SeasonPoster extends ConsumerWidget { LayoutBuilder( builder: (context, constraints) { return FlatButton( - onSecondaryTapDown: (details) { + onSecondaryTapDown: (details) async { Offset localPosition = details.globalPosition; RelativeRect position = RelativeRect.fromLTRB( - localPosition.dx - 260, localPosition.dy, localPosition.dx, localPosition.dy); - showMenu( + localPosition.dx, localPosition.dy, localPosition.dx, localPosition.dy); + await showMenu( context: context, position: position, items: season.generateActions(context, ref).popupMenuItems(useIcons: true)); diff --git a/lib/screens/shared/nested_bottom_appbar.dart b/lib/screens/shared/nested_bottom_appbar.dart index 15c7cbf..da99db5 100644 --- a/lib/screens/shared/nested_bottom_appbar.dart +++ b/lib/screens/shared/nested_bottom_appbar.dart @@ -1,7 +1,5 @@ -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:fladder/widgets/shared/shapes.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; class NestedBottomAppBar extends ConsumerWidget { @@ -10,27 +8,17 @@ class NestedBottomAppBar extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final double bottomPadding = - (AdaptiveLayout.of(context).isDesktop || kIsWeb) ? 12 : MediaQuery.of(context).padding.bottom; - return Card( - color: Theme.of(context).colorScheme.surface, - shape: BottomBarShape(), - elevation: 0, - child: Padding( - padding: const EdgeInsets.only(top: 8), - child: SizedBox( - height: kBottomNavigationBarHeight + 12 + bottomPadding, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12) - .copyWith( - bottom: bottomPadding, - ) - .add(EdgeInsets.only( - left: MediaQuery.of(context).padding.left, - right: MediaQuery.of(context).padding.right, - )), - child: child, - ), + return Padding( + padding: const EdgeInsets.all(8.0).copyWith(bottom: MediaQuery.paddingOf(context).bottom), + child: Card( + color: Theme.of(context).colorScheme.surfaceContainerLow, + elevation: 5, + shape: RoundedRectangleBorder( + borderRadius: BorderRadiusDirectional.circular(24), + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: child, ), ), ); diff --git a/lib/screens/shared/nested_scaffold.dart b/lib/screens/shared/nested_scaffold.dart index aed43c7..7971dc4 100644 --- a/lib/screens/shared/nested_scaffold.dart +++ b/lib/screens/shared/nested_scaffold.dart @@ -2,38 +2,38 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/media_playback_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/providers/video_player_provider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; - class NestedScaffold extends ConsumerWidget { final Widget body; - const NestedScaffold({required this.body, super.key}); + final Widget? background; + const NestedScaffold({ + required this.body, + this.background, + super.key, + }); @override Widget build(BuildContext context, WidgetRef ref) { - final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state)); - - return Card( - child: Scaffold( - backgroundColor: Colors.transparent, - floatingActionButtonAnimator: - playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null, - floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, - floatingActionButton: switch (AdaptiveLayout.layoutOf(context)) { - ViewSize.phone => null, - _ => switch (playerState) { - VideoPlayerState.minimized => const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: FloatingPlayerBar(), - ), - _ => null, - }, - }, - body: body, - ), + return Stack( + alignment: Alignment.bottomCenter, + children: [ + if (background != null) background!, + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).colorScheme.surface.withValues(alpha: 0.98), + Theme.of(context).colorScheme.surface.withValues(alpha: 0.8), + ], + ), + ), + child: Scaffold( + backgroundColor: Colors.transparent, + body: body, + ), + ), + ], ); } } diff --git a/lib/screens/shared/nested_sliver_appbar.dart b/lib/screens/shared/nested_sliver_appbar.dart index bd23967..c2a212d 100644 --- a/lib/screens/shared/nested_sliver_appbar.dart +++ b/lib/screens/shared/nested_sliver_appbar.dart @@ -1,11 +1,12 @@ +import 'package:flutter/material.dart'; + import 'package:auto_route/auto_route.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/util/list_padding.dart'; + import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; import 'package:fladder/widgets/shared/shapes.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class NestedSliverAppBar extends ConsumerWidget { final BuildContext parent; @@ -29,15 +30,14 @@ class NestedSliverAppBar extends ConsumerWidget { padding: const EdgeInsets.only(bottom: 24), child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 10, children: [ - IconButton.filledTonal( - style: ButtonStyle( - backgroundColor: WidgetStatePropertyAll(Theme.of(context).colorScheme.surface), - ), - onPressed: () => Scaffold.of(parent).openDrawer(), - icon: const Icon( - IconsaxPlusBold.menu, - size: 28, + SizedBox( + width: 30, + child: IconButton( + onPressed: () => Scaffold.of(parent).openDrawer(), + icon: const Icon(IconsaxPlusLinear.menu), + padding: EdgeInsets.zero, ), ), Expanded( @@ -62,8 +62,9 @@ class NestedSliverAppBar extends ConsumerWidget { const Icon(IconsaxPlusLinear.search_normal), const SizedBox(width: 16), Transform.translate( - offset: const Offset(0, 2.5), - child: Text(searchTitle ?? "${context.localized.search}...")), + offset: const Offset(0, 1.5), + child: Text(searchTitle ?? "${context.localized.search}..."), + ), ], ), ), @@ -73,7 +74,7 @@ class NestedSliverAppBar extends ConsumerWidget { ), ), const SettingsUserIcon() - ].addInBetween(const SizedBox(width: 16)), + ], ), ), ), diff --git a/lib/screens/shared/user_icon.dart b/lib/screens/shared/user_icon.dart index d8aa2f6..47a9149 100644 --- a/lib/screens/shared/user_icon.dart +++ b/lib/screens/shared/user_icon.dart @@ -49,7 +49,7 @@ class UserIcon extends ConsumerWidget { elevation: 0, surfaceTintColor: Colors.transparent, color: Colors.transparent, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: SizedBox.fromSize( size: size, child: Stack( diff --git a/lib/screens/syncing/sync_item_details.dart b/lib/screens/syncing/sync_item_details.dart index 2cb2180..0b4a276 100644 --- a/lib/screens/syncing/sync_item_details.dart +++ b/lib/screens/syncing/sync_item_details.dart @@ -17,7 +17,7 @@ import 'package:fladder/screens/syncing/sync_child_item.dart'; import 'package:fladder/screens/syncing/sync_widgets.dart'; import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart'; import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/size_formatting.dart'; diff --git a/lib/screens/syncing/sync_list_item.dart b/lib/screens/syncing/sync_list_item.dart index 49c38d7..c759c29 100644 --- a/lib/screens/syncing/sync_list_item.dart +++ b/lib/screens/syncing/sync_list_item.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.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/models/syncing/sync_item.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart'; @@ -35,7 +35,7 @@ class SyncListItemState extends ConsumerState { syncedItem: syncedItem, child: Card( elevation: 1, - color: Theme.of(context).colorScheme.secondaryContainer.withValues(alpha: 0.2), + color: Theme.of(context).colorScheme.surfaceDim, shadowColor: Colors.transparent, child: Dismissible( background: Container( diff --git a/lib/screens/syncing/synced_screen.dart b/lib/screens/syncing/synced_screen.dart index b5d3279..fcd7832 100644 --- a/lib/screens/syncing/synced_screen.dart +++ b/lib/screens/syncing/synced_screen.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.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/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/shared/nested_scaffold.dart'; import 'package:fladder/screens/shared/nested_sliver_appbar.dart'; import 'package:fladder/screens/syncing/sync_list_item.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/sliver_list_padding.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/background_image.dart'; import 'package:fladder/widgets/shared/pinch_poster_zoom.dart'; import 'package:fladder/widgets/shared/pull_to_refresh.dart'; @@ -31,48 +31,49 @@ class _SyncedScreenState extends ConsumerState { @override Widget build(BuildContext context) { final items = ref.watch(syncProvider.select((value) => value.items)); + final padding = AdaptiveLayout.adaptivePadding(context); + return PullToRefresh( refreshOnStart: true, onRefresh: () => ref.read(syncProvider.notifier).refresh(), child: NestedScaffold( + background: BackgroundImage(images: items.map((value) => value.images).nonNulls.toList()), body: PinchPosterZoom( scaleDifference: (difference) => ref.read(clientSettingsProvider.notifier).addPosterSize(difference / 2), - child: items.isNotEmpty - ? CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - controller: widget.navigationScrollController, - slivers: [ - if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) - NestedSliverAppBar( - searchTitle: "${context.localized.search} ...", - parent: context, - route: LibrarySearchRoute(), - ) - else - const DefaultSliverTopBadding(), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - context.localized.syncedItems, - style: Theme.of(context).textTheme.titleLarge, - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16), - sliver: SliverList.builder( - itemBuilder: (context, index) { - final item = items[index]; - return SyncListItem(syncedItem: item); - }, - itemCount: items.length, - ), - ), - const DefautlSliverBottomPadding(), - ], + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + controller: widget.navigationScrollController, + slivers: [ + if (AdaptiveLayout.viewSizeOf(context) == ViewSize.phone) + NestedSliverAppBar( + searchTitle: "${context.localized.search} ...", + parent: context, + route: LibrarySearchRoute(), ) - : Center( + else + const DefaultSliverTopBadding(), + if (items.isNotEmpty) ...[ + SliverToBoxAdapter( + child: Padding( + padding: padding, + child: Text( + context.localized.syncedItems, + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + SliverPadding( + padding: padding, + sliver: SliverList.builder( + itemBuilder: (context, index) { + final item = items[index]; + return SyncListItem(syncedItem: item); + }, + itemCount: items.length, + ), + ), + ] else ...[ + SliverFillRemaining( child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, @@ -87,7 +88,11 @@ class _SyncedScreenState extends ConsumerState { ) ], ), - ), + ) + ], + const DefautlSliverBottomPadding(), + ], + ), ), ), ); diff --git a/lib/screens/video_player/components/video_playback_information.dart b/lib/screens/video_player/components/video_playback_information.dart index 0b575ad..049ef8e 100644 --- a/lib/screens/video_player/components/video_playback_information.dart +++ b/lib/screens/video_player/components/video_playback_information.dart @@ -2,8 +2,8 @@ import 'dart:ui'; import 'package:flutter/material.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/models/playback/playback_model.dart'; import 'package:fladder/providers/session_info_provider.dart'; @@ -33,112 +33,117 @@ class _VideoPlaybackInformation extends ConsumerWidget { return Dialog( child: Padding( padding: const EdgeInsets.all(12.0), - child: IntrinsicWidth( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text("Player info", style: Theme.of(context).textTheme.titleMedium), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4), - child: Opacity( - opacity: 0.80, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [const Text('backend: '), Text(backend?.label(context) ?? context.localized.unknown)], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('url: '), - const SizedBox(width: 8), - Flexible( - child: ImageFiltered( - imageFilter: ImageFilter.blur( - sigmaX: 3.0, - sigmaY: 3.0, - ), - child: Text( - playbackModel?.media?.url ?? "No url", - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: SingleChildScrollView( + child: IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("Player info", style: Theme.of(context).textTheme.titleMedium), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4), + child: Opacity( + opacity: 0.80, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('backend: '), + Text(backend?.label(context) ?? context.localized.unknown) + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('url: '), + const SizedBox(width: 8), + Flexible( + child: ImageFiltered( + imageFilter: ImageFilter.blur( + sigmaX: 3.0, + sigmaY: 3.0, + ), + child: Text( + playbackModel?.media?.url ?? "No url", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), ), - ), - IconButton.filled( - onPressed: () => context.copyToClipboard(playbackModel?.media?.url ?? "No url"), - icon: const Icon(IconsaxPlusLinear.copy), - ) - ], - ) - ].addPadding(const EdgeInsets.symmetric(vertical: 3)), + IconButton.filled( + onPressed: () => context.copyToClipboard(playbackModel?.media?.url ?? "No url"), + icon: const Icon(IconsaxPlusLinear.copy), + ) + ], + ) + ].addPadding(const EdgeInsets.symmetric(vertical: 3)), + ), ), ), - ), - const Divider(), - if (playbackState != null) _PlayerInformation(state: playbackState), - Text("Playback information", style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 4), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4), - child: Opacity( - opacity: 0.8, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisSize: MainAxisSize.min, - children: [const Text('type: '), Text(playbackModel.label ?? "")], - ), - if (sessionInfo.transCodeInfo != null) ...[ - Text("Transcoding", style: Theme.of(context).textTheme.titleMedium), - if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('reason: '), - Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "") - ], - ), - if (sessionInfo.transCodeInfo?.completionPercentage != null) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('transcode progress: '), - Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %") - ], - ), - if (sessionInfo.transCodeInfo?.container != null) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('container: '), - Text(sessionInfo.transCodeInfo!.container.toString()) - ], - ), - ], - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('resolution: '), - Text(playbackModel?.item.streamModel?.resolutionText ?? "") + const Divider(), + if (playbackState != null) _PlayerInformation(state: playbackState), + Text("Playback information", style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 4), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4).copyWith(top: 4), + child: Opacity( + opacity: 0.8, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [const Text('type: '), Text(playbackModel.label(context) ?? "")], + ), + if (sessionInfo.transCodeInfo != null) ...[ + Text("Transcoding", style: Theme.of(context).textTheme.titleMedium), + if (sessionInfo.transCodeInfo?.transcodeReasons?.isNotEmpty == true) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('reason: '), + Text(sessionInfo.transCodeInfo?.transcodeReasons.toString() ?? "") + ], + ), + if (sessionInfo.transCodeInfo?.completionPercentage != null) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('transcode progress: '), + Text("${sessionInfo.transCodeInfo?.completionPercentage?.toStringAsFixed(2)} %") + ], + ), + if (sessionInfo.transCodeInfo?.container != null) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('container: '), + Text(sessionInfo.transCodeInfo!.container.toString()) + ], + ), ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('container: '), - Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "") - ], - ) - ].addPadding(const EdgeInsets.symmetric(vertical: 3)), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('resolution: '), + Text(playbackModel?.item.streamModel?.resolutionText ?? "") + ], + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('container: '), + Text(playbackModel?.playbackInfo?.mediaSources?.firstOrNull?.container ?? "") + ], + ) + ].addPadding(const EdgeInsets.symmetric(vertical: 3)), + ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/lib/screens/video_player/components/video_player_chapters.dart b/lib/screens/video_player/components/video_player_chapters.dart index 26035b5..09328ee 100644 --- a/lib/screens/video_player/components/video_player_chapters.dart +++ b/lib/screens/video_player/components/video_player_chapters.dart @@ -50,7 +50,7 @@ class VideoPlayerChapters extends ConsumerWidget { final isCurrent = chapter == currentChapter; return Card( color: Colors.black, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: Stack( children: [ Center( diff --git a/lib/screens/video_player/components/video_player_controls_extras.dart b/lib/screens/video_player/components/video_player_controls_extras.dart index 32f0ad2..b44e790 100644 --- a/lib/screens/video_player/components/video_player_controls_extras.dart +++ b/lib/screens/video_player/components/video_player_controls_extras.dart @@ -66,13 +66,13 @@ class OpenQueueButton extends ConsumerWidget { class SkipSegmentButton extends ConsumerWidget { final MediaSegment? segment; final SegmentSkip? skipType; - final bool isOverlayVisible; + final SegmentVisibility visibility; final Function() pressedSkip; const SkipSegmentButton({ required this.segment, this.skipType, - required this.isOverlayVisible, + required this.visibility, required this.pressedSkip, super.key, }); @@ -82,7 +82,11 @@ class SkipSegmentButton extends ConsumerWidget { return AnimatedFadeSize( child: segment != null && skipType != SegmentSkip.none ? AnimatedOpacity( - opacity: isOverlayVisible ? 1 : 0.15, + opacity: switch (visibility) { + SegmentVisibility.hidden => 0, + SegmentVisibility.partially => 0.15, + SegmentVisibility.visible => 1.0, + }, duration: const Duration(milliseconds: 500), child: ElevatedButton( onPressed: pressedSkip, diff --git a/lib/screens/video_player/components/video_player_next_wrapper.dart b/lib/screens/video_player/components/video_player_next_wrapper.dart index b66d313..ffbdad0 100644 --- a/lib/screens/video_player/components/video_player_next_wrapper.dart +++ b/lib/screens/video_player/components/video_player_next_wrapper.dart @@ -16,13 +16,12 @@ import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/default_title_bar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; -import 'package:fladder/widgets/shared/full_screen_button.dart' - if (dart.library.html) 'package:fladder/widgets/shared/full_screen_button_web.dart'; import 'package:fladder/widgets/shared/progress_floating_button.dart'; class VideoPlayerNextWrapper extends ConsumerStatefulWidget { @@ -132,7 +131,7 @@ class _VideoPlayerNextWrapperState extends ConsumerState if (AdaptiveLayout.of(context).inputDevice != InputDevice.pointer) { ScreenBrightness().resetApplicationScreenBrightness(); } else { - closeFullScreen(); + fullScreenHelper.closeFullScreen(ref); } SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( diff --git a/lib/screens/video_player/components/video_player_options_sheet.dart b/lib/screens/video_player/components/video_player_options_sheet.dart index d708f14..affbe25 100644 --- a/lib/screens/video_player/components/video_player_options_sheet.dart +++ b/lib/screens/video_player/components/video_player_options_sheet.dart @@ -22,7 +22,7 @@ import 'package:fladder/screens/playlists/add_to_playlists.dart'; import 'package:fladder/screens/video_player/components/video_player_quality_controls.dart'; import 'package:fladder/screens/video_player/components/video_player_queue.dart'; import 'package:fladder/screens/video_player/components/video_subtitle_controls.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/device_orientation_extension.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -502,9 +502,9 @@ Future showPlaybackSpeed(BuildContext context) { width: 250, child: FladderSlider( min: 0.25, - max: 10, + max: 3, value: lastSpeed, - divisions: 39, + divisions: 55, onChanged: (value) { ref.read(playbackRateProvider.notifier).state = value; player.setSpeed(value); diff --git a/lib/screens/video_player/components/video_subtitle_controls.dart b/lib/screens/video_player/components/video_subtitle_controls.dart index de66028..e82e716 100644 --- a/lib/screens/video_player/components/video_subtitle_controls.dart +++ b/lib/screens/video_player/components/video_subtitle_controls.dart @@ -199,7 +199,7 @@ class _VideoSubtitleControlsState extends ConsumerState { (e) => FlatButton( onTap: () => provider.setSubColor(e), borderRadiusGeometry: BorderRadius.circular(5), - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: Container( height: 25, width: 25, @@ -223,7 +223,7 @@ class _VideoSubtitleControlsState extends ConsumerState { onTap: () => provider .setOutlineColor(e == Colors.transparent ? e : e.withValues(alpha: 0.85)), borderRadiusGeometry: BorderRadius.circular(5), - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, child: Container( height: 25, width: 25, diff --git a/lib/screens/video_player/video_player.dart b/lib/screens/video_player/video_player.dart index 56f91a5..59feea0 100644 --- a/lib/screens/video_player/video_player.dart +++ b/lib/screens/video_player/video_player.dart @@ -11,7 +11,7 @@ import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/video_player/components/video_player_next_wrapper.dart'; import 'package:fladder/screens/video_player/video_player_controls.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/themes_data.dart'; class VideoPlayer extends ConsumerStatefulWidget { diff --git a/lib/screens/video_player/video_player_controls.dart b/lib/screens/video_player/video_player_controls.dart index 895e0bb..4216b0f 100644 --- a/lib/screens/video_player/video_player_controls.dart +++ b/lib/screens/video_player/video_player_controls.dart @@ -13,7 +13,6 @@ import 'package:screen_brightness/screen_brightness.dart'; import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/playback/playback_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; @@ -26,14 +25,13 @@ import 'package:fladder/screens/video_player/components/video_player_quality_con import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart'; import 'package:fladder/screens/video_player/components/video_progress_bar.dart'; import 'package:fladder/screens/video_player/components/video_volume_slider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/input_handler.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/string_extensions.dart'; -import 'package:fladder/widgets/shared/full_screen_button.dart' - if (dart.library.html) 'package:fladder/widgets/shared/full_screen_button_web.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; class DesktopControls extends ConsumerStatefulWidget { const DesktopControls({super.key}); @@ -87,7 +85,7 @@ class _DesktopControlsState extends ConsumerState { return true; } if (value.logicalKey == LogicalKeyboardKey.keyF) { - toggleFullScreen(ref); + fullScreenHelper.toggleFullScreen(ref); return true; } if (value.logicalKey == LogicalKeyboardKey.arrowUp) { @@ -138,7 +136,7 @@ class _DesktopControlsState extends ConsumerState { ? () => player.playOrPause() : () => toggleOverlay(), onDoubleTap: AdaptiveLayout.of(context).inputDevice == InputDevice.pointer - ? () => toggleFullScreen(ref) + ? () => fullScreenHelper.toggleFullScreen(ref) : null, ), ), @@ -168,11 +166,13 @@ class _DesktopControlsState extends ConsumerState { builder: (context, ref, child) { final position = ref.watch(mediaPlaybackProvider.select((value) => value.position)); MediaSegment? segment = mediaSegments?.atPosition(position); - bool forceShow = segment?.forceShow(position) ?? false; + SegmentVisibility forceShow = + segment?.visibility(position, force: showOverlay) ?? SegmentVisibility.hidden; final segmentSkipType = ref .watch(videoPlayerSettingsProvider.select((value) => value.segmentSkipSettings[segment?.type])); - final autoSkip = - forceShow == true && segmentSkipType == SegmentSkip.skip && player.lastState?.buffering == false; + final autoSkip = forceShow != SegmentVisibility.hidden && + segmentSkipType == SegmentSkip.skip && + player.lastState?.buffering == false; if (autoSkip) { skipToSegmentEnd(segment); } @@ -185,7 +185,7 @@ class _DesktopControlsState extends ConsumerState { child: SkipSegmentButton( segment: segment, skipType: segmentSkipType, - isOverlayVisible: forceShow ? true : showOverlay, + visibility: forceShow, pressedSkip: () => skipToSegmentEnd(segment), ), ), @@ -441,7 +441,9 @@ class _DesktopControlsState extends ConsumerState { final List details = [ if (AdaptiveLayout.of(context).isDesktop) item?.label(context), mediaPlayback.duration.inMinutes > 1 - ? context.localized.endsAt(DateTime.now().add(mediaPlayback.duration - mediaPlayback.position)) + ? context.localized.endsAt(DateTime.now().add(Duration( + milliseconds: (mediaPlayback.duration.inMilliseconds - mediaPlayback.position.inMilliseconds) ~/ + ref.read(playbackRateProvider)))) : null ]; return Column( @@ -459,14 +461,14 @@ class _DesktopControlsState extends ConsumerState { ), ), const Spacer(), - if (playbackModel.label != null) + if (playbackModel != null) InkWell( onTap: () => showVideoPlaybackInformation(context), child: Card( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Text( - playbackModel?.label ?? "", + playbackModel.label(context) ?? "", ), ), ), @@ -662,6 +664,6 @@ class _DesktopControlsState extends ConsumerState { Future disableFullScreen() async { resetTimer(); - closeFullScreen(); + fullScreenHelper.closeFullScreen(ref); } } diff --git a/lib/theme.dart b/lib/theme.dart index f9b521d..620e283 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -30,9 +30,9 @@ ColorScheme _insertAdditionalColours(ColorScheme scheme) => scheme.copyWith( ); class FladderTheme { - static RoundedRectangleBorder get smallShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)); - static RoundedRectangleBorder get defaultShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)); - static RoundedRectangleBorder get largeShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)); + static RoundedRectangleBorder get smallShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)); + static RoundedRectangleBorder get defaultShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)); + static RoundedRectangleBorder get largeShape => RoundedRectangleBorder(borderRadius: BorderRadius.circular(32)); static Color get darkBackgroundColor => const Color.fromARGB(255, 10, 10, 10); static Color get lightBackgroundColor => const Color.fromARGB(237, 255, 255, 255); @@ -50,11 +50,11 @@ class FladderTheme { trackHeight: 8, thumbColor: colorScheme?.onSurface, ), - cardTheme: CardTheme( + cardTheme: CardThemeData( elevation: 3, clipBehavior: Clip.antiAlias, margin: EdgeInsets.zero, - shape: defaultShape, + shape: smallShape, ), progressIndicatorTheme: const ProgressIndicatorThemeData(), floatingActionButtonTheme: FloatingActionButtonThemeData( @@ -95,7 +95,7 @@ class FladderTheme { ), ), navigationBarTheme: const NavigationBarThemeData(), - dialogTheme: DialogTheme(shape: defaultShape), + dialogTheme: DialogThemeData(shape: defaultShape), scrollbarTheme: ScrollbarThemeData( radius: const Radius.circular(16), thumbColor: WidgetStateProperty.resolveWith((states) { @@ -123,7 +123,26 @@ class FladderTheme { iconColor: scheme?.onSecondaryContainer, surfaceTintColor: scheme?.onSecondaryContainer, ), - listTileTheme: ListTileThemeData(shape: defaultShape), + listTileTheme: ListTileThemeData( + shape: defaultShape, + ), + dividerTheme: const DividerThemeData( + indent: 6, + endIndent: 6, + ), + segmentedButtonTheme: SegmentedButtonThemeData( + style: ButtonStyle( + backgroundColor: WidgetStateProperty.resolveWith((state) { + if (state.contains(WidgetState.selected)) { + return scheme?.primaryContainer; + } + return scheme?.surfaceContainer; + }), + padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 8, horizontal: 12)), + elevation: const WidgetStatePropertyAll(5), + side: const WidgetStatePropertyAll(BorderSide.none), + ), + ), elevatedButtonTheme: ElevatedButtonThemeData(style: ButtonStyle(shape: WidgetStatePropertyAll(defaultShape))), filledButtonTheme: FilledButtonThemeData(style: ButtonStyle(shape: WidgetStatePropertyAll(defaultShape))), outlinedButtonTheme: OutlinedButtonThemeData(style: ButtonStyle(shape: WidgetStatePropertyAll(defaultShape))), diff --git a/lib/util/adaptive_layout.dart b/lib/util/adaptive_layout.dart deleted file mode 100644 index 3638fa6..0000000 --- a/lib/util/adaptive_layout.dart +++ /dev/null @@ -1,198 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/providers/settings/home_settings_provider.dart'; -import 'package:fladder/util/debug_banner.dart'; -import 'package:fladder/util/poster_defaults.dart'; - -enum InputDevice { - touch, - pointer, -} - -class LayoutPoints { - final double start; - final double end; - final ViewSize type; - LayoutPoints({ - required this.start, - required this.end, - required this.type, - }); - - LayoutPoints copyWith({ - double? start, - double? end, - ViewSize? type, - }) { - return LayoutPoints( - start: start ?? this.start, - end: end ?? this.end, - type: type ?? this.type, - ); - } - - @override - String toString() => 'LayoutPoints(start: $start, end: $end, type: $type)'; - - @override - bool operator ==(covariant LayoutPoints other) { - if (identical(this, other)) return true; - - return other.start == start && other.end == end && other.type == type; - } - - @override - int get hashCode => start.hashCode ^ end.hashCode ^ type.hashCode; -} - -class AdaptiveLayout extends InheritedWidget { - final ViewSize viewSize; - final LayoutMode layoutMode; - final InputDevice inputDevice; - final TargetPlatform platform; - final bool isDesktop; - final PosterDefaults posterDefaults; - final ScrollController controller; - - const AdaptiveLayout({ - super.key, - required this.viewSize, - required this.layoutMode, - required this.inputDevice, - required this.platform, - required this.isDesktop, - required this.posterDefaults, - required this.controller, - required super.child, - }); - - static AdaptiveLayout? maybeOf(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType(); - } - - static ViewSize layoutOf(BuildContext context) { - final AdaptiveLayout? result = maybeOf(context); - return result!.viewSize; - } - - static PosterDefaults poster(BuildContext context) { - final AdaptiveLayout? result = maybeOf(context); - return result!.posterDefaults; - } - - static AdaptiveLayout of(BuildContext context) { - final AdaptiveLayout? result = maybeOf(context); - return result!; - } - - static ScrollController scrollOf(BuildContext context) { - final AdaptiveLayout? result = maybeOf(context); - return result!.controller; - } - - static LayoutMode layoutModeOf(BuildContext context) => maybeOf(context)!.layoutMode; - static ViewSize viewSizeOf(BuildContext context) => maybeOf(context)!.viewSize; - - static InputDevice inputDeviceOf(BuildContext context) => maybeOf(context)!.inputDevice; - - @override - bool updateShouldNotify(AdaptiveLayout oldWidget) { - return viewSize != oldWidget.viewSize || - layoutMode != oldWidget.layoutMode || - platform != oldWidget.platform || - inputDevice != oldWidget.inputDevice || - isDesktop != oldWidget.isDesktop; - } -} - -const defaultTitleBarHeight = 35.0; - -class AdaptiveLayoutBuilder extends ConsumerStatefulWidget { - final List layoutPoints; - final ViewSize fallBack; - final Widget child; - const AdaptiveLayoutBuilder({required this.layoutPoints, required this.child, required this.fallBack, super.key}); - - @override - ConsumerState createState() => _AdaptiveLayoutBuilderState(); -} - -class _AdaptiveLayoutBuilderState extends ConsumerState { - late ViewSize viewSize = widget.fallBack; - late LayoutMode layoutMode = LayoutMode.single; - late TargetPlatform currentPlatform = defaultTargetPlatform; - late ScrollController controller = ScrollController(); - - @override - void didChangeDependencies() { - calculateLayout(); - calculateSize(); - super.didChangeDependencies(); - } - - bool get isDesktop { - if (kIsWeb) return false; - return [ - TargetPlatform.macOS, - TargetPlatform.windows, - TargetPlatform.linux, - ].contains(currentPlatform); - } - - void calculateLayout() { - ViewSize? newType; - for (var element in widget.layoutPoints) { - if (MediaQuery.of(context).size.width > element.start && MediaQuery.of(context).size.width < element.end) { - newType = element.type; - } - } - viewSize = newType ?? widget.fallBack; - } - - void calculateSize() { - LayoutMode newSize; - if (MediaQuery.of(context).size.width > 0 && MediaQuery.of(context).size.width < 960) { - newSize = LayoutMode.single; - } else { - newSize = LayoutMode.dual; - } - layoutMode = newSize; - } - - @override - Widget build(BuildContext context) { - final acceptedLayouts = ref.watch(homeSettingsProvider.select((value) => value.screenLayouts)); - final acceptedViewSizes = ref.watch(homeSettingsProvider.select((value) => value.layoutStates)); - return MediaQuery( - data: MediaQuery.of(context).copyWith( - padding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, - viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, - ), - child: AdaptiveLayout( - viewSize: selectAvailableOrSmaller(viewSize, acceptedViewSizes, ViewSize.values), - controller: controller, - layoutMode: selectAvailableOrSmaller(layoutMode, acceptedLayouts, LayoutMode.values), - inputDevice: (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch, - platform: currentPlatform, - isDesktop: isDesktop, - posterDefaults: switch (viewSize) { - ViewSize.phone => const PosterDefaults(size: 300, ratio: 0.55), - ViewSize.tablet => const PosterDefaults(size: 350, ratio: 0.55), - ViewSize.desktop => const PosterDefaults(size: 400, ratio: 0.55), - }, - child: DebugBanner(child: widget.child), - ), - ); - } -} - -double? get topPadding { - return switch (defaultTargetPlatform) { - TargetPlatform.linux || TargetPlatform.windows || TargetPlatform.macOS => 35, - _ => null - }; -} diff --git a/lib/util/adaptive_layout/adaptive_layout.dart b/lib/util/adaptive_layout/adaptive_layout.dart new file mode 100644 index 0000000..29fc6f0 --- /dev/null +++ b/lib/util/adaptive_layout/adaptive_layout.dart @@ -0,0 +1,228 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/settings/home_settings_model.dart'; +import 'package:fladder/providers/settings/home_settings_provider.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout_model.dart'; +import 'package:fladder/util/debug_banner.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/poster_defaults.dart'; +import 'package:fladder/util/resolution_checker.dart'; + +enum InputDevice { + touch, + pointer, +} + +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; +} + +class AdaptiveLayout extends InheritedWidget { + final AdaptiveLayoutModel data; + + const AdaptiveLayout({ + super.key, + required this.data, + required super.child, + }); + + static AdaptiveLayoutModel of(BuildContext context) { + final inherited = context.dependOnInheritedWidgetOfExactType(); + assert(inherited != null, 'No AdaptiveLayout found in context'); + return inherited!.data; + } + + static AdaptiveLayout? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + static ViewSize layoutOf(BuildContext context) { + final AdaptiveLayout? result = maybeOf(context); + return result!.data.viewSize; + } + + static PosterDefaults poster(BuildContext context) { + final AdaptiveLayout? result = maybeOf(context); + return result!.data.posterDefaults; + } + + static ScrollController scrollOf(BuildContext context) { + final AdaptiveLayout? result = maybeOf(context); + return result!.data.controller; + } + + static EdgeInsets adaptivePadding(BuildContext context, {double horizontalPadding = 16}) { + final viewPadding = MediaQuery.paddingOf(context); + final padding = viewPadding.copyWith( + left: AdaptiveLayout.of(context).sideBarWidth + horizontalPadding + viewPadding.left, + top: 0, + bottom: 0, + right: viewPadding.right + horizontalPadding, + ); + return padding; + } + + static LayoutMode layoutModeOf(BuildContext context) => maybeOf(context)!.data.layoutMode; + static ViewSize viewSizeOf(BuildContext context) => maybeOf(context)!.data.viewSize; + + static InputDevice inputDeviceOf(BuildContext context) => maybeOf(context)!.data.inputDevice; + + @override + bool updateShouldNotify(AdaptiveLayout oldWidget) => data != oldWidget.data; +} + +const defaultTitleBarHeight = 35.0; + +class AdaptiveLayoutBuilder extends ConsumerStatefulWidget { + final AdaptiveLayoutModel? adaptiveLayout; + final Widget Function(BuildContext context) child; + const AdaptiveLayoutBuilder({ + this.adaptiveLayout, + required this.child, + super.key, + }); + + @override + ConsumerState createState() => _AdaptiveLayoutBuilderState(); +} + +class _AdaptiveLayoutBuilderState extends ConsumerState { + List 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), + ]; + late ViewSize viewSize = ViewSize.tablet; + late LayoutMode layoutMode = LayoutMode.single; + late TargetPlatform currentPlatform = defaultTargetPlatform; + late ScrollController controller = ScrollController(); + + @override + void didChangeDependencies() { + calculateLayout(); + calculateSize(); + super.didChangeDependencies(); + } + + bool get isDesktop { + if (kIsWeb) return false; + return [ + TargetPlatform.macOS, + TargetPlatform.windows, + TargetPlatform.linux, + ].contains(currentPlatform); + } + + void calculateLayout() { + ViewSize? newType; + for (var element in layoutPoints) { + if (MediaQuery.of(context).size.width > element.start && MediaQuery.of(context).size.width < element.end) { + newType = element.type; + } + } + viewSize = newType ?? ViewSize.tablet; + } + + void calculateSize() { + LayoutMode newSize; + if (MediaQuery.of(context).size.width > 0 && MediaQuery.of(context).size.width < 960) { + newSize = LayoutMode.single; + } else { + newSize = LayoutMode.dual; + } + layoutMode = newSize; + } + + @override + Widget build(BuildContext context) { + final acceptedLayouts = ref.watch(homeSettingsProvider.select((value) => value.screenLayouts)); + final acceptedViewSizes = ref.watch(homeSettingsProvider.select((value) => value.layoutStates)); + + final selectedViewSize = selectAvailableOrSmaller(viewSize, acceptedViewSizes, ViewSize.values); + final selectedLayoutMode = selectAvailableOrSmaller(layoutMode, acceptedLayouts, LayoutMode.values); + final input = (isDesktop || kIsWeb) ? InputDevice.pointer : InputDevice.touch; + + final posterDefaults = switch (selectedViewSize) { + ViewSize.phone => const PosterDefaults(size: 300, ratio: 0.55), + ViewSize.tablet => const PosterDefaults(size: 350, ratio: 0.55), + ViewSize.desktop => const PosterDefaults(size: 400, ratio: 0.55), + }; + + final currentLayout = widget.adaptiveLayout ?? + AdaptiveLayoutModel( + viewSize: selectedViewSize, + layoutMode: selectedLayoutMode, + inputDevice: input, + platform: currentPlatform, + isDesktop: isDesktop, + sideBarWidth: 0, + controller: controller, + posterDefaults: posterDefaults, + ); + + return MediaQuery( + data: MediaQuery.of(context).copyWith( + padding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, + viewPadding: isDesktop || kIsWeb ? const EdgeInsets.only(top: defaultTitleBarHeight, bottom: 16) : null, + ), + child: AdaptiveLayout( + data: currentLayout.copyWith( + viewSize: selectedViewSize, + layoutMode: selectedLayoutMode, + inputDevice: input, + platform: currentPlatform, + isDesktop: isDesktop, + controller: controller, + posterDefaults: posterDefaults, + ), + child: Builder( + builder: (context) => ResolutionChecker( + child: widget.adaptiveLayout == null ? DebugBanner(child: widget.child(context)) : widget.child(context), + ), + ), + ), + ); + } +} + +double? get topPadding { + return switch (defaultTargetPlatform) { + TargetPlatform.linux || TargetPlatform.windows || TargetPlatform.macOS => 35, + _ => null + }; +} diff --git a/lib/util/adaptive_layout/adaptive_layout_model.dart b/lib/util/adaptive_layout/adaptive_layout_model.dart new file mode 100644 index 0000000..28149a9 --- /dev/null +++ b/lib/util/adaptive_layout/adaptive_layout_model.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/poster_defaults.dart'; + +class LayoutPoints { + final double start; + final double end; + final ViewSize type; + LayoutPoints({ + required this.start, + required this.end, + required this.type, + }); + + LayoutPoints copyWith({ + double? start, + double? end, + ViewSize? type, + }) { + return LayoutPoints( + start: start ?? this.start, + end: end ?? this.end, + type: type ?? this.type, + ); + } + + @override + String toString() => 'LayoutPoints(start: $start, end: $end, type: $type)'; + + @override + bool operator ==(covariant LayoutPoints other) { + if (identical(this, other)) return true; + return other.start == start && other.end == end && other.type == type; + } + + @override + int get hashCode => start.hashCode ^ end.hashCode ^ type.hashCode; +} + +@immutable +class AdaptiveLayoutModel { + final ViewSize viewSize; + final LayoutMode layoutMode; + final InputDevice inputDevice; + final TargetPlatform platform; + final bool isDesktop; + final PosterDefaults posterDefaults; + final ScrollController controller; + final double sideBarWidth; + + const AdaptiveLayoutModel({ + required this.viewSize, + required this.layoutMode, + required this.inputDevice, + required this.platform, + required this.isDesktop, + required this.posterDefaults, + required this.controller, + required this.sideBarWidth, + }); + + AdaptiveLayoutModel copyWith({ + ViewSize? viewSize, + LayoutMode? layoutMode, + InputDevice? inputDevice, + TargetPlatform? platform, + bool? isDesktop, + PosterDefaults? posterDefaults, + ScrollController? controller, + double? sideBarWidth, + }) { + return AdaptiveLayoutModel( + viewSize: viewSize ?? this.viewSize, + layoutMode: layoutMode ?? this.layoutMode, + inputDevice: inputDevice ?? this.inputDevice, + platform: platform ?? this.platform, + isDesktop: isDesktop ?? this.isDesktop, + posterDefaults: posterDefaults ?? this.posterDefaults, + controller: controller ?? this.controller, + sideBarWidth: sideBarWidth ?? this.sideBarWidth, + ); + } + + @override + bool operator ==(covariant AdaptiveLayoutModel other) { + if (identical(this, other)) return true; + return other.viewSize == viewSize && other.layoutMode == layoutMode && other.sideBarWidth == sideBarWidth; + } + + @override + int get hashCode => viewSize.hashCode ^ layoutMode.hashCode ^ sideBarWidth.hashCode; +} diff --git a/lib/util/application_info.freezed.dart b/lib/util/application_info.freezed.dart index 92dc1c4..fd153dd 100644 --- a/lib/util/application_info.freezed.dart +++ b/lib/util/application_info.freezed.dart @@ -20,112 +20,6 @@ mixin _$ApplicationInfo { String get version => throw _privateConstructorUsedError; String get buildNumber => throw _privateConstructorUsedError; String get os => throw _privateConstructorUsedError; - - /// Create a copy of ApplicationInfo - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $ApplicationInfoCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $ApplicationInfoCopyWith<$Res> { - factory $ApplicationInfoCopyWith( - ApplicationInfo value, $Res Function(ApplicationInfo) then) = - _$ApplicationInfoCopyWithImpl<$Res, ApplicationInfo>; - @useResult - $Res call({String name, String version, String buildNumber, String os}); -} - -/// @nodoc -class _$ApplicationInfoCopyWithImpl<$Res, $Val extends ApplicationInfo> - implements $ApplicationInfoCopyWith<$Res> { - _$ApplicationInfoCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of ApplicationInfo - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? name = null, - Object? version = null, - Object? buildNumber = null, - Object? os = null, - }) { - return _then(_value.copyWith( - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - version: null == version - ? _value.version - : version // ignore: cast_nullable_to_non_nullable - as String, - buildNumber: null == buildNumber - ? _value.buildNumber - : buildNumber // ignore: cast_nullable_to_non_nullable - as String, - os: null == os - ? _value.os - : os // ignore: cast_nullable_to_non_nullable - as String, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$ApplicationInfoImplCopyWith<$Res> - implements $ApplicationInfoCopyWith<$Res> { - factory _$$ApplicationInfoImplCopyWith(_$ApplicationInfoImpl value, - $Res Function(_$ApplicationInfoImpl) then) = - __$$ApplicationInfoImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({String name, String version, String buildNumber, String os}); -} - -/// @nodoc -class __$$ApplicationInfoImplCopyWithImpl<$Res> - extends _$ApplicationInfoCopyWithImpl<$Res, _$ApplicationInfoImpl> - implements _$$ApplicationInfoImplCopyWith<$Res> { - __$$ApplicationInfoImplCopyWithImpl( - _$ApplicationInfoImpl _value, $Res Function(_$ApplicationInfoImpl) _then) - : super(_value, _then); - - /// Create a copy of ApplicationInfo - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? name = null, - Object? version = null, - Object? buildNumber = null, - Object? os = null, - }) { - return _then(_$ApplicationInfoImpl( - name: null == name - ? _value.name - : name // ignore: cast_nullable_to_non_nullable - as String, - version: null == version - ? _value.version - : version // ignore: cast_nullable_to_non_nullable - as String, - buildNumber: null == buildNumber - ? _value.buildNumber - : buildNumber // ignore: cast_nullable_to_non_nullable - as String, - os: null == os - ? _value.os - : os // ignore: cast_nullable_to_non_nullable - as String, - )); - } } /// @nodoc @@ -146,30 +40,6 @@ class _$ApplicationInfoImpl extends _ApplicationInfo { final String buildNumber; @override final String os; - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ApplicationInfoImpl && - (identical(other.name, name) || other.name == name) && - (identical(other.version, version) || other.version == version) && - (identical(other.buildNumber, buildNumber) || - other.buildNumber == buildNumber) && - (identical(other.os, os) || other.os == os)); - } - - @override - int get hashCode => Object.hash(runtimeType, name, version, buildNumber, os); - - /// Create a copy of ApplicationInfo - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$ApplicationInfoImplCopyWith<_$ApplicationInfoImpl> get copyWith => - __$$ApplicationInfoImplCopyWithImpl<_$ApplicationInfoImpl>( - this, _$identity); } abstract class _ApplicationInfo extends ApplicationInfo { @@ -188,11 +58,4 @@ abstract class _ApplicationInfo extends ApplicationInfo { String get buildNumber; @override String get os; - - /// Create a copy of ApplicationInfo - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$ApplicationInfoImplCopyWith<_$ApplicationInfoImpl> get copyWith => - throw _privateConstructorUsedError; } diff --git a/lib/util/fladder_image.dart b/lib/util/fladder_image.dart index 28ae8a5..2d60063 100644 --- a/lib/util/fladder_image.dart +++ b/lib/util/fladder_image.dart @@ -13,6 +13,7 @@ class FladderImage extends ConsumerWidget { final Widget Function(BuildContext context, Object object, StackTrace? stack)? imageErrorBuilder; final Widget? placeHolder; final BoxFit fit; + final BoxFit? blurFit; final AlignmentGeometry? alignment; final bool disableBlur; final bool blurOnly; @@ -22,6 +23,7 @@ class FladderImage extends ConsumerWidget { this.imageErrorBuilder, this.placeHolder, this.fit = BoxFit.cover, + this.blurFit, this.alignment, this.disableBlur = false, this.blurOnly = false, @@ -39,14 +41,12 @@ class FladderImage extends ConsumerWidget { key: Key(newImage.key), fit: StackFit.expand, children: [ - if (!disableBlur && useBluredPlaceHolder && newImage.hash.isNotEmpty) - Image( - fit: fit, - excludeFromSemantics: true, - filterQuality: FilterQuality.low, - image: BlurHashImage( - newImage.hash, - ), + if (!disableBlur && useBluredPlaceHolder && newImage.hash.isNotEmpty || blurOnly) + BlurHash( + hash: newImage.hash, + optimizationMode: BlurHashOptimizationMode.approximation, + color: Colors.transparent, + imageFit: blurFit ?? fit, ), if (!blurOnly) FadeInImage( diff --git a/lib/util/item_base_model/item_base_model_extensions.dart b/lib/util/item_base_model/item_base_model_extensions.dart index c5cdff1..dcdca9c 100644 --- a/lib/util/item_base_model/item_base_model_extensions.dart +++ b/lib/util/item_base_model/item_base_model_extensions.dart @@ -1,8 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.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/models/book_model.dart'; import 'package:fladder/models/item_base_model.dart'; @@ -41,6 +41,37 @@ extension ItemBaseModelsBooleans on List { } return groupedItems; } + + FladderItemType get getMostCommonType { + final Map counts = {}; + + for (final item in this) { + final type = item.type; + counts[type] = (counts[type] ?? 0) + 1; + } + + return counts.entries.reduce((a, b) => a.value >= b.value ? a : b).key; + } + + double? getMostCommonAspectRatio({double tolerance = 0.01}) { + final Map> buckets = {}; + + for (final item in this) { + final aspectRatio = item.primaryRatio; + if (aspectRatio == null) continue; + + final bucketKey = (aspectRatio / tolerance).round(); + + buckets.putIfAbsent(bucketKey, () => []).add(aspectRatio); + } + + if (buckets.isEmpty) return null; + + final mostCommonBucket = buckets.entries.reduce((a, b) => a.value.length >= b.value.length ? a : b); + + final average = mostCommonBucket.value.reduce((a, b) => a + b) / mostCommonBucket.value.length; + return average; + } } enum ItemActions { diff --git a/lib/util/item_base_model/play_item_helpers.dart b/lib/util/item_base_model/play_item_helpers.dart index c152ee5..b00c264 100644 --- a/lib/util/item_base_model/play_item_helpers.dart +++ b/lib/util/item_base_model/play_item_helpers.dart @@ -1,32 +1,27 @@ - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/book_model.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/media_playback_model.dart'; import 'package:fladder/models/playback/playback_model.dart'; -import 'package:fladder/models/syncing/sync_item.dart'; -import 'package:fladder/models/video_stream_model.dart'; import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/book_viewer_provider.dart'; import 'package:fladder/providers/items/book_details_provider.dart'; -import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/book_viewer/book_viewer_screen.dart'; import 'package:fladder/screens/photo_viewer/photo_viewer_screen.dart'; -import 'package:fladder/screens/shared/adaptive_dialog.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/video_player/video_player.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/list_extensions.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; Future _showLoadingIndicator(BuildContext context) async { return showDialog( @@ -104,10 +99,7 @@ Future _playVideo( ), ); if (AdaptiveLayout.of(context).isDesktop) { - final fullScreen = await windowManager.isFullScreen(); - if (fullScreen) { - await windowManager.setFullScreen(false); - } + fullScreenHelper.closeFullScreen(ref); } if (context.mounted) { context.refreshData(); @@ -214,33 +206,12 @@ extension ItemBaseModelExtensions on ItemBaseModel? { _showLoadingIndicator(context); - SyncedItem? syncedItem = ref.read(syncProvider.notifier).getSyncedItem(this); - - final options = { - PlaybackType.directStream, - PlaybackType.transcode, - if (syncedItem != null && syncedItem.status == SyncStatus.complete) PlaybackType.offline, - }; - - PlaybackModel? model; - - if (showPlaybackOption) { - final playbackType = await _showPlaybackTypeSelection( - context: context, - options: options, - ); - - model = switch (playbackType) { - PlaybackType.directStream || PlaybackType.transcode => await ref - .read(playbackModelHelper) - .createServerPlaybackModel(itemModel, playbackType, startPosition: startPosition), - PlaybackType.offline => await ref.read(playbackModelHelper).createOfflinePlaybackModel(itemModel, syncedItem), - null => null - }; - } else { - model = (await ref.read(playbackModelHelper).createServerPlaybackModel(itemModel, PlaybackType.directStream)) ?? - await ref.read(playbackModelHelper).createOfflinePlaybackModel(itemModel, syncedItem); - } + PlaybackModel? model = await ref.read(playbackModelHelper).createPlaybackModel( + context, + itemModel, + showPlaybackOptions: showPlaybackOption, + startPosition: startPosition, + ); await _playVideo(context, startPosition: startPosition, current: model, ref: ref); } @@ -271,69 +242,17 @@ extension ItemBaseModelsBooleans on List { expandedList.shuffle(); } - PlaybackModel? model = await ref.read(playbackModelHelper).createServerPlaybackModel( + PlaybackModel? model = await ref.read(playbackModelHelper).createPlaybackModel( + context, expandedList.firstOrNull, - PlaybackType.directStream, libraryQueue: expandedList, ); if (context.mounted) { await _playVideo(context, ref: ref, queue: expandedList, current: model); if (context.mounted) { - RefreshState.of(context).refresh(); + RefreshState.maybeOf(context)?.refresh(); } } } } - -Future _showPlaybackTypeSelection({ - required BuildContext context, - required Set 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 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( - "Playback type", - style: Theme.of(context).textTheme.titleLarge, - ), - ), - const Divider(), - ...options.map((type) => ListTile( - title: Text(type.name), - leading: Icon(type.icon), - onTap: () { - onClose(type); - }, - )) - ], - ); - } -} diff --git a/lib/util/keyed_list_view.dart b/lib/util/keyed_list_view.dart deleted file mode 100644 index bf38921..0000000 --- a/lib/util/keyed_list_view.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:collection/collection.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class KeyedListView extends ConsumerStatefulWidget { - final Map map; - final Widget Function(BuildContext context, int index) itemBuilder; - const KeyedListView({required this.map, required this.itemBuilder, super.key}); - - @override - ConsumerState createState() => _KeyedListViewState(); -} - -class _KeyedListViewState extends ConsumerState { - final ItemScrollController itemScrollController = ItemScrollController(); - final ScrollOffsetController scrollOffsetController = ScrollOffsetController(); - final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create(); - final ScrollOffsetListener scrollOffsetListener = ScrollOffsetListener.create(); - int currentIndex = 0; - - @override - void initState() { - super.initState(); - itemPositionsListener.itemPositions.addListener(() { - if (currentIndex != itemPositionsListener.itemPositions.value.toList()[0].index) { - setState(() { - currentIndex = itemPositionsListener.itemPositions.value.toList()[0].index; - }); - } - }); - } - - @override - void dispose() { - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Flexible( - child: ScrollablePositionedList.builder( - itemCount: widget.map.length, - itemBuilder: widget.itemBuilder, - itemScrollController: itemScrollController, - scrollOffsetController: scrollOffsetController, - itemPositionsListener: itemPositionsListener, - scrollOffsetListener: scrollOffsetListener, - ), - ), - const SizedBox(width: 8), - SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: widget.map.keys.mapIndexed( - (index, e) { - final atPosition = currentIndex == index; - return Container( - decoration: BoxDecoration( - color: atPosition ? Theme.of(context).colorScheme.secondary : Colors.transparent, - borderRadius: BorderRadius.circular(5), - ), - height: 28, - width: 28, - child: TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - textStyle: Theme.of(context).textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold), - iconColor: atPosition - ? Theme.of(context).colorScheme.onSecondary - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.35), - foregroundColor: atPosition - ? Theme.of(context).colorScheme.onSecondary - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.35), - ), - onPressed: () { - itemScrollController.scrollTo( - index: index, - duration: const Duration(seconds: 1), - opacityAnimationWeights: [20, 20, 60], - curve: Curves.easeOutCubic, - ); - }, - child: Text( - e, - ), - ), - ); - }, - ).toList(), - ), - ), - ], - ); - } -} diff --git a/lib/util/localization_helper.dart b/lib/util/localization_helper.dart index eda6922..2d2dc6e 100644 --- a/lib/util/localization_helper.dart +++ b/lib/util/localization_helper.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/l10n/generated/app_localizations.dart'; + ///Only use for base translations, under normal circumstances ALWAYS use the widgets provided context final localizationContextProvider = StateProvider((ref) => null); @@ -30,3 +31,11 @@ class _LocalizationContextWrapperState extends ConsumerState widget.child; } + +extension LocaleDisplayCodeExtension on Locale { + String toDisplayCode() { + return countryCode != null && countryCode!.isNotEmpty + ? "${languageCode.toUpperCase()}-${countryCode!.toUpperCase()}" + : languageCode.toUpperCase(); + } +} diff --git a/lib/util/resolution_checker.dart b/lib/util/resolution_checker.dart new file mode 100644 index 0000000..a13c110 --- /dev/null +++ b/lib/util/resolution_checker.dart @@ -0,0 +1,63 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:screen_retriever/screen_retriever.dart'; +import 'package:window_manager/window_manager.dart'; + +import 'package:fladder/providers/arguments_provider.dart'; + +class ResolutionChecker extends ConsumerStatefulWidget { + final Widget child; + const ResolutionChecker({required this.child, super.key}); + + @override + ConsumerState createState() => _ResolutionCheckerState(); +} + +class _ResolutionCheckerState extends ConsumerState { + Size? lastResolution; + Timer? _timer; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((value) async { + if (ref.read(argumentsStateProvider).htpcMode) { + lastResolution = (await screenRetriever.getPrimaryDisplay()).size; + _timer = Timer.periodic(const Duration(seconds: 2), (timer) => checkResolution()); + } + }); + } + + Future checkResolution() async { + if (!mounted) return; + final newResolution = (await screenRetriever.getPrimaryDisplay()).size; + if (lastResolution != newResolution) { + lastResolution = newResolution; + shouldSetResolution(); + } + } + + Future shouldSetResolution() async { + if (lastResolution != null) { + final isFullScreen = await windowManager.isFullScreen(); + if (isFullScreen) { + await windowManager.setFullScreen(false); + } + await windowManager.setFullScreen(true); + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/util/sliver_list_padding.dart b/lib/util/sliver_list_padding.dart index e1b30cf..0a11a7b 100644 --- a/lib/util/sliver_list_padding.dart +++ b/lib/util/sliver_list_padding.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; class DefautlSliverBottomPadding extends StatelessWidget { const DefautlSliverBottomPadding({super.key}); @@ -9,8 +8,8 @@ class DefautlSliverBottomPadding extends StatelessWidget { @override Widget build(BuildContext context) { return (AdaptiveLayout.viewSizeOf(context) != ViewSize.phone) - ? SliverPadding(padding: EdgeInsets.only(bottom: 35 + MediaQuery.of(context).padding.bottom)) - : SliverPadding(padding: EdgeInsets.only(bottom: 85 + MediaQuery.of(context).padding.bottom)); + ? SliverPadding(padding: EdgeInsets.only(bottom: 60 + MediaQuery.of(context).padding.bottom)) + : SliverPadding(padding: EdgeInsets.only(bottom: 100 + MediaQuery.of(context).padding.bottom)); } } diff --git a/lib/util/sticky_header_text.dart b/lib/util/sticky_header_text.dart index c934057..ae5c07d 100644 --- a/lib/util/sticky_header_text.dart +++ b/lib/util/sticky_header_text.dart @@ -1,7 +1,9 @@ -import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/screens/shared/flat_button.dart'; import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; + +import 'package:fladder/screens/shared/flat_button.dart'; class StickyHeaderText extends ConsumerStatefulWidget { final String label; @@ -21,16 +23,21 @@ class StickyHeaderTextState extends ConsumerState { return FlatButton( onTap: widget.onClick, child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), + padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, + spacing: 6, children: [ - Text( - widget.label, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, - ), + Flexible( + child: Text( + widget.label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), ), if (widget.onClick != null) Padding( diff --git a/lib/util/streams_selection.dart b/lib/util/streams_selection.dart new file mode 100644 index 0000000..c66ed26 --- /dev/null +++ b/lib/util/streams_selection.dart @@ -0,0 +1,67 @@ +import 'package:fladder/models/items/media_streams_model.dart'; + +int? selectAudioStream( + bool rememberAudioSelection, + AudioAndSubStreamModel? previousStream, + List? currentStream, + int? defaultStream, +) { + if (!rememberAudioSelection) { + return defaultStream; + } + return _selectStream(previousStream, currentStream, defaultStream); +} + +int? selectSubStream( + bool rememberSubSelection, + AudioAndSubStreamModel? previousStream, + List? currentStream, + int? defaultStream, +) { + if (!rememberSubSelection) { + return defaultStream; + } + return _selectStream(previousStream, currentStream, defaultStream); +} + +int? _selectStream( + AudioAndSubStreamModel? previousStream, + List? currentStream, + int? defaultStream, +) { + if (currentStream == null || previousStream == null) { + return defaultStream; + } + + int? bestStreamIndex; + int bestStreamScore = 0; + + // Find the relative index of the previous stream + int prevRelIndex = 0; + for (var stream in currentStream) { + if (stream.index == previousStream.index) break; + prevRelIndex += 1; + } + + int newRelIndex = 0; + for (var stream in currentStream) { + int score = 0; + + if (previousStream.codec == stream.codec) score += 1; + if (prevRelIndex == newRelIndex) score += 1; + if (previousStream.displayTitle == stream.displayTitle) { + score += 2; + } + if (previousStream.language != 'und' && previousStream.language == stream.language) { + score += 2; + } + + if (score > bestStreamScore && score >= 3) { + bestStreamScore = score; + bestStreamIndex = stream.index; + } + + newRelIndex += 1; + } + return bestStreamIndex ?? defaultStream; +} diff --git a/lib/util/update_checker.dart b/lib/util/update_checker.dart new file mode 100644 index 0000000..ebcb3e9 --- /dev/null +++ b/lib/util/update_checker.dart @@ -0,0 +1,150 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; + +import 'package:http/http.dart' as http; +import 'package:package_info_plus/package_info_plus.dart'; + +class ReleaseInfo { + final String version; + final String changelog; + final String url; + final bool isNewerThanCurrent; + final Map downloads; + + ReleaseInfo({ + required this.version, + required this.changelog, + required this.url, + required this.isNewerThanCurrent, + required this.downloads, + }); + + String? downloadUrlFor(String platform) => downloads[platform]; + + Map get preferredDownloads { + final group = _platformGroup(); + final entries = downloads.entries.where((e) => e.key.contains(group)); + return Map.fromEntries(entries); + } + + Map get otherDownloads { + final group = _platformGroup(); + final entries = downloads.entries.where((e) => !e.key.contains(group)); + return Map.fromEntries(entries); + } + + String _platformGroup() { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return 'android'; + case TargetPlatform.iOS: + return 'ios'; + case TargetPlatform.windows: + return 'windows'; + case TargetPlatform.macOS: + return 'macos'; + case TargetPlatform.linux: + return 'linux'; + default: + return ''; + } + } +} + +extension DownloadLabelFormatter on String { + String prettifyKey() { + final parts = split('_'); + if (parts.isEmpty) return this; + + final base = parts.first.capitalize(); + if (parts.length == 1) return base; + + final variant = parts.sublist(1).join(' ').capitalize(); + return '$base ($variant)'; + } + + String capitalize() => isEmpty ? this : '${this[0].toUpperCase()}${substring(1)}'; +} + +class UpdateChecker { + final String owner = 'DonutWare'; + final String repo = 'Fladder'; + + Future> fetchRecentReleases({int count = 5}) async { + final info = await PackageInfo.fromPlatform(); + final currentVersion = info.version; + + final url = Uri.parse('https://api.github.com/repos/$owner/$repo/releases?per_page=$count'); + final response = await http.get(url); + + if (response.statusCode != 200) { + print('Failed to fetch releases: ${response.statusCode}'); + return []; + } + + final List releases = jsonDecode(response.body); + return releases.map((json) { + final tag = (json['tag_name'] as String?)?.replaceFirst(RegExp(r'^v'), ''); + final changelog = json['body'] as String? ?? ''; + final htmlUrl = json['html_url'] as String? ?? ''; + final assets = json['assets'] as List? ?? []; + + final Map downloads = {}; + for (final asset in assets) { + final name = asset['name'] as String? ?? ''; + final downloadUrl = asset['browser_download_url'] as String? ?? ''; + + if (name.contains('Android') && name.endsWith('.apk')) { + downloads['android'] = downloadUrl; + } else if (name.contains('iOS') && name.endsWith('.ipa')) { + downloads['ios'] = downloadUrl; + } else if (name.contains('Windows') && name.endsWith('Setup.exe')) { + downloads['windows_installer'] = downloadUrl; + } else if (name.contains('Windows') && name.endsWith('.zip')) { + downloads['windows_portable'] = downloadUrl; + } else if (name.contains('macOS') && name.endsWith('.dmg')) { + downloads['macos'] = downloadUrl; + } else if (name.contains('Linux') && name.endsWith('.AppImage')) { + downloads['linux_appimage'] = downloadUrl; + } else if (name.contains('Linux') && name.endsWith('.flatpak')) { + downloads['linux_flatpak'] = downloadUrl; + } else if (name.contains('Linux') && name.endsWith('.zip')) { + downloads['linux_zip'] = downloadUrl; + } else if (name.contains('Linux') && name.endsWith('.zsync')) { + downloads['linux_zsync'] = downloadUrl; + } else if (name.contains('Web') && name.endsWith('.zip')) { + downloads['web'] = downloadUrl; + } + } + + bool isNewer = tag != null && _compareVersions(tag, currentVersion) > 0; + + return ReleaseInfo( + version: tag ?? 'unknown', + changelog: changelog.trim(), + url: htmlUrl, + isNewerThanCurrent: isNewer, + downloads: downloads, + ); + }).toList(); + } + + Future isUpToDate() async { + final releases = await fetchRecentReleases(count: 1); + if (releases.isEmpty) return true; + return !releases.first.isNewerThanCurrent; + } + + static int _compareVersions(String a, String b) { + final aParts = a.split('.').map(int.tryParse).toList(); + final bParts = b.split('.').map(int.tryParse).toList(); + + for (var i = 0; i < aParts.length || i < bParts.length; i++) { + final aVal = i < aParts.length ? (aParts[i] ?? 0) : 0; + final bVal = i < bParts.length ? (bParts[i] ?? 0) : 0; + if (aVal != bVal) return aVal.compareTo(bVal); + } + return 0; + } +} diff --git a/lib/util/video_properties.dart b/lib/util/video_properties.dart index fdd1651..0af7dd2 100644 --- a/lib/util/video_properties.dart +++ b/lib/util/video_properties.dart @@ -1,10 +1,12 @@ +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart' as dto; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:flutter/material.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; enum Resolution { sd("SD"), @@ -47,8 +49,10 @@ enum DisplayProfile { sdr("SDR"), hdr("HDR"), hdr10("HDR10"), + hdr10Plus("HDR10+"), dolbyVision("Dolby Vision"), dolbyVisionHdr10("DoVi/HDR10"), + dolbyVisionHlg("DoVi/Hlg"), hlg("HLG"); const DisplayProfile(this.value); @@ -80,15 +84,16 @@ enum DisplayProfile { } static DisplayProfile fromVideoStream(VideoStreamModel stream) { - switch (stream.videoRangeType) { - case null: - case dto.VideoRangeType.hlg: - return DisplayProfile.hlg; - case dto.VideoRangeType.hdr10: - return DisplayProfile.hdr10; - default: - return DisplayProfile.sdr; - } + return switch (stream.videoRangeType) { + dto.VideoRangeType.doviwithsdr => DisplayProfile.dolbyVisionHlg, + dto.VideoRangeType.doviwithhdr10 => DisplayProfile.dolbyVisionHdr10, + dto.VideoRangeType.dovi => DisplayProfile.dolbyVision, + dto.VideoRangeType.hlg => DisplayProfile.hlg, + dto.VideoRangeType.hdr10 => DisplayProfile.hdr10, + dto.VideoRangeType.doviwithhlg => DisplayProfile.dolbyVisionHlg, + dto.VideoRangeType.hdr10plus => DisplayProfile.hdr10Plus, + _ => DisplayProfile.sdr + }; } } diff --git a/lib/widgets/full_screen_helpers/full_screen_helper_desktop.dart b/lib/widgets/full_screen_helpers/full_screen_helper_desktop.dart new file mode 100644 index 0000000..8095df4 --- /dev/null +++ b/lib/widgets/full_screen_helpers/full_screen_helper_desktop.dart @@ -0,0 +1,28 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:window_manager/window_manager.dart'; + +import 'package:fladder/providers/arguments_provider.dart'; +import 'package:fladder/providers/video_player_provider.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; + +class FullScreenHelper implements FullScreenWrapper { + const FullScreenHelper._(); + factory FullScreenHelper.instantiate() => const FullScreenHelper._(); + @override + Future closeFullScreen(WidgetRef ref) async { + if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return; + final isFullScreen = await windowManager.isFullScreen(); + if (isFullScreen) { + await windowManager.setFullScreen(false); + } + ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: false)); + } + + @override + Future toggleFullScreen(WidgetRef ref) async { + if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return; + final isFullScreen = await windowManager.isFullScreen(); + await windowManager.setFullScreen(!isFullScreen); + ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: !isFullScreen)); + } +} diff --git a/lib/widgets/full_screen_helpers/full_screen_helper_web.dart b/lib/widgets/full_screen_helpers/full_screen_helper_web.dart new file mode 100644 index 0000000..69d546d --- /dev/null +++ b/lib/widgets/full_screen_helpers/full_screen_helper_web.dart @@ -0,0 +1,34 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:universal_html/html.dart' as html; + +import 'package:fladder/providers/video_player_provider.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_wrapper.dart'; + +class FullScreenHelper implements FullScreenWrapper { + const FullScreenHelper._(); + factory FullScreenHelper.instantiate() => const FullScreenHelper._(); + @override + Future closeFullScreen(WidgetRef ref) async { + if (html.document.fullscreenElement != null) { + html.document.exitFullscreen(); + await Future.delayed(const Duration(milliseconds: 500)); + ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: false)); + } + } + + @override + Future toggleFullScreen(WidgetRef ref) async { + final isFullScreen = html.document.fullscreenElement != null; + + if (isFullScreen) { + html.document.exitFullscreen(); + //Wait for 1 second + await Future.delayed(const Duration(seconds: 1)); + } else { + await html.document.documentElement?.requestFullscreen(); + } + ref + .read(mediaPlaybackProvider.notifier) + .update((state) => state.copyWith(fullScreen: html.document.fullscreenElement != null)); + } +} diff --git a/lib/widgets/full_screen_helpers/full_screen_wrapper.dart b/lib/widgets/full_screen_helpers/full_screen_wrapper.dart new file mode 100644 index 0000000..2328248 --- /dev/null +++ b/lib/widgets/full_screen_helpers/full_screen_wrapper.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/providers/arguments_provider.dart'; +import 'package:fladder/providers/video_player_provider.dart'; +import 'package:fladder/widgets/full_screen_helpers/full_screen_helper_desktop.dart' + if (dart.library.html) 'package:fladder/widgets/full_screen_helpers/full_screen_helper_web.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; + +final FullScreenHelper fullScreenHelper = FullScreenHelper.instantiate(); + +abstract class FullScreenWrapper { + Future closeFullScreen(WidgetRef ref); + Future toggleFullScreen(WidgetRef ref); +} + +class FullScreenButton extends ConsumerWidget { + const FullScreenButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (ref.watch(argumentsStateProvider.select((value) => value.htpcMode))) return const SizedBox.shrink(); + final fullScreen = ref.watch(mediaPlaybackProvider.select((value) => value.fullScreen)); + return IconButton( + onPressed: () => fullScreenHelper.toggleFullScreen(ref), + icon: Icon( + fullScreen ? IconsaxPlusLinear.screenmirroring : IconsaxPlusLinear.maximize_4, + ), + ); + } +} diff --git a/lib/widgets/navigation_scaffold/components/adaptive_fab.dart b/lib/widgets/navigation_scaffold/components/adaptive_fab.dart index a0ae48c..bd3d744 100644 --- a/lib/widgets/navigation_scaffold/components/adaptive_fab.dart +++ b/lib/widgets/navigation_scaffold/components/adaptive_fab.dart @@ -29,19 +29,15 @@ class AdaptiveFab { duration: const Duration(milliseconds: 250), height: 60, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: ElevatedButton( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: FilledButton.tonal( onPressed: onPressed, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Row( - children: [ - child, - const Spacer(), - Flexible(child: Text(title)), - const Spacer(), - ], - ), + child: Row( + spacing: 24, + children: [ + child, + Flexible(child: Text(title)), + ], ), ), ), diff --git a/lib/widgets/navigation_scaffold/components/background_image.dart b/lib/widgets/navigation_scaffold/components/background_image.dart new file mode 100644 index 0000000..e07fbf7 --- /dev/null +++ b/lib/widgets/navigation_scaffold/components/background_image.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; + +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/items/images_models.dart'; +import 'package:fladder/providers/api_provider.dart'; +import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/util/fladder_image.dart'; + +class BackgroundImage extends ConsumerStatefulWidget { + final List items; + final List images; + const BackgroundImage({this.items = const [], this.images = const [], super.key}); + + @override + ConsumerState createState() => _BackgroundImageState(); +} + +class _BackgroundImageState extends ConsumerState { + ImageData? backgroundImage; + + @override + void didUpdateWidget(covariant BackgroundImage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.items.length != widget.items.length || oldWidget.images.length != widget.images.length) { + updateItems(); + } + } + + @override + void initState() { + super.initState(); + updateItems(); + } + + void updateItems() { + final enabled = ref.read(clientSettingsProvider.select((value) => value.backgroundPosters)); + + WidgetsBinding.instance.addPostFrameCallback((value) async { + if (!enabled && mounted) return; + + if (widget.images.isNotEmpty) { + final image = widget.images.shuffled().firstOrNull?.primary; + if (mounted) setState(() => backgroundImage = image); + return; + } + + if (widget.items.isEmpty) return; + + final randomItem = widget.items.shuffled().firstOrNull; + final itemId = switch (randomItem?.type) { + FladderItemType.folder => randomItem?.id, + FladderItemType.series => randomItem?.parentId ?? randomItem?.id, + _ => randomItem?.id, + }; + + if (itemId == null) return; + + final apiResponse = await ref.read(jellyApiProvider).usersUserIdItemsItemIdGet(itemId: itemId); + final image = + apiResponse.body?.parentBaseModel.getPosters?.randomBackDrop ?? apiResponse.body?.getPosters?.randomBackDrop; + + if (mounted) setState(() => backgroundImage = image); + }); + } + + @override + Widget build(BuildContext context) { + final enabled = ref.watch(clientSettingsProvider.select((value) => value.backgroundPosters)); + return enabled + ? FladderImage( + image: backgroundImage, + fit: BoxFit.cover, + blurOnly: false, + ) + : const SizedBox.shrink(); + } +} diff --git a/lib/widgets/navigation_scaffold/components/destination_model.dart b/lib/widgets/navigation_scaffold/components/destination_model.dart index 2761745..7be11e9 100644 --- a/lib/widgets/navigation_scaffold/components/destination_model.dart +++ b/lib/widgets/navigation_scaffold/components/destination_model.dart @@ -1,7 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:auto_route/auto_route.dart'; + import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart'; -import 'package:flutter/material.dart'; class DestinationModel { final String label; @@ -79,12 +81,13 @@ class DestinationModel { ); } - NavigationButton toNavigationButton(bool selected, bool expanded) { + NavigationButton toNavigationButton(bool selected, bool horizontal, bool expanded) { return NavigationButton( label: label, selected: selected, onPressed: action, - horizontal: expanded, + horizontal: horizontal, + expanded: expanded, selectedIcon: selectedIcon!, icon: icon!, ); diff --git a/lib/widgets/navigation_scaffold/components/drawer_list_button.dart b/lib/widgets/navigation_scaffold/components/drawer_list_button.dart index 55ee378..5b5477f 100644 --- a/lib/widgets/navigation_scaffold/components/drawer_list_button.dart +++ b/lib/widgets/navigation_scaffold/components/drawer_list_button.dart @@ -1,5 +1,5 @@ import 'package:fladder/screens/shared/animated_fade_size.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/widgets/shared/item_actions.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/navigation_scaffold/components/fladder_app_bar.dart b/lib/widgets/navigation_scaffold/components/fladder_app_bar.dart index 563e21a..2783b2d 100644 --- a/lib/widgets/navigation_scaffold/components/fladder_app_bar.dart +++ b/lib/widgets/navigation_scaffold/components/fladder_app_bar.dart @@ -5,7 +5,7 @@ import 'package:flutter/services.dart'; import 'package:auto_route/auto_route.dart'; import 'package:fladder/screens/shared/default_title_bar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; bool get _isDesktop { if (kIsWeb) return false; diff --git a/lib/widgets/navigation_scaffold/components/floating_player_bar.dart b/lib/widgets/navigation_scaffold/components/floating_player_bar.dart index 3c73930..b9078b7 100644 --- a/lib/widgets/navigation_scaffold/components/floating_player_bar.dart +++ b/lib/widgets/navigation_scaffold/components/floating_player_bar.dart @@ -1,8 +1,8 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/media_playback_model.dart'; @@ -11,14 +11,15 @@ import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/video_player/video_player.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/duration_extensions.dart'; -import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/refresh_state.dart'; const videoPlayerHeroTag = "HeroPlayer"; +const floatingPlayerHeight = 70.0; + class FloatingPlayerBar extends ConsumerStatefulWidget { const FloatingPlayerBar({super.key}); @@ -71,29 +72,29 @@ class _CurrentlyPlayingBarState extends ConsumerState { }, direction: DismissDirection.vertical, child: InkWell( - onLongPress: () { - fladderSnackbar(context, title: "Swipe up/down to open/close the player"); - }, + onLongPress: () => fladderSnackbar(context, title: "Swipe up/down to open/close the player"), child: Card( elevation: 5, color: Theme.of(context).colorScheme.primaryContainer, - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 50, maxHeight: 85), + child: SizedBox( + height: floatingPlayerHeight, child: LayoutBuilder(builder: (context, constraints) { return Row( children: [ Flexible( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(8), - child: Row( - children: [ - if (playbackInfo.state == VideoPlayerState.minimized) - Card( - child: SizedBox( + child: Padding( + padding: MediaQuery.paddingOf(context).copyWith(top: 0, bottom: 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(6), + child: Row( + spacing: 7, + children: [ + if (playbackInfo.state == VideoPlayerState.minimized) + Card( child: AspectRatio( aspectRatio: 1.67, child: MouseRegion( @@ -131,72 +132,76 @@ class _CurrentlyPlayingBarState extends ConsumerState { ), ), ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - playbackModel?.title ?? "", - style: Theme.of(context).textTheme.titleLarge, - ), - ), - if (playbackModel?.detailedName(context)?.isNotEmpty == true) + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ Flexible( child: Text( - playbackModel?.detailedName(context) ?? "", + playbackModel?.title ?? "", style: Theme.of(context).textTheme.titleMedium, ), ), - ], - ), - ), - if (!progress.isNaN && constraints.maxWidth > 500) - Text( - "${playbackInfo.position.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: IconButton.filledTonal( - onPressed: () => ref.read(videoPlayerProvider).playOrPause(), - icon: playbackInfo.playing - ? const Icon(Icons.pause_rounded) - : const Icon(Icons.play_arrow_rounded), - ), - ), - if (constraints.maxWidth > 500) ...{ - IconButton( - onPressed: () { - final volume = player.lastState?.volume == 0 ? 100.0 : 0.0; - player.setVolume(volume); - }, - icon: Icon( - ref.watch(videoPlayerSettingsProvider.select((value) => value.volume)) <= 0 - ? IconsaxPlusBold.volume_cross - : IconsaxPlusBold.volume_high, + if (playbackModel?.detailedName(context)?.isNotEmpty == true) + Flexible( + child: Text( + playbackModel?.detailedName(context) ?? "", + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: + Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.65), + ), + ), + ), + ], ), ), - }, - Tooltip( - message: context.localized.stop, - waitDuration: const Duration(milliseconds: 500), - child: IconButton( - onPressed: () async => stopPlayer(), - icon: const Icon(IconsaxPlusBold.stop), + if (!progress.isNaN && constraints.maxWidth > 500) + Text( + "${playbackInfo.position.readAbleDuration} / ${playbackInfo.duration.readAbleDuration}"), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: IconButton.filledTonal( + onPressed: () => ref.read(videoPlayerProvider).playOrPause(), + icon: playbackInfo.playing + ? const Icon(Icons.pause_rounded) + : const Icon(Icons.play_arrow_rounded), + ), ), - ), - ].addInBetween(const SizedBox(width: 6)), + if (constraints.maxWidth > 500) ...[ + IconButton( + onPressed: () { + final volume = player.lastState?.volume == 0 ? 100.0 : 0.0; + player.setVolume(volume); + }, + icon: Icon( + ref.watch(videoPlayerSettingsProvider.select((value) => value.volume)) <= 0 + ? IconsaxPlusBold.volume_cross + : IconsaxPlusBold.volume_high, + ), + ), + Tooltip( + message: context.localized.stop, + waitDuration: const Duration(milliseconds: 500), + child: IconButton( + onPressed: () async => stopPlayer(), + icon: const Icon(IconsaxPlusBold.stop), + ), + ), + ], + ], + ), ), ), - ), - LinearProgressIndicator( - minHeight: 6, - backgroundColor: Colors.black.withValues(alpha: 0.25), - color: Theme.of(context).colorScheme.primary, - value: progress.clamp(0, 1), - ), - ], + LinearProgressIndicator( + minHeight: 6, + backgroundColor: Colors.black.withValues(alpha: 0.25), + color: Theme.of(context).colorScheme.primary, + value: progress.clamp(0, 1), + ), + ], + ), ), ), ], diff --git a/lib/widgets/navigation_scaffold/components/navigation_body.dart b/lib/widgets/navigation_scaffold/components/navigation_body.dart index 0e40256..bdc6f78 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_body.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_body.dart @@ -2,21 +2,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/routes/auto_router.dart'; -import 'package:fladder/routes/auto_router.gr.dart'; -import 'package:fladder/screens/shared/animated_fade_size.dart'; -import 'package:fladder/util/adaptive_layout.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/navigation_drawer.dart'; -import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/side_navigation_bar.dart'; class NavigationBody extends ConsumerStatefulWidget { final BuildContext parentContext; @@ -40,7 +33,7 @@ class NavigationBody extends ConsumerStatefulWidget { } class _NavigationBodyState extends ConsumerState { - bool expandedSideBar = true; + double currentSideBarWidth = 80; @override void initState() { @@ -52,9 +45,9 @@ class _NavigationBodyState extends ConsumerState { @override Widget build(BuildContext context) { - final views = ref.watch(viewsProvider.select((value) => value.views)); final hasOverlay = AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual || homeRoutes.any((element) => element.name.contains(context.router.current.name)); + ref.listen( clientSettingsProvider, (previous, next) { @@ -66,56 +59,28 @@ class _NavigationBodyState extends ConsumerState { }, ); - return switch (AdaptiveLayout.layoutOf(context)) { - ViewSize.phone => MediaQuery.removePadding( - context: widget.parentContext, + Widget paddedChild() => MediaQuery( + data: semiNestedPadding(widget.parentContext, hasOverlay), child: widget.child, - ), - ViewSize.tablet => Row( - children: [ - AnimatedFadeSize( - duration: const Duration(milliseconds: 250), - child: hasOverlay ? navigationRail(context) : const SizedBox(), - ), - Flexible( - child: MediaQuery( - data: semiNestedPadding(context, hasOverlay), - child: widget.child, - ), + ); + + return switch (AdaptiveLayout.layoutOf(context)) { + ViewSize.phone => paddedChild(), + ViewSize.tablet => hasOverlay + ? SideNavigationBar( + currentIndex: widget.currentIndex, + destinations: widget.destinations, + currentLocation: widget.currentLocation, + child: paddedChild(), + scaffoldKey: widget.drawerKey, ) - ], - ), - ViewSize.desktop => Row( - children: [ - AnimatedFadeSize( - duration: const Duration(milliseconds: 125), - child: hasOverlay - ? expandedSideBar - ? MediaQuery.removePadding( - context: widget.parentContext, - child: NestedNavigationDrawer( - isExpanded: expandedSideBar, - actionButton: actionButton(), - toggleExpanded: (value) { - setState(() { - expandedSideBar = value; - }); - }, - views: views, - destinations: widget.destinations, - currentLocation: widget.currentLocation, - ), - ) - : navigationRail(context) - : const SizedBox(), - ), - Flexible( - child: MediaQuery( - data: semiNestedPadding(context, hasOverlay), - child: widget.child, - ), - ), - ], + : paddedChild(), + ViewSize.desktop => SideNavigationBar( + currentIndex: widget.currentIndex, + destinations: widget.destinations, + currentLocation: widget.currentLocation, + child: paddedChild(), + scaffoldKey: widget.drawerKey, ) }; } @@ -126,89 +91,4 @@ class _NavigationBodyState extends ConsumerState { padding: paddingOf.copyWith(left: hasOverlay ? 0 : paddingOf.left), ); } - - AdaptiveFab? actionButton() { - return (widget.currentIndex >= 0 && widget.currentIndex < widget.destinations.length) - ? widget.destinations[widget.currentIndex].floatingActionButton - : null; - } - - Widget navigationRail(BuildContext context) { - return Column( - children: [ - if (AdaptiveLayout.of(context).isDesktop && AdaptiveLayout.of(context).platform != TargetPlatform.macOS) ...{ - const SizedBox(height: 4), - Text( - "Fladder", - style: Theme.of(context).textTheme.titleSmall, - ), - }, - if (AdaptiveLayout.of(context).platform == TargetPlatform.macOS) - SizedBox(height: MediaQuery.of(context).padding.top), - Flexible( - child: Padding( - key: const Key('navigation_rail'), - padding: - MediaQuery.paddingOf(context).copyWith(right: 0, top: AdaptiveLayout.of(context).isDesktop ? 8 : null), - child: Column( - children: [ - IconButton( - onPressed: () { - if (AdaptiveLayout.layoutOf(context) != ViewSize.desktop) { - widget.drawerKey.currentState?.openDrawer(); - } else { - setState(() { - expandedSideBar = true; - }); - } - }, - icon: const Icon(IconsaxPlusBold.menu), - ), - if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.dual) ...[ - const SizedBox(height: 8), - AnimatedFadeSize( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - transitionBuilder: (Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - }, - child: actionButton()?.normal, - ), - ), - ], - const Spacer(), - IconTheme( - data: const IconThemeData(size: 28), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...widget.destinations.mapIndexed( - (index, destination) => destination.toNavigationButton(widget.currentIndex == index, false), - ) - ], - ), - ), - const Spacer(), - SizedBox( - height: 48, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - child: widget.currentLocation.contains(const SettingsRoute().routeName) - ? Card( - color: Theme.of(context).colorScheme.primaryContainer, - child: const Padding( - padding: EdgeInsets.all(10), - child: Icon(IconsaxPlusBold.setting_3), - ), - ) - : const SettingsUserIcon()), - ), - if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16), - ], - ), - ), - ), - ], - ); - } } diff --git a/lib/widgets/navigation_scaffold/components/navigation_button.dart b/lib/widgets/navigation_scaffold/components/navigation_button.dart index 305b0f7..004fec5 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_button.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_button.dart @@ -2,14 +2,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/util/widget_extensions.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/widgets/shared/item_actions.dart'; class NavigationButton extends ConsumerStatefulWidget { final String? label; final Widget selectedIcon; final Widget icon; final bool horizontal; + final bool expanded; final Function()? onPressed; + final Function()? onLongPress; + final List trailing; final bool selected; final Duration duration; const NavigationButton({ @@ -17,8 +21,11 @@ class NavigationButton extends ConsumerStatefulWidget { required this.selectedIcon, required this.icon, this.horizontal = false, + this.expanded = false, this.onPressed, + this.onLongPress, this.selected = false, + this.trailing = const [], this.duration = const Duration(milliseconds: 125), super.key, }); @@ -28,106 +35,119 @@ class NavigationButton extends ConsumerStatefulWidget { } class _NavigationButtonState extends ConsumerState { + bool showPopupButton = false; @override Widget build(BuildContext context) { - return Tooltip( - waitDuration: const Duration(seconds: 1), - preferBelow: false, - triggerMode: TooltipTriggerMode.longPress, - message: widget.label ?? "", - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), + final foreGroundColor = widget.selected + ? widget.expanded + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: ElevatedButton( + onHover: (value) => setState(() => showPopupButton = value), + style: ButtonStyle( + elevation: const WidgetStatePropertyAll(0), + padding: const WidgetStatePropertyAll(EdgeInsets.zero), + backgroundColor: WidgetStatePropertyAll( + widget.expanded && widget.selected ? Theme.of(context).colorScheme.primary : Colors.transparent, + ), + iconSize: const WidgetStatePropertyAll(24), + iconColor: WidgetStateProperty.resolveWith((states) { + return foreGroundColor; + }), + foregroundColor: WidgetStateProperty.resolveWith((states) { + return foreGroundColor; + })), + onPressed: widget.onPressed, + onLongPress: widget.onLongPress, child: widget.horizontal - ? Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: getChildren(context), - ) - : Column( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: getChildren(context), - ), - ), - ); - } - - List getChildren(BuildContext context) { - return [ - Flexible( - child: ElevatedButton( - style: ButtonStyle( - elevation: const WidgetStatePropertyAll(0), - padding: const WidgetStatePropertyAll(EdgeInsets.zero), - backgroundColor: const WidgetStatePropertyAll(Colors.transparent), - iconSize: const WidgetStatePropertyAll(24), - iconColor: WidgetStateProperty.resolveWith((states) { - return widget.selected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45); - }), - foregroundColor: WidgetStateProperty.resolveWith((states) { - return widget.selected - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45); - })), - onPressed: widget.onPressed, - child: AnimatedContainer( - curve: Curves.fastOutSlowIn, - duration: widget.duration, - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( + ? Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + child: SizedBox( + height: 35, + child: Row( + spacing: 4, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 250), + height: widget.selected ? 16 : 0, + margin: const EdgeInsets.only(top: 1.5), + width: 6, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context) + .colorScheme + .primary + .withValues(alpha: widget.selected && !widget.expanded ? 1 : 0), + ), + ), AnimatedSwitcher( duration: widget.duration, - child: widget.selected - ? widget.selectedIcon.setKey(Key("${widget.label}+selected")) - : widget.icon.setKey(Key("${widget.label}+normal")), + child: widget.selected ? widget.selectedIcon : widget.icon, ), - if (widget.horizontal && widget.label != null) - Padding( - padding: const EdgeInsets.only(left: 8.0), - child: _Label(widget: widget), - ) + const SizedBox(width: 6), + if (widget.horizontal && widget.expanded) ...[ + if (widget.label != null) + Expanded( + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 80), + child: Text( + widget.label!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + if (widget.trailing.isNotEmpty) + AnimatedOpacity( + duration: const Duration(milliseconds: 125), + opacity: showPopupButton ? 1 : 0, + child: PopupMenuButton( + tooltip: context.localized.options, + iconColor: foreGroundColor, + iconSize: 18, + itemBuilder: (context) => widget.trailing.popupMenuItems(useIcons: true), + ), + ) + ], ], ), - AnimatedContainer( - duration: const Duration(milliseconds: 250), - margin: EdgeInsets.only(top: widget.selected ? 8 : 0), - height: widget.selected ? 6 : 0, - width: widget.selected ? 14 : 0, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context).colorScheme.primary.withValues(alpha: widget.selected ? 1 : 0), + ), + ) + : Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + spacing: 8, + children: [ + AnimatedSwitcher( + duration: widget.duration, + child: widget.selected ? widget.selectedIcon : widget.icon, + ), + if (widget.label != null && widget.horizontal && widget.expanded) + Flexible(child: Text(widget.label!)) + ], ), - ), - ], + AnimatedContainer( + duration: const Duration(milliseconds: 250), + margin: EdgeInsets.only(top: widget.selected ? 4 : 0), + height: widget.selected ? 6 : 0, + width: widget.selected ? 14 : 0, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.primary.withValues(alpha: widget.selected ? 1 : 0), + ), + ), + ], + ), ), - ), - ), - ), ), - ]; - } -} - -class _Label extends StatelessWidget { - const _Label({required this.widget}); - - final NavigationButton widget; - - @override - Widget build(BuildContext context) { - return Text( - widget.label!, - maxLines: 1, - overflow: TextOverflow.fade, - style: - Theme.of(context).textTheme.labelMedium?.copyWith(color: Theme.of(context).colorScheme.onSecondaryContainer), ); } } diff --git a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart index a1adf7d..ba5ec29 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_drawer.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_drawer.dart @@ -6,12 +6,11 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/collection_types.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/models/view_model.dart'; import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/screens/metadata/refresh_metadata.dart'; import 'package:fladder/screens/shared/animated_fade_size.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; @@ -54,7 +53,7 @@ class NestedNavigationDrawer extends ConsumerWidget { ), IconButton( onPressed: () => toggleExpanded(false), - icon: const Icon(IconsaxPlusLinear.menu_1), + icon: const Icon(IconsaxPlusLinear.sidebar_left), ), ], ), @@ -71,16 +70,18 @@ class NestedNavigationDrawer extends ConsumerWidget { ), ), ), - ...destinations.map((destination) => DrawerListButton( - label: destination.label, - selected: context.router.current.name == destination.route?.routeName, - selectedIcon: destination.selectedIcon!, - icon: destination.icon!, - onPressed: () { - destination.action!(); - Scaffold.of(context).closeDrawer(); - }, - )), + ...destinations.map( + (destination) => DrawerListButton( + label: destination.label, + selected: context.router.current.name == destination.route?.routeName, + selectedIcon: destination.selectedIcon!, + icon: destination.icon!, + onPressed: () { + destination.action!(); + Scaffold.of(context).closeDrawer(); + }, + ), + ), if (views.isNotEmpty) ...{ const Divider(indent: 28, endIndent: 28), Padding( diff --git a/lib/widgets/navigation_scaffold/components/settings_user_icon.dart b/lib/widgets/navigation_scaffold/components/settings_user_icon.dart index 0befea7..055908a 100644 --- a/lib/widgets/navigation_scaffold/components/settings_user_icon.dart +++ b/lib/widgets/navigation_scaffold/components/settings_user_icon.dart @@ -2,26 +2,29 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; +import 'package:fladder/providers/update_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/routes/auto_router.gr.dart'; +import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/user_icon.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/theme_extensions.dart'; class SettingsUserIcon extends ConsumerWidget { const SettingsUserIcon({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final users = ref.watch(userProvider); + final size = 24.0; + final user = ref.watch(userProvider); + final hasNewUpdate = ref.watch(hasNewUpdateProvider); return Tooltip( message: context.localized.settings, waitDuration: const Duration(seconds: 1), - child: UserIcon( - user: users, - cornerRadius: 200, + child: FlatButton( onLongPress: () => context.router.push(const LockRoute()), onTap: () { if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) { @@ -30,6 +33,36 @@ class SettingsUserIcon extends ConsumerWidget { context.router.push(const ClientSettingsRoute()); } }, + child: Stack( + alignment: Alignment.bottomRight, + children: [ + UserIcon( + user: user, + cornerRadius: 200, + ), + if (hasNewUpdate) + Transform.translate( + offset: Offset(size / 4, size / 4), + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + color: context.colors.surface, + shape: BoxShape.circle, + ), + child: Padding( + padding: const EdgeInsets.all(2), + child: FittedBox( + child: Icon( + IconsaxPlusBold.information, + color: context.colors.primary, + ), + ), + ), + ), + ) + ], + ), ), ); } diff --git a/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart new file mode 100644 index 0000000..44ba989 --- /dev/null +++ b/lib/widgets/navigation_scaffold/components/side_navigation_bar.dart @@ -0,0 +1,255 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:overflow_view/overflow_view.dart'; + +import 'package:fladder/models/collection_types.dart'; +import 'package:fladder/providers/views_provider.dart'; +import 'package:fladder/routes/auto_router.gr.dart'; +import 'package:fladder/screens/metadata/refresh_metadata.dart'; +import 'package:fladder/screens/shared/animated_fade_size.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/adaptive_fab.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/navigation_button.dart'; +import 'package:fladder/widgets/navigation_scaffold/components/settings_user_icon.dart'; +import 'package:fladder/widgets/shared/item_actions.dart'; +import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; + +class SideNavigationBar extends ConsumerStatefulWidget { + final int currentIndex; + final List destinations; + final String currentLocation; + final Widget child; + final GlobalKey scaffoldKey; + const SideNavigationBar({ + required this.currentIndex, + required this.destinations, + required this.currentLocation, + required this.child, + required this.scaffoldKey, + super.key, + }); + + @override + ConsumerState createState() => _SideNavigationBarState(); +} + +class _SideNavigationBarState extends ConsumerState { + bool expandedSideBar = false; + bool showOnHover = false; + Timer? timer; + double currentWidth = 80; + + void startTimer() { + timer?.cancel(); + timer = Timer(const Duration(milliseconds: 650), () { + setState(() { + showOnHover = true; + }); + }); + } + + void stopTimer() { + timer?.cancel(); + timer = Timer(const Duration(milliseconds: 125), () { + setState(() { + showOnHover = false; + }); + }); + } + + @override + Widget build(BuildContext context) { + final views = ref.watch(viewsProvider.select((value) => value.views)); + final expandedWidth = 250.0; + final padding = MediaQuery.paddingOf(context); + + final collapsedWidth = 90.0 + padding.left; + final largeBar = AdaptiveLayout.layoutModeOf(context) != LayoutMode.single; + final fullyExpanded = largeBar ? expandedSideBar : false; + final shouldExpand = showOnHover || fullyExpanded; + final isDesktop = AdaptiveLayout.of(context).isDesktop; + return Stack( + children: [ + AdaptiveLayoutBuilder( + adaptiveLayout: AdaptiveLayout.of(context).copyWith( + sideBarWidth: fullyExpanded ? expandedWidth : collapsedWidth, + ), + child: (context) => widget.child, + ), + Container( + color: Theme.of(context).colorScheme.surface.withValues(alpha: shouldExpand ? 0.95 : 0.85), + width: shouldExpand ? expandedWidth : collapsedWidth, + child: MouseRegion( + onEnter: (value) => startTimer(), + onExit: (event) => stopTimer(), + onHover: (value) => startTimer(), + child: Column( + children: [ + if (isDesktop && AdaptiveLayout.of(context).platform != TargetPlatform.macOS) ...{ + const SizedBox(height: 4), + Text( + "Fladder", + style: Theme.of(context).textTheme.titleSmall, + ), + }, + if (AdaptiveLayout.of(context).platform == TargetPlatform.macOS) SizedBox(height: padding.top), + Expanded( + child: Padding( + key: const Key('navigation_rail'), + padding: padding.copyWith(right: 0, top: isDesktop ? 8 : null), + child: Column( + spacing: 2, + children: [ + Align( + alignment: largeBar && expandedSideBar ? Alignment.centerRight : Alignment.center, + child: Opacity( + opacity: largeBar && expandedSideBar ? 0.65 : 1.0, + child: IconButton( + onPressed: !largeBar + ? () => widget.scaffoldKey.currentState?.openDrawer() + : () => setState(() { + expandedSideBar = !expandedSideBar; + if (!expandedSideBar) { + showOnHover = false; + } + }), + icon: Icon( + largeBar && expandedSideBar ? IconsaxPlusLinear.sidebar_left : IconsaxPlusLinear.menu, + ), + ), + ), + ), + const SizedBox(height: 8), + if (largeBar) ...[ + AnimatedFadeSize( + duration: const Duration(milliseconds: 250), + child: shouldExpand ? actionButton(context).extended : actionButton(context).normal, + ), + ], + Expanded( + child: Column( + spacing: 2, + mainAxisAlignment: !largeBar ? MainAxisAlignment.center : MainAxisAlignment.start, + children: [ + ...widget.destinations.mapIndexed( + (index, destination) => + destination.toNavigationButton(widget.currentIndex == index, true, shouldExpand), + ), + if (views.isNotEmpty && largeBar) ...[ + const Divider( + indent: 32, + endIndent: 32, + ), + Flexible( + child: OverflowView.flexible( + direction: Axis.vertical, + spacing: 4, + children: views.map( + (view) { + final actions = [ + ItemActionButton( + label: Text(context.localized.scanLibrary), + icon: const Icon(IconsaxPlusLinear.refresh), + action: () => showRefreshPopup(context, view.id, view.name), + ) + ]; + return view.toNavigationButton( + context.router.currentUrl.contains(view.id), + true, + shouldExpand, + () => context.pushRoute(LibrarySearchRoute(viewModelId: view.id)), + onLongPress: () => showBottomSheetPill( + context: context, + content: (context, scrollController) => ListView( + shrinkWrap: true, + controller: scrollController, + children: actions.listTileItems(context, useIcons: true), + ), + ), + trailing: actions, + ); + }, + ).toList(), + builder: (context, remaining) { + return PopupMenuButton( + iconColor: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.45), + padding: EdgeInsets.zero, + icon: NavigationButton( + label: context.localized.other, + selectedIcon: const Icon(IconsaxPlusLinear.arrow_square_down), + icon: const Icon(IconsaxPlusLinear.arrow_square_down), + expanded: shouldExpand, + horizontal: true, + ), + itemBuilder: (context) => views + .sublist(views.length - remaining) + .map( + (e) => PopupMenuItem( + onTap: () => context.pushRoute(LibrarySearchRoute(viewModelId: e.id)), + child: Row( + spacing: 8, + children: [ + Icon(e.collectionType.iconOutlined), + Text(e.name), + ], + ), + ), + ) + .toList(), + ); + }, + ), + ), + ], + ], + ), + ), + NavigationButton( + label: context.localized.settings, + selected: widget.currentLocation.contains(const SettingsRoute().routeName), + selectedIcon: const Icon(IconsaxPlusBold.setting_3), + horizontal: true, + expanded: shouldExpand, + icon: const SettingsUserIcon(), + onPressed: () { + if (AdaptiveLayout.layoutModeOf(context) == LayoutMode.single) { + context.router.push(const SettingsRoute()); + } else { + context.router.push(const ClientSettingsRoute()); + } + }, + ), + ], + ), + ), + ), + if (AdaptiveLayout.of(context).inputDevice == InputDevice.pointer) const SizedBox(height: 16), + ], + ), + ), + ), + ], + ); + } + + AdaptiveFab actionButton(BuildContext context) { + return ((widget.currentIndex >= 0 && widget.currentIndex < widget.destinations.length) + ? widget.destinations[widget.currentIndex].floatingActionButton + : null) ?? + AdaptiveFab( + context: context, + title: context.localized.search, + key: const Key("Search"), + onPressed: () => context.router.navigate(LibrarySearchRoute()), + child: const Icon(IconsaxPlusLinear.search_normal_1), + ); + } +} diff --git a/lib/widgets/navigation_scaffold/navigation_scaffold.dart b/lib/widgets/navigation_scaffold/navigation_scaffold.dart index 648a1ed..c6cbb47 100644 --- a/lib/widgets/navigation_scaffold/navigation_scaffold.dart +++ b/lib/widgets/navigation_scaffold/navigation_scaffold.dart @@ -1,15 +1,16 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/media_playback_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/providers/views_provider.dart'; import 'package:fladder/routes/auto_router.dart'; +import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/nested_bottom_appbar.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/theme_extensions.dart'; import 'package:fladder/widgets/navigation_scaffold/components/destination_model.dart'; import 'package:fladder/widgets/navigation_scaffold/components/fladder_app_bar.dart'; import 'package:fladder/widgets/navigation_scaffold/components/floating_player_bar.dart'; @@ -51,11 +52,15 @@ class _NavigationScaffoldState extends ConsumerState { @override Widget build(BuildContext context) { - final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state)); final views = ref.watch(viewsProvider.select((value) => value.views)); + final playerState = ref.watch(mediaPlaybackProvider.select((value) => value.state)); + final showPlayerBar = playerState == VideoPlayerState.minimized; - final isHomeRoutes = homeRoutes.any((element) => element.name.contains(context.router.current.name)); + final isDesktop = AdaptiveLayout.of(context).isDesktop; + final bottomPadding = isDesktop || kIsWeb ? 0.0 : MediaQuery.paddingOf(context).bottom; + + final isHomeScreen = currentIndex != -1; return PopScope( canPop: currentIndex == 0, onPopInvokedWithResult: (didPop, result) { @@ -63,67 +68,86 @@ class _NavigationScaffoldState extends ConsumerState { widget.destinations.first.action!(); } }, - child: Scaffold( - key: _key, - appBar: const FladderAppBar(), - extendBodyBehindAppBar: true, - resizeToAvoidBottomInset: false, - extendBody: true, - floatingActionButtonAnimator: - playerState == VideoPlayerState.minimized ? FloatingActionButtonAnimator.noAnimation : null, - floatingActionButtonLocation: - playerState == VideoPlayerState.minimized ? FloatingActionButtonLocation.centerFloat : null, - floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeRoutes - ? switch (playerState) { - VideoPlayerState.minimized => AdaptiveLayout.viewSizeOf(context) == ViewSize.phone - ? const Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: FloatingPlayerBar(), - ) - : null, - _ => currentIndex != -1 + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Positioned.fill( + child: Padding( + padding: EdgeInsets.only(bottom: showPlayerBar ? floatingPlayerHeight - 12 + bottomPadding : 0), + child: Scaffold( + key: _key, + appBar: const FladderAppBar(), + extendBodyBehindAppBar: true, + resizeToAvoidBottomInset: false, + extendBody: true, + floatingActionButton: AdaptiveLayout.layoutModeOf(context) == LayoutMode.single && isHomeScreen ? widget.destinations.elementAtOrNull(currentIndex)?.floatingActionButton?.normal : null, - } - : null, - drawer: NestedNavigationDrawer( - actionButton: null, - toggleExpanded: (value) => _key.currentState?.closeDrawer(), - views: views, - destinations: widget.destinations, - currentLocation: currentLocation, - ), - bottomNavigationBar: AdaptiveLayout.viewSizeOf(context) == ViewSize.phone - ? HideOnScroll( - controller: AdaptiveLayout.scrollOf(context), - forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)), - child: NestedBottomAppBar( - child: Transform.translate( - offset: const Offset(0, 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: widget.destinations - .map( - (destination) => destination.toNavigationButton( - widget.currentRouteName == destination.route?.routeName, false), - ) - .toList(), - ), + drawer: homeRoutes.any((element) => element.name.contains(currentLocation)) + ? NestedNavigationDrawer( + actionButton: null, + toggleExpanded: (value) => _key.currentState?.closeDrawer(), + views: views, + destinations: widget.destinations, + currentLocation: currentLocation, + ) + : null, + bottomNavigationBar: isHomeScreen && AdaptiveLayout.viewSizeOf(context) == ViewSize.phone + ? HideOnScroll( + controller: AdaptiveLayout.scrollOf(context), + forceHide: !homeRoutes.any((element) => element.name.contains(currentLocation)), + child: NestedBottomAppBar( + child: SizedBox( + height: 65, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: widget.destinations + .map( + (destination) => destination.toNavigationButton( + widget.currentRouteName == destination.route?.routeName, false, false), + ) + .toList(), + ), + ), + ), + ) + : null, + body: widget.nestedChild != null + ? NavigationBody( + child: widget.nestedChild!, + parentContext: context, + currentIndex: currentIndex, + destinations: widget.destinations, + currentLocation: currentLocation, + drawerKey: _key, + ) + : null, + ), + ), + ), + Material( + color: Colors.transparent, + child: AnimatedFadeSize( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: context.colors.surface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(12), + topRight: Radius.circular(12), ), ), - ) - : null, - body: widget.nestedChild != null - ? NavigationBody( - child: widget.nestedChild!, - parentContext: context, - currentIndex: currentIndex, - destinations: widget.destinations, - currentLocation: currentLocation, - drawerKey: _key, - ) - : null, + child: showPlayerBar + ? Padding( + padding: EdgeInsets.only(bottom: bottomPadding), + child: const FloatingPlayerBar(), + ) + : const SizedBox.shrink(), + ), + ), + ) + ], ), ); } diff --git a/lib/widgets/shared/alert_content.dart b/lib/widgets/shared/alert_content.dart index ebb1c48..e1f3e65 100644 --- a/lib/widgets/shared/alert_content.dart +++ b/lib/widgets/shared/alert_content.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:fladder/util/list_padding.dart'; - class ActionContent extends StatelessWidget { final Widget? title; final Widget child; @@ -23,6 +21,7 @@ class ActionContent extends StatelessWidget { padding: padding ?? MediaQuery.paddingOf(context).add(const EdgeInsets.symmetric(horizontal: 16)), child: Column( mainAxisSize: MainAxisSize.min, + spacing: 16, children: [ if (title != null) ...[ title!, @@ -42,7 +41,7 @@ class ActionContent extends StatelessWidget { children: actions, ) ], - ].addInBetween(const SizedBox(height: 16)), + ], ), ); } diff --git a/lib/widgets/shared/background_item_image.dart b/lib/widgets/shared/background_item_image.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/widgets/shared/button_group.dart b/lib/widgets/shared/button_group.dart new file mode 100644 index 0000000..d7e54ab --- /dev/null +++ b/lib/widgets/shared/button_group.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + +class ExpressiveButtonGroup extends StatelessWidget { + final List> options; + final Set selectedValues; + final ValueChanged> onSelected; + final bool multiSelection; + + const ExpressiveButtonGroup({ + super.key, + required this.options, + required this.selectedValues, + required this.onSelected, + this.multiSelection = false, + }); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.max, + spacing: 2, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: List.generate(options.length, (index) { + final option = options[index]; + final isSelected = selectedValues.contains(option.value); + final isFirst = index == 0; + final isLast = index == options.length - 1; + + final borderRadius = BorderRadius.horizontal( + left: isSelected || isFirst ? const Radius.circular(20) : const Radius.circular(6), + right: isSelected || isLast ? const Radius.circular(20) : const Radius.circular(6), + ); + + return ElevatedButton.icon( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder(borderRadius: borderRadius), + elevation: isSelected ? 3 : 0, + backgroundColor: isSelected + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.surfaceContainerHighest, + foregroundColor: + isSelected ? Theme.of(context).colorScheme.onPrimary : Theme.of(context).colorScheme.onSurfaceVariant, + textStyle: Theme.of(context).textTheme.labelLarge, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), + onPressed: () { + final newSet = Set.from(selectedValues); + if (multiSelection) { + isSelected ? newSet.remove(option.value) : newSet.add(option.value); + } else { + newSet + ..clear() + ..add(option.value); + } + onSelected(newSet); + }, + label: option.child, + icon: isSelected ? option.selected ?? const Icon(Icons.check_rounded) : option.icon, + ); + }), + ); + } +} + +class ButtonGroupOption { + final T value; + final Icon? icon; + final Icon? selected; + final Widget child; + + const ButtonGroupOption({ + required this.value, + this.icon, + this.selected, + required this.child, + }); +} diff --git a/lib/widgets/shared/enum_selection.dart b/lib/widgets/shared/enum_selection.dart index cb72ef8..d661159 100644 --- a/lib/widgets/shared/enum_selection.dart +++ b/lib/widgets/shared/enum_selection.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; import 'package:fladder/screens/shared/flat_button.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/widgets/shared/modal_bottom_sheet.dart'; class EnumBox extends StatelessWidget { diff --git a/lib/widgets/shared/fladder_scrollbar.dart b/lib/widgets/shared/fladder_scrollbar.dart index c95d87b..fd9f124 100644 --- a/lib/widgets/shared/fladder_scrollbar.dart +++ b/lib/widgets/shared/fladder_scrollbar.dart @@ -1,5 +1,6 @@ -import 'package:flexible_scrollbar/flexible_scrollbar.dart'; import 'package:flutter/material.dart'; + +import 'package:flexible_scrollbar/flexible_scrollbar.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class FladderScrollbar extends ConsumerWidget { diff --git a/lib/widgets/shared/full_screen_button.dart b/lib/widgets/shared/full_screen_button.dart deleted file mode 100644 index 4720dde..0000000 --- a/lib/widgets/shared/full_screen_button.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:window_manager/window_manager.dart'; - -import 'package:fladder/providers/video_player_provider.dart'; - -Future closeFullScreen() async { - final isFullScreen = await windowManager.isFullScreen(); - if (isFullScreen) { - await windowManager.setFullScreen(false); - } -} - -Future toggleFullScreen(WidgetRef ref) async { - final isFullScreen = await windowManager.isFullScreen(); - await windowManager.setFullScreen(!isFullScreen); - ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(fullScreen: !isFullScreen)); -} - -class FullScreenButton extends ConsumerWidget { - const FullScreenButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final fullScreen = ref.watch(mediaPlaybackProvider.select((value) => value.fullScreen)); - return IconButton( - onPressed: () => toggleFullScreen(ref), - icon: Icon( - fullScreen ? IconsaxPlusLinear.screenmirroring : IconsaxPlusLinear.maximize_4, - ), - ); - } -} diff --git a/lib/widgets/shared/full_screen_button_web.dart b/lib/widgets/shared/full_screen_button_web.dart deleted file mode 100644 index 2a48335..0000000 --- a/lib/widgets/shared/full_screen_button_web.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:universal_html/html.dart' as html; - -import 'package:fladder/providers/video_player_provider.dart'; - -Future closeFullScreen() async { - if (html.document.fullscreenElement != null) { - html.document.exitFullscreen(); - await Future.delayed(const Duration(milliseconds: 500)); - } -} - -Future toggleFullScreen(WidgetRef ref) async { - final isFullScreen = html.document.fullscreenElement != null; - - if (isFullScreen) { - html.document.exitFullscreen(); - //Wait for 1 second - await Future.delayed(const Duration(seconds: 1)); - } else { - await html.document.documentElement?.requestFullscreen(); - } - ref - .read(mediaPlaybackProvider.notifier) - .update((state) => state.copyWith(fullScreen: html.document.fullscreenElement != null)); -} - -class FullScreenButton extends ConsumerWidget { - const FullScreenButton({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final fullScreen = ref.watch(mediaPlaybackProvider.select((value) => value.fullScreen)); - return IconButton( - onPressed: () => toggleFullScreen(ref), - icon: Icon( - fullScreen ? IconsaxPlusLinear.screenmirroring : IconsaxPlusLinear.maximize_4, - ), - ); - } -} diff --git a/lib/widgets/shared/hide_on_scroll.dart b/lib/widgets/shared/hide_on_scroll.dart index e543aa5..31f4e0c 100644 --- a/lib/widgets/shared/hide_on_scroll.dart +++ b/lib/widgets/shared/hide_on_scroll.dart @@ -3,8 +3,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; class HideOnScroll extends ConsumerStatefulWidget { final Widget? child; @@ -28,59 +27,64 @@ class HideOnScroll extends ConsumerStatefulWidget { } class _HideOnScrollState extends ConsumerState { - late final scrollController = widget.controller ?? ScrollController(); + late final ScrollController scrollController = widget.controller ?? ScrollController(); bool isVisible = true; - bool atEdge = false; @override void initState() { super.initState(); - scrollController.addListener(listen); + scrollController.addListener(_onScroll); } @override void dispose() { - scrollController.removeListener(listen); + scrollController.removeListener(_onScroll); + if (widget.controller == null) { + scrollController.dispose(); + } super.dispose(); } - void listen() { - final direction = scrollController.position.userScrollDirection; + void _onScroll() { + final position = scrollController.position; + final direction = position.userScrollDirection; - if (scrollController.offset < scrollController.position.maxScrollExtent) { - if (direction == ScrollDirection.forward) { - if (!isVisible) { - setState(() => isVisible = true); - } - } else if (direction == ScrollDirection.reverse) { - if (isVisible) { - setState(() => isVisible = false); - } - } + bool newVisible; + if (position.atEdge && position.pixels >= position.maxScrollExtent) { + // Always show when scrolled to bottom + newVisible = true; } else { - setState(() { - isVisible = true; - }); + newVisible = direction == ScrollDirection.forward; + } + + if (newVisible != isVisible) { + setState(() => isVisible = newVisible); } } @override Widget build(BuildContext context) { - if (widget.visibleBuilder != null) return widget.visibleBuilder!(isVisible)!; + if (widget.visibleBuilder != null) { + return widget.visibleBuilder!(isVisible) ?? const SizedBox(); + } + if (widget.child == null) return const SizedBox(); + if (AdaptiveLayout.viewSizeOf(context) == ViewSize.desktop) { return widget.child!; - } else { - return AnimatedAlign( - alignment: const Alignment(0, -1), - heightFactor: widget.forceHide - ? 0 - : isVisible - ? 1.0 - : 0, - duration: widget.duration, - child: Wrap(children: [widget.child!]), - ); } + + return AnimatedAlign( + alignment: const Alignment(0, -1), + heightFactor: widget.forceHide + ? 0 + : isVisible + ? 1.0 + : 0, + duration: widget.duration, + child: Wrap( + children: [widget.child!], + ), + ); } } diff --git a/lib/widgets/shared/horizontal_list.dart b/lib/widgets/shared/horizontal_list.dart index 43b42f7..0130734 100644 --- a/lib/widgets/shared/horizontal_list.dart +++ b/lib/widgets/shared/horizontal_list.dart @@ -2,20 +2,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/sticky_header_text.dart'; -class HorizontalList extends ConsumerStatefulWidget { +class HorizontalList extends ConsumerStatefulWidget { final String? label; final List titleActions; final Function()? onLabelClick; final String? subtext; - final List items; + final List items; final int? startIndex; final Widget Function(BuildContext context, int index) itemBuilder; final bool scrollToEnd; @@ -42,19 +41,16 @@ class HorizontalList extends ConsumerStatefulWidget { } class _HorizontalListState extends ConsumerState { - final itemScrollController = ItemScrollController(); - late final scrollOffsetController = ScrollOffsetController(); + final GlobalKey _firstItemKey = GlobalKey(); + final ScrollController _scrollController = ScrollController(); + final contentPadding = 8.0; + double? contentWidth; + double? _firstItemWidth; @override void initState() { super.initState(); - Future.microtask(() async { - if (widget.startIndex != null) { - itemScrollController.jumpTo(index: widget.startIndex!); - scrollOffsetController.animateScroll( - offset: -widget.contentPadding.left, duration: const Duration(milliseconds: 125)); - } - }); + _measureFirstItem(scrollTo: true); } @override @@ -62,19 +58,56 @@ class _HorizontalListState extends ConsumerState { super.dispose(); } + void _measureFirstItem({bool scrollTo = false}) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.startIndex != null) { + final context = _firstItemKey.currentContext; + if (context != null) { + final box = context.findRenderObject() as RenderBox; + _firstItemWidth = box.size.width; + if (scrollTo) { + _scrollToPosition(widget.startIndex!); + } + } + } + }); + } + + void _scrollToPosition(int index) { + final offset = index * _firstItemWidth! + index * contentPadding; + _scrollController.animateTo( + offset, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); + } + void _scrollToStart() { - itemScrollController.scrollTo(index: 0, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut); + _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); } void _scrollToEnd() { - itemScrollController.scrollTo( - index: widget.items.length, duration: const Duration(milliseconds: 250), curve: Curves.easeInOut); + _scrollController.animateTo( + (_firstItemWidth ?? 200) * widget.items.length + 200, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ); + } + + int getFirstVisibleIndex() { + if (widget.startIndex == null) return 0; + if (!_scrollController.hasClients || _firstItemWidth == null) return 0; + return (_scrollController.offset / _firstItemWidth!).floor().clamp(0, widget.items.length - 1); } @override Widget build(BuildContext context) { final hasPointer = AdaptiveLayout.of(context).inputDevice == InputDevice.pointer; - return Column( + final content = Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -97,11 +130,13 @@ class _HorizontalListState extends ConsumerState { ), ), if (widget.subtext != null) - Opacity( - opacity: 0.5, - child: Text( - widget.subtext!, - style: Theme.of(context).textTheme.titleMedium, + Flexible( + child: Opacity( + opacity: 0.5, + child: Text( + widget.subtext!, + style: Theme.of(context).textTheme.titleMedium, + ), ), ), ...widget.titleActions @@ -120,8 +155,8 @@ class _HorizontalListState extends ConsumerState { onLongPress: () => _scrollToStart(), child: IconButton( onPressed: () { - scrollOffsetController.animateScroll( - offset: -(MediaQuery.of(context).size.width / 1.75), + _scrollController.animateTo( + _scrollController.offset + -(MediaQuery.of(context).size.width / 1.75), duration: const Duration(milliseconds: 250), curve: Curves.easeInOut); }, @@ -134,12 +169,8 @@ class _HorizontalListState extends ConsumerState { IconButton( tooltip: "Scroll to current", onPressed: () { - if (widget.startIndex != null) { - itemScrollController.jumpTo(index: widget.startIndex!); - scrollOffsetController.animateScroll( - offset: -widget.contentPadding.left, - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOutQuad); + if (_firstItemWidth != null && widget.startIndex != null) { + _scrollToPosition(widget.startIndex!); } }, icon: const Icon( @@ -151,8 +182,8 @@ class _HorizontalListState extends ConsumerState { onLongPress: () => _scrollToEnd(), child: IconButton( onPressed: () { - scrollOffsetController.animateScroll( - offset: (MediaQuery.of(context).size.width / 1.75), + _scrollController.animateTo( + _scrollController.offset + (MediaQuery.of(context).size.width / 1.75), duration: const Duration(milliseconds: 250), curve: Curves.easeInOut); }, @@ -170,23 +201,30 @@ class _HorizontalListState extends ConsumerState { ), const SizedBox(height: 8), SizedBox( - height: widget.height ?? + height: (widget.height ?? AdaptiveLayout.poster(context).size * - ref.watch(clientSettingsProvider.select((value) => value.posterSize)), - child: ScrollablePositionedList.separated( - shrinkWrap: widget.shrinkWrap, - itemScrollController: itemScrollController, - scrollOffsetController: scrollOffsetController, - padding: widget.contentPadding, - itemCount: widget.items.length, + ref.watch(clientSettingsProvider.select((value) => value.posterSize))), + child: ListView.separated( + controller: _scrollController, scrollDirection: Axis.horizontal, - separatorBuilder: (context, index) => const SizedBox( - width: 16, - ), - itemBuilder: widget.itemBuilder, + padding: widget.contentPadding, + itemBuilder: (context, index) => index == getFirstVisibleIndex() + ? Container( + key: _firstItemKey, + child: widget.itemBuilder(context, index), + ) + : widget.itemBuilder(context, index), + separatorBuilder: (context, index) => SizedBox(width: contentPadding), + itemCount: widget.items.length, ), ), ], ); + return widget.startIndex == null + ? content + : LayoutBuilder(builder: (context, constraints) { + _measureFirstItem(); + return content; + }); } } diff --git a/lib/widgets/shared/modal_bottom_sheet.dart b/lib/widgets/shared/modal_bottom_sheet.dart index 3a06205..8733092 100644 --- a/lib/widgets/shared/modal_bottom_sheet.dart +++ b/lib/widgets/shared/modal_bottom_sheet.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/item_base_model.dart'; -import 'package:fladder/models/settings/home_settings_model.dart'; -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/theme.dart'; import 'package:fladder/util/fladder_image.dart'; Future showBottomSheetPill({ @@ -18,31 +17,58 @@ Future showBottomSheetPill({ ScrollController scrollController, ) content, }) async { - final screenSize = MediaQuery.sizeOf(context); await showModalBottomSheet( isScrollControlled: true, + backgroundColor: Colors.transparent, useRootNavigator: true, - showDragHandle: true, enableDrag: true, context: context, - constraints: AdaptiveLayout.viewSizeOf(context) == ViewSize.phone - ? BoxConstraints(maxHeight: screenSize.height * 0.9) - : BoxConstraints(maxWidth: screenSize.width * 0.75, maxHeight: screenSize.height * 0.85), builder: (context) { final controller = ScrollController(); - return ListView( - shrinkWrap: true, - controller: controller, - children: [ - if (item != null) ...{ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: ItemBottomSheetPreview(item: item), + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.sizeOf(context).height * 0.85, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8).add(MediaQuery.paddingOf(context)), + child: Card( + shape: RoundedRectangleBorder( + borderRadius: FladderTheme.largeShape.borderRadius, ), - const Divider(), - }, - content(context, controller), - ], + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Container( + height: 8, + width: 35, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurface, + borderRadius: FladderTheme.largeShape.borderRadius, + ), + ), + ), + Flexible( + child: ListView( + shrinkWrap: true, + controller: controller, + children: [ + if (item != null) ...{ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: ItemBottomSheetPreview(item: item), + ), + const Divider(), + }, + content(context, ScrollController()), + ], + ), + ), + ], + ), + ), + ), ); }, ); diff --git a/lib/widgets/shared/modal_side_sheet.dart b/lib/widgets/shared/modal_side_sheet.dart index 360a8a0..60601a5 100644 --- a/lib/widgets/shared/modal_side_sheet.dart +++ b/lib/widgets/shared/modal_side_sheet.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:fladder/theme.dart'; + Future showModalSideSheet( BuildContext context, { required Widget content, @@ -30,13 +32,18 @@ Future showModalSideSheet( pageBuilder: (context, animation1, animation2) { return Align( alignment: Alignment.centerRight, - child: Sheet( - header: header, - backButton: backButton, - closeButton: closeButton, - actions: actions, - content: content, - addDivider: addDivider, + child: Padding( + padding: const EdgeInsets.all(16.0).copyWith( + top: MediaQuery.paddingOf(context).top, + ), + child: Sheet( + header: header, + backButton: backButton, + closeButton: closeButton, + actions: actions, + content: content, + addDivider: addDivider, + ), ), ); }, @@ -64,32 +71,40 @@ class Sheet extends StatelessWidget { @override Widget build(BuildContext context) { - final size = MediaQuery.of(context).size; + final mediaQuery = MediaQuery.of(context); + final size = mediaQuery.size; final theme = Theme.of(context); final colorScheme = theme.colorScheme; + final padding = mediaQuery.padding.copyWith(left: 0, top: 0); - return Material( - elevation: 1, - color: colorScheme.surface, - surfaceTintColor: colorScheme.onSurface, - borderRadius: const BorderRadius.horizontal(left: Radius.circular(20)), - child: Padding( - padding: MediaQuery.of(context).padding, - child: Container( - constraints: BoxConstraints( - minWidth: 256, - maxWidth: size.width <= 600 ? size.width : 400, - minHeight: size.height, - maxHeight: size.height, - ), - child: Column( - children: [ - _buildHeader(context), - Expanded( - child: content, - ), - if (actions?.isNotEmpty ?? false) _buildFooter(context) - ], + return MediaQuery( + data: mediaQuery.copyWith( + padding: mediaQuery.padding.copyWith( + left: 0, + )), + child: Material( + elevation: 1, + color: colorScheme.surface, + surfaceTintColor: colorScheme.onSurface, + borderRadius: FladderTheme.largeShape.borderRadius, + child: Padding( + padding: padding, + child: Container( + constraints: BoxConstraints( + minWidth: 256, + maxWidth: size.width <= 600 ? size.width : 400, + minHeight: size.height, + maxHeight: size.height, + ), + child: Column( + children: [ + _buildHeader(context), + Expanded( + child: content, + ), + if (actions?.isNotEmpty ?? false) _buildFooter(context) + ], + ), ), ), ), diff --git a/lib/widgets/shared/pull_to_refresh.dart b/lib/widgets/shared/pull_to_refresh.dart index 3d6a17b..38eb684 100644 --- a/lib/widgets/shared/pull_to_refresh.dart +++ b/lib/widgets/shared/pull_to_refresh.dart @@ -1,4 +1,4 @@ -import 'package:fladder/util/adaptive_layout.dart'; +import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/refresh_state.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/shared/scroll_position.dart b/lib/widgets/shared/scroll_position.dart index fb3bc6d..11924da 100644 --- a/lib/widgets/shared/scroll_position.dart +++ b/lib/widgets/shared/scroll_position.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; enum ScrollState { @@ -22,45 +23,41 @@ class ScrollStatePosition extends ConsumerStatefulWidget { } class _ScrollStatePositionState extends ConsumerState { - late final scrollController = widget.controller ?? ScrollController(); - ScrollState scrollState = ScrollState.top; + late final ScrollController _scrollController = widget.controller ?? ScrollController(); + ScrollState _scrollState = ScrollState.top; @override void initState() { super.initState(); - scrollController.addListener(listen); + _scrollController.addListener(_onScroll); } @override void dispose() { - scrollController.removeListener(listen); + _scrollController.removeListener(_onScroll); + if (widget.controller == null) { + _scrollController.dispose(); + } super.dispose(); } - void listen() { - if (scrollController.offset < scrollController.position.maxScrollExtent) { - if (scrollController.position.atEdge) { - bool isTop = scrollController.position.pixels == 0; - if (isTop) { - setState(() { - scrollState = ScrollState.top; - }); - print('At the top'); - } else { - setState(() { - scrollState = ScrollState.bottom; - }); - } - } else { - setState(() { - scrollState = ScrollState.middle; - }); - } + void _onScroll() { + final position = _scrollController.position; + final newState = () { + if (position.pixels == 0) return ScrollState.top; + if (position.pixels >= position.maxScrollExtent) return ScrollState.bottom; + return ScrollState.middle; + }(); + + if (newState != _scrollState) { + setState(() { + _scrollState = newState; + }); } } @override Widget build(BuildContext context) { - return widget.positionBuilder(scrollState); + return widget.positionBuilder(_scrollState); } } diff --git a/lib/wrappers/media_control_wrapper.dart b/lib/wrappers/media_control_wrapper.dart index c480dd5..5ad95c6 100644 --- a/lib/wrappers/media_control_wrapper.dart +++ b/lib/wrappers/media_control_wrapper.dart @@ -296,6 +296,9 @@ class MediaControlsWrapper extends BaseAudioHandler { @override Future seek(Duration position) { _player?.seek(position); + if (_player?.lastState.playing == false) { + ref.read(mediaPlaybackProvider.notifier).update((state) => state.copyWith(position: position)); + } return super.seek(position); } diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index c3f0eb4..0485f9a 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -14,6 +14,7 @@ #include #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -41,6 +42,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) volume_controller_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "VolumeControllerPlugin"); + volume_controller_plugin_register_with_registrar(volume_controller_registrar); g_autoptr(FlPluginRegistrar) window_manager_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); window_manager_plugin_register_with_registrar(window_manager_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 38a1bfc..5bfb5da 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -11,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_video screen_retriever_linux url_launcher_linux + volume_controller window_manager ) diff --git a/pubspec.lock b/pubspec.lock index 23bd056..917cbff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -50,10 +50,10 @@ packages: dependency: "direct main" description: name: archive - sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12" + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "4.0.5" + version: "4.0.7" args: dependency: transitive description: @@ -66,18 +66,18 @@ packages: dependency: "direct main" description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" audio_service: dependency: "direct main" description: name: audio_service - sha256: "887ddf15fce31fd12aa8044c3bffd14c58929fb20e31d96284fe3aaf48315ac6" + sha256: cb122c7c2639d2a992421ef96b67948ad88c5221da3365ccef1031393a76e044 url: "https://pub.dev" source: hosted - version: "0.18.17" + version: "0.18.18" audio_service_platform_interface: dependency: transitive description: @@ -130,10 +130,10 @@ packages: dependency: "direct main" description: name: background_downloader - sha256: c3814aa0466368a4c1c32d24bc73ded752edacf06731ead43857a3ac992ed52d + sha256: d3016a9eb584f6cb16384c8b4a008943c39119730d60046044349b5dbbda4ccb url: "https://pub.dev" source: hosted - version: "8.9.5" + version: "9.2.2" boolean_selector: dependency: transitive description: @@ -210,10 +210,10 @@ packages: dependency: transitive description: name: built_value - sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + sha256: "082001b5c3dc495d4a42f1d5789990505df20d8547d42507c29050af6933ee27" url: "https://pub.dev" source: hosted - version: "8.9.5" + version: "8.10.1" cached_network_image: dependency: "direct main" description: @@ -258,18 +258,18 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" chewie: dependency: transitive description: name: chewie - sha256: df6711bc3ba165ad19cb496e350250be5673327f79c61c9cc8a15088ed8007ed + sha256: "4d9554a8f87cc2dc6575dfd5ad20a4375015a29edd567fd6733febe6365e2566" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.3" chopper: dependency: "direct main" description: @@ -330,10 +330,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27" + sha256: "051849e2bd7c7b3bc5844ea0d096609ddc3a859890ec3a9ac4a65a2620cc1f99" url: "https://pub.dev" source: hosted - version: "6.1.3" + version: "6.1.4" connectivity_plus_platform_interface: dependency: transitive description: @@ -442,10 +442,10 @@ packages: dependency: "direct main" description: name: desktop_drop - sha256: "03abf1c0443afdd1d65cf8fa589a2f01c67a11da56bbb06f6ea1de79d5628e94" + sha256: bd21017e0415632c85f6b813c846bc8c9811742507776dcf6abf91a14d946e98 url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.6.0" diffutil_dart: dependency: transitive description: @@ -474,26 +474,26 @@ packages: dependency: "direct main" description: name: extended_image - sha256: "85199f9233e03abc2ce2e68cbb2991648666af4a527ae4e6250935be8edfddae" + sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "10.0.1" extended_image_library: dependency: transitive description: name: extended_image_library - sha256: e61dafd94400fff6ef7ed1523d445ff3af137f198f3228e4a3107bc5b4bec5d1 + sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.0.1" fake_async: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -514,10 +514,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: ab13ae8ef5580a411c458d6207b6774a6c237d77ac37011b13994879f68a8810 + sha256: "77f8e81d22d2a07d0dee2c62e1dda71dc1da73bf43bb2d45af09727406167964" url: "https://pub.dev" source: hosted - version: "8.3.7" + version: "10.1.9" fixnum: dependency: transitive description: @@ -543,10 +543,10 @@ packages: dependency: "direct main" description: name: flutter_blurhash - sha256: "5e67678e479ac639069d7af1e133f4a4702311491188ff3e0227486430db0c06" + sha256: e97b9aff13b9930bbaa74d0d899fec76e3f320aba3190322dcc5d32104e3d25d url: "https://pub.dev" source: hosted - version: "0.8.2" + version: "0.9.1" flutter_cache_manager: dependency: "direct main" description: @@ -559,42 +559,50 @@ packages: dependency: "direct main" description: name: flutter_custom_tabs - sha256: ec5424e196f145ac32e92ddfaee07b883cd4f16f9972812fa8f67fb318b34a4c + sha256: ac3543d7b4e0ac6ecdf3744360039ebb573656c0ce759149d228e1934d4f7535 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.0" flutter_custom_tabs_android: dependency: transitive description: name: flutter_custom_tabs_android - sha256: "09bc6297fca815a721d21fa168335ad9d266ac3df483628e716d788b7caf191e" + sha256: "925fc5e7d27372ee523962dcfcd4b77c0443549482c284dd7897b77815547621" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" flutter_custom_tabs_ios: dependency: transitive description: name: flutter_custom_tabs_ios - sha256: "4c93cba13e51e6edafd6f6984811b31fa774ef0ef58a58b93164f7eff18d5dc3" + sha256: c61a58d30b29ccb09ea4da0daa335bbf8714bcf8798d0d9f4f58a0b83c6c421b url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.0" flutter_custom_tabs_platform_interface: dependency: transitive description: name: flutter_custom_tabs_platform_interface - sha256: d6056832fceb163cabb751b3dd8a4919c03677cad7e4728de8ebcf84cfa4a074 + sha256: "54a6ff5cc7571cb266a47ade9f6f89d1980b9ed2dba18162a6d5300afc408449" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" flutter_custom_tabs_web: dependency: transitive description: name: flutter_custom_tabs_web - sha256: f90a09501c8be7db4604da1d504f39b73c7525dc60f2ffce4cdea0f015e42eb0 + sha256: "236e035c73b6d3ef0a2f85cd8b6b815954e7559c9f9d50a15ed2e53a297b58b0" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" + flutter_highlight: + dependency: transitive + description: + name: flutter_highlight + sha256: "7b96333867aa07e122e245c033b8ad622e4e3a42a1a2372cbb098a2541d8782c" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_keyboard_visibility: dependency: transitive description: @@ -647,10 +655,10 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -660,10 +668,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.0.28" flutter_riverpod: dependency: "direct main" description: @@ -688,14 +696,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + flutter_sticky_header: + dependency: "direct main" + description: + name: flutter_sticky_header + sha256: fb4fda6164ef3e5fc7ab73aba34aad253c17b7c6ecf738fa26f1a905b7d2d1e2 + url: "https://pub.dev" + source: hosted + version: "0.8.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -718,18 +734,18 @@ packages: dependency: "direct main" description: name: flutter_widget_from_html - sha256: f3967a5b42896662efdd420b5adaf8a7d3692b0f44462a07c80e3b4c173b1a02 + sha256: "0dfebf7417df2149de93926520c703db9be0c9017e60dc5cf43cebed37f4d11e" url: "https://pub.dev" source: hosted - version: "0.15.3" + version: "0.16.0" flutter_widget_from_html_core: dependency: transitive description: name: flutter_widget_from_html_core - sha256: b1048fd119a14762e2361bd057da608148a895477846d6149109b2151d2f7abf + sha256: f77ea1aa1ba29a38fcce04483f44f12382f541b9e8c2150df37166c23bbbd30f url: "https://pub.dev" source: hosted - version: "0.15.2" + version: "0.16.0" font_awesome_flutter: dependency: "direct main" description: @@ -766,50 +782,50 @@ packages: dependency: "direct main" description: name: fvp - sha256: "724802f4c3f4d22cd2913356e91208096ba5aaab1db71c9e12b66bef4cf6f799" + sha256: a2b6f305a5e559abc21b1be06ca0ffb5bb6b5b523d6d45eb8e78d53f3b89e9a2 url: "https://pub.dev" source: hosted - version: "0.31.0" + version: "0.32.1" fwfh_cached_network_image: dependency: transitive description: name: fwfh_cached_network_image - sha256: "8e44226801bfba27930673953afce8af44da7e92573be93f60385d9865a089dd" + sha256: "8f4896109ff3e42424ccacf9058ba3afe5d575b58946c8ac646ac85ae882ce23" url: "https://pub.dev" source: hosted - version: "0.14.3" + version: "0.16.0" fwfh_chewie: dependency: transitive description: name: fwfh_chewie - sha256: "37bde9cedfb6dc5546176f7f0c56af1e814966cb33ec58f16c9565ed93ccb704" + sha256: "1ce7c56894db19881a997813b933835dec142878431370c0eb40f1f878396a25" url: "https://pub.dev" source: hosted - version: "0.14.8" + version: "0.16.0" fwfh_just_audio: dependency: transitive description: name: fwfh_just_audio - sha256: "38dc2c55803bd3cef33042c473e0c40b891ad4548078424641a32032f6a1245f" + sha256: "17816168de1fd180fd3d1fd4500e23136630a248a6889b553e2d2067e133c1a6" url: "https://pub.dev" source: hosted - version: "0.15.2" + version: "0.16.0" fwfh_svg: dependency: transitive description: name: fwfh_svg - sha256: "550b1014d12b5528d8bdb6e3b44b58721f3fb1f65d7a852d1623a817008bdfc4" + sha256: "82f3eb378186fe39b3e2e01ed48a1830d34b0b9a237d951077e74ff0d99e2ac3" url: "https://pub.dev" source: hosted - version: "0.8.3" + version: "0.16.0" fwfh_url_launcher: dependency: transitive description: name: fwfh_url_launcher - sha256: b9f5d55a5ae2c2c07243ba33f7ba49ac9544bdb2f4c16d8139df9ccbebe3449c + sha256: "5cf1b1baa16740abaef8eb41a8e16ba430295d5ec20b880e4cb94e2924774f0a" url: "https://pub.dev" source: hosted - version: "0.9.1" + version: "0.16.0" fwfh_webview: dependency: transitive description: @@ -834,22 +850,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + highlight: + dependency: transitive + description: + name: highlight + sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21" + url: "https://pub.dev" + source: hosted + version: "0.7.0" html: dependency: transitive description: name: html - sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" url: "https://pub.dev" source: hosted - version: "0.15.5" + version: "0.15.6" http: dependency: "direct main" description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_client_helper: dependency: transitive description: @@ -902,10 +926,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -974,18 +998,18 @@ packages: dependency: transitive description: name: just_audio_web - sha256: "8c7e779892e180cbc9ffb5a3c52f6e90e1cbbf4a63694cc450972a7edbd2bb6d" + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" url: "https://pub.dev" source: hosted - version: "0.4.15" + version: "0.4.16" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -1006,10 +1030,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.0" local_auth: dependency: "direct main" description: @@ -1022,10 +1046,10 @@ packages: dependency: transitive description: name: local_auth_android - sha256: "0abe4e72f55c785b28900de52a2522c86baba0988838b5dc22241b072ecccd74" + sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e" url: "https://pub.dev" source: hosted - version: "1.0.48" + version: "1.0.49" local_auth_darwin: dependency: transitive description: @@ -1074,6 +1098,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.3.0" + markdown_widget: + dependency: "direct main" + description: + name: markdown_widget + sha256: b52c13d3ee4d0e60c812e15b0593f142a3b8a2003cde1babb271d001a1dbdc1c + url: "https://pub.dev" + source: hosted + version: "2.3.2+8" matcher: dependency: transitive description: @@ -1194,6 +1226,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + overflow_view: + dependency: "direct main" + description: + name: overflow_view + sha256: aa39c40f8229e6dcd243e544d707457ff630bb99063d2fd0be8b31f8da902e27 + url: "https://pub.dev" + source: hosted + version: "0.5.0" package_config: dependency: transitive description: @@ -1254,10 +1294,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -1342,10 +1382,10 @@ packages: dependency: transitive description: name: pointer_interceptor_web - sha256: "7a7087782110f8c1827170660b09f8aa893e0e9a61431dbbe2ac3fc482e8c044" + sha256: "460b600e71de6fcea2b3d5f662c92293c049c4319e27f0829310e5a953b3ee2a" url: "https://pub.dev" source: hosted - version: "0.10.2+1" + version: "0.10.3" pool: dependency: transitive description: @@ -1358,18 +1398,18 @@ packages: dependency: transitive description: name: posix - sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.2" provider: dependency: transitive description: name: provider - sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310" + sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.1.5" pub_semver: dependency: transitive description: @@ -1390,10 +1430,10 @@ packages: dependency: transitive description: name: qs_dart - sha256: c775dbe663cd59365050220b3499dee259b72ad6b352a3e087a15bd77e161b74 + sha256: f8d9f9f75fa6e6e72437995ccb549a27d52ec06236cfad1f4e5eceb755427649 url: "https://pub.dev" source: hosted - version: "1.3.3+1" + version: "1.3.7+1" recase: dependency: transitive description: @@ -1470,10 +1510,10 @@ packages: dependency: "direct main" description: name: screen_brightness - sha256: eca7bd9d2c3c688bcad14855361cab7097839400b6b4a56f62b7ae511c709958 + sha256: "20b43489fbb12316d64633d5abb731f8d3e2c49871f65c8e434c6225d0f58fcf" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" screen_brightness_android: dependency: transitive description: @@ -1486,10 +1526,10 @@ packages: dependency: transitive description: name: screen_brightness_ios - sha256: bfd9bfd0ac852e7aa170e7e356cc27195b2a75037b72c8c6336cf6fb2115cffb + sha256: "2493953340ecfe8f4f13f61db50ce72533a55b0bbd58ba1402893feecf3727f5" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" screen_brightness_macos: dependency: transitive description: @@ -1498,6 +1538,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + screen_brightness_ohos: + dependency: transitive + description: + name: screen_brightness_ohos + sha256: "61e313e46eaee3f83dd4e85a2a91f8a81be02c154bc9e60830a7c0fd76dac286" + url: "https://pub.dev" + source: hosted + version: "2.1.0" screen_brightness_platform_interface: dependency: transitive description: @@ -1515,7 +1563,7 @@ packages: source: hosted version: "2.1.0" screen_retriever: - dependency: transitive + dependency: "direct main" description: name: screen_retriever sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" @@ -1554,30 +1602,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" - scrollable_positioned_list: - dependency: "direct main" + scroll_to_index: + dependency: transitive description: - name: scrollable_positioned_list - sha256: "1b54d5f1329a1e263269abc9e2543d90806131aa14fe7c6062a8054d57249287" + name: scroll_to_index + sha256: b707546e7500d9f070d63e5acf74fd437ec7eeeb68d3412ef7b0afada0b4f176 url: "https://pub.dev" source: hosted - version: "0.3.8" + version: "3.0.1" share_plus: dependency: "direct main" description: name: share_plus - sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + sha256: b2961506569e28948d75ec346c28775bb111986bb69dc6a20754a457e3d97fa0 url: "https://pub.dev" source: hosted - version: "10.1.4" + version: "11.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + sha256: "1032d392bc5d2095a77447a805aa3f804d2ae6a4d5eef5e6ebb3bd94c1bc19ef" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "6.0.0" shared_preferences: dependency: "direct main" description: @@ -1590,10 +1638,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -1907,10 +1955,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "1d0eae19bd7606ef60fe69ef3b312a437a16549476c42321d5dc1506c9ca3bf4" + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.15" + version: "6.3.16" url_launcher_ios: dependency: transitive description: @@ -1947,10 +1995,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" url_launcher_windows: dependency: transitive description: @@ -1967,6 +2015,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.1" + value_layout_builder: + dependency: transitive + description: + name: value_layout_builder + sha256: ab4b7d98bac8cefeb9713154d43ee0477490183f5aa23bb4ffa5103d9bbf6275 + url: "https://pub.dev" + source: hosted + version: "0.5.0" vector_graphics: dependency: transitive description: @@ -1987,10 +2043,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.17" vector_math: dependency: transitive description: @@ -2003,26 +2059,26 @@ packages: dependency: "direct main" description: name: video_player - sha256: "7d78f0cfaddc8c19d4cb2d3bebe1bfef11f2103b0a03e5398b303a1bf65eeb14" + sha256: "0d55b1f1a31e5ad4c4967bfaa8ade0240b07d20ee4af1dfef5f531056512961a" url: "https://pub.dev" source: hosted - version: "2.9.5" + version: "2.10.0" video_player_android: dependency: transitive description: name: video_player_android - sha256: ae7d4f1b41e3ac6d24dd9b9d5d6831b52d74a61bdd90a7a6262a33d8bb97c29a + sha256: "4a5135754a62dbc827a64a42ef1f8ed72c962e191c97e2d48744225c2b9ebb73" url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.8.7" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "84b4752745eeccb6e75865c9aab39b3d28eb27ba5726d352d45db8297fbd75bc" + sha256: "9ee764e5cd2fc1e10911ae8ad588e1a19db3b6aa9a6eb53c127c42d3a3c3f22f" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.1" video_player_platform_interface: dependency: transitive description: @@ -2035,42 +2091,50 @@ packages: dependency: transitive description: name: video_player_web - sha256: "3ef40ea6d72434edbfdba4624b90fd3a80a0740d260667d91e7ecd2d79e13476" + sha256: e8bba2e5d1e159d5048c9a491bb2a7b29c535c612bb7d10c1e21107f5bd365ba url: "https://pub.dev" source: hosted - version: "2.3.4" + version: "2.3.5" + visibility_detector: + dependency: transitive + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" vm_service: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" volume_controller: dependency: transitive description: name: volume_controller - sha256: e82fd689bb8e1fe8e64be3fa5946ff8699058f8cf9f4c1679acdba20cda7f5bd + sha256: d75039e69c0d90e7810bfd47e3eedf29ff8543ea7a10392792e81f9bded7edf5 url: "https://pub.dev" source: hosted - version: "3.3.3" + version: "3.4.0" wakelock_plus: dependency: "direct main" description: name: wakelock_plus - sha256: b90fbcc8d7bdf3b883ea9706d9d76b9978cb1dfa4351fcc8014d6ec31a493354 + sha256: a474e314c3e8fb5adef1f9ae2d247e57467ad557fa7483a2b895bc1b421c5678 url: "https://pub.dev" source: hosted - version: "1.2.11" + version: "1.3.2" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "70e780bc99796e1db82fe764b1e7dcb89a86f1e5b3afb1db354de50f2e41eb7a" + sha256: e10444072e50dbc4999d7316fd303f7ea53d31c824aa5eb05d7ccbdd98985207 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.3" watcher: dependency: transitive description: @@ -2099,66 +2163,66 @@ packages: dependency: transitive description: name: web_socket - sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" url: "https://pub.dev" source: hosted - version: "0.1.6" + version: "1.0.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" webview_flutter: dependency: transitive description: name: webview_flutter - sha256: "889a0a678e7c793c308c68739996227c9661590605e70b1f6cf6b9a6634f7aec" + sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.13.0" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: e09150b28a07933839adef0e4a088bb43e8c8d9e6b93025b01882d4067a58ab0 + sha256: f6e6afef6e234801da77170f7a1847ded8450778caf2fe13979d140484be3678 url: "https://pub.dev" source: hosted - version: "4.3.4" + version: "4.7.0" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + sha256: f0dc2dc3a2b1e3a6abdd6801b9355ebfeb3b8f6cde6b9dc7c9235909c4a1f147 url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.13.1" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: c14455137ce60a68e1ccaf4e8f2dae8cebcb3465ddaa2fcfb57584fb7c5afe4d + sha256: a3d461fe3467014e05f3ac4962e5fdde2a4bf44c561cb53e9ae5c586600fdbc3 url: "https://pub.dev" source: hosted - version: "3.18.5" + version: "3.22.0" win32: dependency: transitive description: name: win32 - sha256: dc6ecaa00a7c708e5b4d10ee7bec8c270e9276dfcab1783f57e9962d7884305f + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" url: "https://pub.dev" source: hosted - version: "5.12.0" + version: "5.13.0" window_manager: dependency: "direct main" description: name: window_manager - sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" + sha256: "51d50168ab267d344b975b15390426b1243600d436770d3f13de67e55b05ec16" url: "https://pub.dev" source: hosted - version: "0.4.3" + version: "0.5.0" xdg_directories: dependency: transitive description: @@ -2192,5 +2256,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0 <4.0.0" - flutter: ">=3.27.0" + dart: ">=3.8.0 <4.0.0" + flutter: ">=3.32.0" diff --git a/pubspec.yaml b/pubspec.yaml index 8b37fdb..7ceb72c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 0.6.2+1 +version: 0.7.0+1 environment: sdk: ">=3.1.3 <4.0.0" @@ -40,11 +40,11 @@ dependencies: iconsax_plus: ^1.0.0 # Network and HTTP - chopper: ^8.0.4 + chopper: ^8.1.0 cached_network_image: ^3.4.1 - http: ^1.3.0 + http: ^1.4.0 flutter_cache_manager: ^3.4.1 - connectivity_plus: ^6.1.3 + connectivity_plus: ^6.1.4 # State Management flutter_riverpod: ^2.6.1 @@ -58,71 +58,74 @@ dependencies: logging: ^1.3.0 # Internationalization - intl: ^0.19.0 + intl: ^0.20.2 # Local Storage - shared_preferences: ^2.5.1 + shared_preferences: ^2.5.3 path_provider: ^2.1.5 # Media media_kit: ^1.2.0 # Primary package. - media_kit_video: ^1.2.5 # For video rendering. - media_kit_libs_video: ^1.0.5 # Native video dependencies. - audio_service: ^0.18.16 - fvp: ^0.31.0 - video_player: ^2.9.2 + media_kit_video: ^1.3.0 # For video rendering. + media_kit_libs_video: ^1.0.6 # Native video dependencies. + audio_service: ^0.18.18 + fvp: ^0.32.1 + video_player: ^2.10.0 # UI Components dynamic_color: ^1.7.0 - flutter_svg: ^2.0.17 + flutter_svg: ^2.1.0 animations: ^2.0.11 automatic_animated_list: ^1.1.0 page_transition: ^2.2.1 sticky_headers: ^0.3.0+2 flutter_staggered_grid_view: ^0.7.0 - scrollable_positioned_list: ^0.3.8 sliver_tools: ^0.2.12 - square_progress_indicator: ^0.0.7 - flutter_blurhash: ^0.8.2 - extended_image: ^9.1.0 - flutter_widget_from_html: ^0.15.3 + square_progress_indicator: ^0.0.8 + flutter_blurhash: ^0.9.1 + extended_image: ^10.0.1 + flutter_widget_from_html: ^0.16.0 font_awesome_flutter: ^10.8.0 + reorderable_grid: ^1.0.10 + overflow_view: ^0.5.0 + flutter_sticky_header: ^0.8.0 + markdown_widget: ^2.3.2+8 # Navigation auto_route: ^9.3.0+1 url_launcher: ^6.3.1 - flutter_custom_tabs: ^2.1.0 + flutter_custom_tabs: ^2.4.0 # Utility - path: ^1.9.0 - file_picker: ^8.3.1 + path: ^1.9.1 + file_picker: ^10.1.9 transparent_image: ^2.0.1 universal_html: ^2.2.4 - collection: ^1.19.0 + collection: ^1.19.1 # Device and System local_auth: ^2.3.0 - package_info_plus: ^8.1.4 - wakelock_plus: ^1.2.10 - screen_brightness: ^2.1.1 - window_manager: ^0.4.3 + package_info_plus: ^8.3.0 + wakelock_plus: ^1.3.2 + screen_brightness: ^2.1.4 + window_manager: ^0.5.0 smtc_windows: ^1.0.0 - background_downloader: ^8.9.4 + background_downloader: ^9.2.2 + screen_retriever: ^0.2.0 # Data isar: ^4.0.0-dev.14 isar_flutter_libs: ^4.0.0-dev.14 # contains Isar Core # Other - async: ^2.11.0 + async: ^2.13.0 xid: ^1.2.1 - desktop_drop: ^0.5.0 + desktop_drop: ^0.6.0 flexible_scrollbar: ^0.1.3 flutter_typeahead: ^5.2.0 - share_plus: ^10.1.4 - archive: ^4.0.2 - dart_mappable: ^4.3.0 - reorderable_grid: ^1.0.10 + share_plus: ^11.0.0 + archive: ^4.0.7 + dart_mappable: ^4.5.0 dev_dependencies: flutter_test: @@ -133,17 +136,17 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^5.0.0 - build_runner: ^2.4.14 - chopper_generator: ^8.0.4 + flutter_lints: ^6.0.0 + build_runner: ^2.4.15 + chopper_generator: ^8.1.0 json_serializable: ^6.9.0 custom_lint: ^0.7.0 freezed: ^2.5.7 swagger_dart_code_generator: ^3.0.1 riverpod_generator: ^2.6.3 - dart_mappable_builder: ^4.3.0 + dart_mappable_builder: ^4.3.1+1 auto_route_generator: ^9.0.0 - icons_launcher: ^3.0.0 + icons_launcher: ^3.0.1 flutter: # The following line ensures that the Material Icons font is diff --git a/scripts/create_dmg.sh b/scripts/create_dmg.sh new file mode 100755 index 0000000..e664d38 --- /dev/null +++ b/scripts/create_dmg.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +# Script to create DMG for Fladder macOS app using create-dmg +# Usage: ./create_dmg.sh + +set -e + +# Configuration +APP_NAME="Fladder" +APP_PATH="build/macos/Build/Products/Release-production/Fladder.app" +DMG_PATH="build/macos/Build/Products/Release-production/macOS.dmg" +BACKGROUND_IMAGE="assets/macos-dmg/Fladder-DMG-Background.jpg" +TEMP_DMG_DIR="dmg_temp" + +# Check if app exists +if [ ! -d "$APP_PATH" ]; then + echo "Error: App not found at $APP_PATH" + echo "Please build the app first with: flutter build macos --flavor production" + exit 1 +fi + +# Check if background image exists +if [ ! -f "$BACKGROUND_IMAGE" ]; then + echo "Error: Background image not found at $BACKGROUND_IMAGE" + exit 1 +fi + +# Clean up any existing artifacts +rm -rf "$TEMP_DMG_DIR" +rm -f "$DMG_PATH" + +echo "Creating DMG for $APP_NAME with custom background..." + +# Create temporary directory structure for DMG +mkdir -p "$TEMP_DMG_DIR" +cp -R "$APP_PATH" "$TEMP_DMG_DIR/" + +# Create DMG with create-dmg using enhanced settings +create-dmg \ + --volname "$APP_NAME" \ + --volicon "$APP_PATH/Contents/Resources/AppIcon.icns" \ + --background "$BACKGROUND_IMAGE" \ + --window-pos 200 120 \ + --window-size 800 500 \ + --icon-size 80 \ + --icon "$APP_NAME.app" 210 250 \ + --hide-extension "$APP_NAME.app" \ + --app-drop-link 603 250 \ + --format UDZO \ + --hdiutil-quiet \ + "$DMG_PATH" \ + "$TEMP_DMG_DIR" + +# Clean up temp directory +rm -rf "$TEMP_DMG_DIR" + +echo "DMG created successfully at: $DMG_PATH" + +# Verify the DMG was created +if [ -f "$DMG_PATH" ]; then + echo "DMG file size: $(du -h "$DMG_PATH" | cut -f1)" + echo "You can test the DMG by opening: $DMG_PATH" +else + echo "ERROR: DMG creation failed!" + exit 1 +fi diff --git a/windows/windows_setup.iss b/windows/windows_setup.iss index 950bcd2..17e0308 100644 --- a/windows/windows_setup.iss +++ b/windows/windows_setup.iss @@ -1,3 +1,5 @@ +#define SourcePath ".." + [Setup] AppId={{D573EDD5-117A-47AD-88AC-62C8EBD11DC7} AppName="Fladder" @@ -21,9 +23,9 @@ SetupLogging=yes UninstallLogging=yes UninstallDisplayName="Fladder" UninstallDisplayIcon={app}\fladder.exe -SetupIconFile="D:\a\Fladder\Fladder\icons\production\fladder_icon.ico" -LicenseFile="D:\a\Fladder\Fladder\LICENSE" -WizardImageFile=D:\a\Fladder\Fladder\assets\windows-installer\fladder-installer-100.bmp,D:\a\Fladder\Fladder\assets\windows-installer\fladder-installer-125.bmp,D:\a\Fladder\Fladder\assets\windows-installer\fladder-installer-150.bmp +SetupIconFile="{#SourcePath}\icons\production\fladder_icon.ico" +LicenseFile="{#SourcePath}\LICENSE" +WizardImageFile={#SourcePath}\assets\windows-installer\fladder-installer-100.bmp,{#SourcePath}\assets\windows-installer\fladder-installer-125.bmp,{#SourcePath}\assets\windows-installer\fladder-installer-150.bmp [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" @@ -32,7 +34,7 @@ Name: "english"; MessagesFile: "compiler:Default.isl" Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked [Files] -Source: "D:\a\Fladder\Fladder\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#SourcePath}\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: "{autoprograms}\Fladder"; Filename: "{app}\fladder.exe" @@ -56,4 +58,4 @@ begin end; end; end; -end; \ No newline at end of file +end;