diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..45c2124 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.38.6" +} \ No newline at end of file diff --git a/.github/actions/flutter-setup/action.yaml b/.github/actions/flutter-setup/action.yaml new file mode 100644 index 0000000..6534571 --- /dev/null +++ b/.github/actions/flutter-setup/action.yaml @@ -0,0 +1,46 @@ +# .github/actions/flutter-setup/action.yml +name: "Flutter Setup Composite Action" +description: "Checks out code, sets up Java/Flutter, caches, and runs pub get" + +# Define inputs for customization (optional, but good practice) +inputs: + flutter-channel: + description: "Flutter channel to use (stable, beta, dev, master)" + required: false + default: "stable" + java-version: + description: "Java version to set up" + required: false + default: "17" + +runs: + using: "composite" # Specify this is a composite action + steps: + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: ${{ inputs.java-version }} + + - name: Set up Flutter SDK + uses: subosito/flutter-action@v2 + with: + channel: ${{ inputs.flutter-channel }} + flutter-version-file: pubspec.yaml + cache: true # Cache Flutter SDK itself + + - name: Cache Flutter dependencies + id: cache-pub + uses: actions/cache@v4 + with: + path: ${{ env.FLUTTER_HOME }}/.pub-cache + key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }} + restore-keys: | + ${{ runner.os }}-flutter-pub- + + - name: Get Flutter dependencies + run: flutter pub get + # Use shell: bash for potential cross-platform compatibility in complex commands + shell: bash + + # Add other common setup steps if needed diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 1979a56..3e3ee30 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,13 +1,14 @@ -name-template: "v$RESOLVED_VERSION 🌈" +name-template: "v$RESOLVED_VERSION" tag-template: "v$RESOLVED_VERSION" -# name-template: 'v$NEXT_PATCH_VERSION' -# tag-template: 'v$NEXT_PATCH_VERSION' categories: - title: "🚀 Features" labels: - "feature" - "enhancement" + - title: "📱 UI Changes" + labels: + - "ui" - title: "🐛 Bug Fixes" labels: - "fix" @@ -15,7 +16,10 @@ categories: - "bug" - title: "🧰 Maintenance" label: "chore" -change-template: "- $TITLE @$AUTHOR (#$NUMBER)" + - title: "💥 Breaking Changes" + labels: + - "breaking" +change-template: "- $TITLE (#$NUMBER)" change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. version-resolver: major: @@ -33,10 +37,10 @@ template: | $CHANGES - **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...$NEW_TAG + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION exclude-labels: - - "skip-changelog" + - "skip changelog" exclude-contributors: - "Dr-Blank" @@ -51,6 +55,15 @@ autolabeler: branch: - '/feature\/.+/' title: - - "/feat(ure)?/i" + - "/^feat(ure)?/i" body: - "/JIRA-[0-9]{1,4}/" + - label: "chore" + title: + - "/^chore\b/i" + - label: "ui" + title: + - "/^ui\b/i" + - label: "refactor" + title: + - "/^refactor/i" diff --git a/.github/workflows/flutter-ci.yaml b/.github/workflows/flutter-ci.yaml new file mode 100644 index 0000000..c503b65 --- /dev/null +++ b/.github/workflows/flutter-ci.yaml @@ -0,0 +1,218 @@ +name: Flutter CI & Release + +on: + push: + branches: [main] + tags: ["v*.*.*"] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup Flutter Environment + uses: ./.github/actions/flutter-setup # Path to the composite action directory + # Pass inputs if needed (optional, using defaults here) + # with: + # flutter-channel: 'stable' + # java-version: '17' + # Debug: Echo current directory contents + - name: List root directory contents + run: | + pwd + ls -la + + # Debug: Recursive directory structure + - name: Show full directory structure + run: | + echo "Full directory structure:" + tree -L 3 + + # Debug: Submodule status and details + - name: Check submodule status + run: | + echo "Submodule status:" + git submodule status + + echo "\nSubmodule details:" + git submodule foreach 'echo $path: && pwd && ls -la' + + # - name: Run static analysis + # run: flutter analyze + + - name: Check formatting + run: | + dart format -o none --set-exit-if-changed lib/ + + - name: Run tests + run: flutter test + + build_android: + name: Build Android APKs + needs: test + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Setup Flutter Environment + uses: ./.github/actions/flutter-setup # Path to the composite action directory + with: + flutter-channel: stable + java-version: 17 + + - name: Accept Android SDK Licenses + run: | + yes | sudo $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses + + - name: Decode android/upload.jks + run: echo "${{ secrets.UPLOAD_KEYSTORE_JKS }}" | base64 --decode > android/upload.jks + + - name: Decode android/key.properties + run: echo "${{ secrets.KEY_PROPERTIES }}" | base64 --decode > android/key.properties + + - name: Build APKs + run: flutter build apk --release --split-per-abi + + - name: Build Universal APK + run: flutter build apk --release + + - name: Rename Universal APK + run: mv build/app/outputs/flutter-apk/{app-release,app-universal-release}.apk + + - name: Build App Bundle + run: flutter build appbundle --release + + - name: Upload Android APK Artifact + uses: actions/upload-artifact@v4 + with: + name: android-release-artifacts + path: | + build/app/outputs/flutter-apk/*-release*.apk + build/app/outputs/bundle/release/*.aab + + build_linux: + needs: test + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + - name: Setup Flutter Environment + uses: ./.github/actions/flutter-setup # Path to the composite action directory + + - name: Install Linux dependencies + run: | + sudo apt-get update -y + sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev locate libfuse2 + # Download and install appimagetool + wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage + chmod +x appimagetool-x86_64.AppImage + sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool + shell: bash + - name: setup fastforge + run: | + dart pub global activate fastforge + - name: Build Linux AppImage and deb + run: fastforge package --platform linux --targets deb,appimage + + - name: Rename Linux Artifacts + run: | + # Find and rename .deb file + DEB_FILE=$(find dist/ -name "*.deb" -type f) + if [ -n "$DEB_FILE" ]; then + mv "$DEB_FILE" dist/vaani-linux-amd64.deb + echo "Renamed DEB: $DEB_FILE to dist/vaani-linux-amd64.deb" + else + echo "Error: .deb file not found in dist/" + exit 1 + fi + + # Find and rename .AppImage file + APPIMAGE_FILE=$(find dist/ -name "*.AppImage" -type f) + if [ -n "$APPIMAGE_FILE" ]; then + mv "$APPIMAGE_FILE" dist/vaani-linux-amd64.AppImage + echo "Renamed AppImage: $APPIMAGE_FILE to dist/vaani-linux-amd64.AppImage" + else + echo "Error: .AppImage file not found in dist/" + exit 1 + fi + shell: bash + + - name: Upload Linux Artifacts + uses: actions/upload-artifact@v4 + with: + name: linux-release-artifacts + path: | + dist/vaani-linux-amd64.deb + dist/vaani-linux-amd64.AppImage + + # Job 4: Create GitHub Release (NEW - runs only on tag pushes) + create_release: + name: Create GitHub Release + needs: [build_android, build_linux] # Depends on successful builds + runs-on: ubuntu-latest + permissions: + contents: write # Need write access to create release + # <<< CONDITION: Only run this job if the trigger was a tag starting with 'v' + if: startsWith(github.ref, 'refs/tags/v') + + steps: + # No checkout needed if only downloading artifacts and using context variables + # - name: Checkout repository + # uses: actions/checkout@v4 + + # Download artifacts created earlier IN THIS SAME WORKFLOW RUN + - name: Download Android Artifacts + uses: actions/download-artifact@v4 + with: + name: android-release-artifacts + path: ./release-artifacts/android + + - name: Download Linux Artifacts + uses: actions/download-artifact@v4 + with: + name: linux-release-artifacts + path: ./release-artifacts/linux + + - name: List downloaded files (for debugging) + run: ls -R ./release-artifacts + shell: bash + + # Extract version info from the tag + - name: Extract Version from Tag + id: version + run: | + TAG_NAME=${GITHUB_REF#refs/tags/} + VERSION=${TAG_NAME#v} + echo "tag=${TAG_NAME}" >> $GITHUB_OUTPUT + echo "version=${VERSION}" >> $GITHUB_OUTPUT + shell: bash + + # Generate release notes (optional, consider its configuration for tags) + - name: Generate Release Notes + id: generate_release_notes + uses: release-drafter/release-drafter@v6 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # Create the GitHub Release using downloaded artifacts + - name: Create GitHub Release + uses: ncipollo/release-action@v1 + with: + artifacts: "./release-artifacts/**/*" # Use downloaded artifacts + name: Release v${{ steps.version.outputs.version }} + tag: ${{ github.ref }} + body: ${{ steps.generate_release_notes.outputs.body }} + # token: ${{ secrets.GITHUB_TOKEN }} # Usually inferred diff --git a/.github/workflows/flutter_release.yaml b/.github/workflows/flutter_release.yaml deleted file mode 100644 index a8fde31..0000000 --- a/.github/workflows/flutter_release.yaml +++ /dev/null @@ -1,75 +0,0 @@ -name: Flutter Release Workflow - -on: - push: - tags: - - "v**" - # manually trigger a release if needed - workflow_dispatch: - -jobs: - build: - permissions: - # write permission is required to create a github release - contents: write - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - name: Checkout shelfsdk - uses: actions/checkout@v3 - with: - repository: Dr-Blank/shelfsdk - path: ./shelfsdk - - - name: Set Up Java - uses: actions/setup-java@v3.12.0 - with: - distribution: "oracle" - java-version: "17" - - - name: Set up Flutter - uses: subosito/flutter-action@v2 - with: - channel: "stable" - - - name: Install dependencies - run: flutter pub get - - # - name: Run tests - # run: flutter test - - - name: Build APK - run: flutter build apk --release - - - name: Upload APK - uses: actions/upload-artifact@v4 - with: - name: release-apk - path: build/app/outputs/flutter-apk/app-release.apk - - - name: version - id: version - run: | - tag=${GITHUB_REF/refs\/tags\//} - version=${tag#v} - major=${version%%.*} - echo "tag=${tag}" >> $GITHUB_OUTPUT - echo "version=${version}" >> $GITHUB_OUTPUT - echo "major=${major}" >> $GITHUB_OUTPUT - - - name: Generate Release Notes - id: generate_release_notes - uses: release-drafter/release-drafter@v5 - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: Create GitHub Release - uses: ncipollo/release-action@v1 - with: - artifacts: build/app/outputs/flutter-apk/app-release.apk - name: Release ${{ steps.version.outputs.version }} - tag: ${{ github.ref }} - body: ${{ steps.generate_release_notes.outputs.body }} diff --git a/.github/workflows/prepare-release.yaml b/.github/workflows/prepare-release.yaml new file mode 100644 index 0000000..ceb9105 --- /dev/null +++ b/.github/workflows/prepare-release.yaml @@ -0,0 +1,130 @@ +# .github/workflows/prepare-release.yml +name: Prepare Release (using Cider) + +on: + workflow_dispatch: + inputs: + bump_type: + description: "Type of version bump (patch, minor, major)" + required: true + type: choice + options: + - patch + - minor + - major + default: "patch" + +permissions: + contents: write # NEEDED to commit, push, and tag + +jobs: + bump_version_and_tag: + name: Bump Version and Tag using Cider + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # Use a PAT if pushing to protected branches is restricted for GITHUB_TOKEN + token: ${{ secrets.PAT_TOKEN }} # Create PAT with repo scope + # token: ${{ secrets.GITHUB_TOKEN }} # this does not trigger other workflows + + # Setup Flutter/Dart environment needed to run dart pub global activate + - name: Setup Flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" # Or match your project's channel + flutter-version-file: pubspec.yaml + + - name: Install Cider + run: dart pub global activate cider + shell: bash + + # Add pub global bin to PATH for this job + - name: Add pub global bin to PATH + run: echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH + shell: bash + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + shell: bash + + - name: Bump version using Cider + id: bump + run: | + echo "Current version:" + grep '^version:' pubspec.yaml + + # Run cider to bump version and build number + # Cider modifies pubspec.yaml in place + cider bump ${{ github.event.inputs.bump_type }} --bump-build + + echo "New version (after cider bump):" + # Read the *new* version directly from the modified file + new_version_line=$(grep '^version:' pubspec.yaml) + # Extract just the version string (e.g., 1.2.3+4) + new_version=$(echo "$new_version_line" | sed 's/version: *//') + + echo "$new_version_line" + echo "Extracted new version: $new_version" + + if [[ -z "$new_version" ]]; then + echo "Error: Could not extract new version after cider bump." + exit 1 + fi + + # Create tag name (e.g., v1.2.3 - usually tags don't include build number) + # Extract version part before '+' for the tag + version_for_tag=$(echo "$new_version" | cut -d'+' -f1) + new_tag="v$version_for_tag" + echo "New tag: $new_tag" + + # Set outputs for later steps + echo "new_version=$new_version" >> $GITHUB_OUTPUT + echo "new_tag=$new_tag" >> $GITHUB_OUTPUT + shell: bash + + - name: Commit version bump + run: | + # Add pubspec.yaml. Add CHANGELOG.md if cider modifies it and you want to commit it. + git add pubspec.yaml + # git add CHANGELOG.md # Uncomment if needed + + # Check if there are changes to commit + if git diff --staged --quiet; then + echo "No changes detected in pubspec.yaml (or CHANGELOG.md) to commit." + else + # Use the version *without* build number for the commit message usually + git commit -m "chore(release): bump version to ${{ steps.bump.outputs.new_tag }}" + fi + shell: bash + + - name: Create Git tag + # Only run if the commit step actually committed something (check git status) + # or simply run always, it won't hurt if the commit was skipped + run: | + git tag ${{ steps.bump.outputs.new_tag }} + shell: bash + + - name: Push changes and tag + run: | + # Push the commit first (e.g., to main branch - adjust if needed) + # Handle potential conflicts if main changed since checkout? (More advanced setup) + # Check if there are commits to push before pushing branch + if ! git diff --quiet HEAD^ HEAD; then + echo "Pushing commit to main..." + git push origin HEAD:main + else + echo "No new commits to push to main." + fi + + # Always push the tag + echo "Pushing tag ${{ steps.bump.outputs.new_tag }}..." + git push origin ${{ steps.bump.outputs.new_tag }} + shell: bash + + - name: Output New Tag + run: echo "Successfully tagged release ${{ steps.bump.outputs.new_tag }}" diff --git a/.gitignore b/.gitignore index c137f7a..3eee232 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ migrate_working_dir/ .pub-cache/ .pub/ /build/ +dist/ # Symbolication related app.*.symbols @@ -41,6 +42,10 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release +/android/app/.cxx/ -# separate git repo for api sdk -/shelfsdk \ No newline at end of file +# secret keys +/secrets + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..557497e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "shelfsdk"] + path = shelfsdk + url = https://github.com/Dr-Blank/shelfsdk diff --git a/.vscode/launch.json b/.vscode/launch.json index 18ecea1..3d5d2ff 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,6 +7,7 @@ { "name": "vaani", "request": "launch", + "program": "lib/main.dart", "type": "dart" }, { diff --git a/.vscode/settings.json b/.vscode/settings.json index ffa9f03..fd30314 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,27 +1,35 @@ { - "workbench.colorCustomizations": { - "activityBar.background": "#5A1021", - "titleBar.activeBackground": "#7E162E", - "titleBar.activeForeground": "#FEFBFC" - }, - "files.exclude": { - "**/*.freezed.dart": true, - "**/*.g.dart": true - }, + "cmake.configureOnOpen": false, "cSpell.words": [ "audioplayers", "autolabeler", "Autovalidate", + "Checkmark", + "Debounceable", "deeplinking", "fullscreen", "Lerp", "miniplayer", "mocktail", + "nodename", + "numberpicker", "riverpod", + "Schyler", "shelfsdk", + "sysname", "tapable", "unfocus", + "utsname", "Vaani" ], - "cmake.configureOnOpen": false + "dart.flutterSdkPath": ".fvm/versions/3.38.6", + "files.exclude": { + "**/*.freezed.dart": true, + "**/*.g.dart": true + }, + "workbench.colorCustomizations": { + "activityBar.background": "#5A1021", + "titleBar.activeBackground": "#7E162E", + "titleBar.activeForeground": "#FEFBFC" + } } \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..606e94c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,181 @@ +# Contributing to Vaani + +## Welcome Contributors! 🚀 + +We appreciate your interest in contributing to Vaani. This guide will help you navigate the contribution process effectively. + +## How to Contribute + +### Reporting Bugs 🐞 + +1. **Check Existing Issues**: + - Search through the [GitHub Issues](https://github.com/Dr-Blank/Vaani/issues) + +2. **Create a Detailed Bug Report**: + - Provide: + * Exact steps to reproduce + * Relevant error logs or screenshots + +### Submodule Contribution Workflow 🧩 + +#### Understanding Vaani's Submodule Structure + +Vaani uses Git submodules to manage interconnected components. This means each submodule is a separate Git repository nested within the main project. + +#### Working with Submodules + +1. **Identifying Submodules**: + - List all submodules in the project + ```bash + git submodule status + ``` + +2. **Initializing Submodules**: + ```bash + # Ensure all submodules are initialized + git submodule update --init --recursive + ``` + +3. **Contributing to a Specific Submodule**: + + a. **Navigate to Submodule Directory**: + ```bash + cd path/to/submodule + ``` + + b. **Create a Separate Branch**: + ```bash + git checkout -b feature/your-submodule-feature + ``` + + c. **Make and Commit Changes**: + ```bash + # Stage changes + git add . + + # Commit with descriptive message + git commit -m "feat(submodule): describe specific change" + ``` + + d. **Push Submodule Changes**: + ```bash + git push origin feature/your-submodule-feature + ``` + +4. **Updating Submodule References**: + After making changes to a submodule: + ```bash + # From the main repository root + git add path/to/submodule + git commit -m "Update submodule reference to latest changes" + ``` + +5. **Pulling Latest Submodule Changes**: + ```bash + # Update all submodules + git submodule update --recursive --remote + + # Or update a specific submodule + git submodule update --remote path/to/specific/submodule + ``` + +#### Submodule Contribution Best Practices + +- Always work in a feature branch within the submodule +- Ensure submodule changes do not break the main application +- Write tests for submodule-specific changes +- Update documentation if the submodule's interface changes +- Create a pull request for the submodule first, then update the main project's submodule reference + +### Development Workflow + +#### Setting Up the Development Environment + +1. **Prerequisites**: + - [Git](https://git-scm.com/) + - [Flutter SDK](https://flutter.dev/) + - Recommended IDE: [VS Code](https://code.visualstudio.com/) + +2. **Repository Setup**: + + 1. [Fork the repo](https://github.com/Dr-Blank/Vaani/fork) + 1. Clone the forked repository to your local machine + ```bash + # Fork the main repository on GitHub + git clone --recursive https://github.com/[YOUR_USERNAME]/Vaani.git + cd Vaani + + # Initialize and update submodules + git submodule update --init --recursive + + # Install dependencies for the main app and submodules + flutter pub get + ``` + +#### Coding Standards + +1. **Code Style**: + - Follow [Flutter's style guide](https://dart.dev/guides/language/effective-dart/style) + - Use `dart format` and `flutter analyze` + + ```bash + dart format . + flutter analyze + ``` + +2. **Testing**: + - Write unit and widget tests + - Ensure tests pass for both the main app and submodules + + ```bash + flutter test + ``` + +### Pull Request Process + +1. **Branch Naming**: + - Use descriptive branch names + - Prefix with feature/, bugfix/, or docs/ + + ```bash + git checkout -b feature/add-accessibility-support + ``` + +2. **Commit Messages**: + - Use clear, concise descriptions + - Reference issue numbers when applicable + - Follow conventional commits format: + `(scope): ` + +3. **Pull Request Guidelines**: + - Clearly describe the purpose of your changes + - Include screenshots for visual changes + - Specify if changes affect specific submodules + - Ensure all CI checks pass + +### Signing the app + +once the keystore is created, you can sign the app with the keystore. + +but for github action you need to make a base64 encoded string of the keystore. + +```bash +# convert keystore to base64 +cat android/key.properties | base64 > key.base64 + +# convert keystore to base64 +cat android/upload.jks | base64 > keystore.base64 +``` + +## Communication + +* [Open an Issue](https://github.com/Dr-Blank/Vaani/issues) +* [Discussion Forum](https://github.com/Dr-Blank/Vaani/discussions) + +## Code of Conduct + +* Be respectful and inclusive +* Constructive feedback is welcome +* Collaborate and support fellow contributors + +Happy Contributing! 🌟 \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..1db5478 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,221 @@ +GEM + remote: https://rubygems.org/ + specs: + CFPropertyList (3.0.7) + base64 + nkf + rexml + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) + artifactory (3.0.17) + atomos (0.1.3) + aws-eventstream (1.3.0) + aws-partitions (1.1018.0) + aws-sdk-core (3.214.0) + aws-eventstream (~> 1, >= 1.3.0) + aws-partitions (~> 1, >= 1.992.0) + aws-sigv4 (~> 1.9) + jmespath (~> 1, >= 1.6.1) + aws-sdk-kms (1.96.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sigv4 (~> 1.5) + aws-sdk-s3 (1.176.0) + aws-sdk-core (~> 3, >= 3.210.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.5) + aws-sigv4 (1.10.1) + aws-eventstream (~> 1, >= 1.0.2) + babosa (1.0.4) + base64 (0.2.0) + claide (1.1.0) + colored (1.2) + colored2 (3.1.2) + commander (4.6.0) + highline (~> 2.0.0) + declarative (0.0.20) + digest-crc (0.6.5) + rake (>= 12.0.0, < 14.0.0) + domain_name (0.6.20240107) + dotenv (2.8.1) + emoji_regex (3.2.3) + excon (0.112.0) + faraday (1.10.4) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) + http-cookie (~> 1.0.0) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.4) + multipart-post (~> 2) + faraday-net_http (1.0.2) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + faraday_middleware (1.2.1) + faraday (~> 1.0) + fastimage (2.3.1) + fastlane (2.225.0) + CFPropertyList (>= 2.3, < 4.0.0) + addressable (>= 2.8, < 3.0.0) + artifactory (~> 3.0) + aws-sdk-s3 (~> 1.0) + babosa (>= 1.0.3, < 2.0.0) + bundler (>= 1.12.0, < 3.0.0) + colored (~> 1.2) + commander (~> 4.6) + dotenv (>= 2.1.1, < 3.0.0) + emoji_regex (>= 0.1, < 4.0) + excon (>= 0.71.0, < 1.0.0) + faraday (~> 1.0) + faraday-cookie_jar (~> 0.0.6) + faraday_middleware (~> 1.0) + fastimage (>= 2.1.0, < 3.0.0) + fastlane-sirp (>= 1.0.0) + gh_inspector (>= 1.1.2, < 2.0.0) + google-apis-androidpublisher_v3 (~> 0.3) + google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) + google-cloud-storage (~> 1.31) + highline (~> 2.0) + http-cookie (~> 1.0.5) + json (< 3.0.0) + jwt (>= 2.1.0, < 3) + mini_magick (>= 4.9.4, < 5.0.0) + multipart-post (>= 2.0.0, < 3.0.0) + naturally (~> 2.2) + optparse (>= 0.1.1, < 1.0.0) + plist (>= 3.1.0, < 4.0.0) + rubyzip (>= 2.0.0, < 3.0.0) + security (= 0.1.5) + simctl (~> 1.6.3) + terminal-notifier (>= 2.0.0, < 3.0.0) + terminal-table (~> 3) + tty-screen (>= 0.6.3, < 1.0.0) + tty-spinner (>= 0.8.0, < 1.0.0) + word_wrap (~> 1.0.0) + xcodeproj (>= 1.13.0, < 2.0.0) + xcpretty (~> 0.3.0) + xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) + fastlane-sirp (1.0.0) + sysrandom (~> 1.0) + gh_inspector (1.1.3) + google-apis-androidpublisher_v3 (0.54.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-core (0.11.3) + addressable (~> 2.5, >= 2.5.1) + googleauth (>= 0.16.2, < 2.a) + httpclient (>= 2.8.1, < 3.a) + mini_mime (~> 1.0) + representable (~> 3.0) + retriable (>= 2.0, < 4.a) + rexml + google-apis-iamcredentials_v1 (0.17.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-playcustomapp_v1 (0.13.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.1) + google-cloud-env (>= 1.0, < 3.a) + google-cloud-errors (~> 1.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) + addressable (~> 2.8) + digest-crc (~> 0.4) + google-apis-iamcredentials_v1 (~> 0.1) + google-apis-storage_v1 (~> 0.31.0) + google-cloud-core (~> 1.6) + googleauth (>= 0.16.2, < 2.a) + mini_mime (~> 1.0) + googleauth (1.8.1) + faraday (>= 0.17.3, < 3.a) + jwt (>= 1.4, < 3.0) + multi_json (~> 1.11) + os (>= 0.9, < 2.0) + signet (>= 0.16, < 2.a) + highline (2.0.3) + http-cookie (1.0.8) + domain_name (~> 0.5) + httpclient (2.8.3) + jmespath (1.6.2) + json (2.9.0) + jwt (2.9.3) + base64 + mini_magick (4.13.2) + mini_mime (1.1.5) + multi_json (1.15.0) + multipart-post (2.4.1) + nanaimo (0.4.0) + naturally (2.2.1) + nkf (0.2.0) + optparse (0.6.0) + os (1.1.4) + plist (3.7.1) + public_suffix (6.0.1) + rake (13.2.1) + representable (3.2.0) + declarative (< 0.1.0) + trailblazer-option (>= 0.1.1, < 0.2.0) + uber (< 0.2.0) + retriable (3.1.2) + rexml (3.3.9) + rouge (2.0.7) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + security (0.1.5) + signet (0.19.0) + addressable (~> 2.8) + faraday (>= 0.17.5, < 3.a) + jwt (>= 1.5, < 3.0) + multi_json (~> 1.10) + simctl (1.6.10) + CFPropertyList + naturally + sysrandom (1.0.5) + terminal-notifier (2.0.0) + terminal-table (3.0.2) + unicode-display_width (>= 1.1.1, < 3) + trailblazer-option (0.1.2) + tty-cursor (0.7.1) + tty-screen (0.8.2) + tty-spinner (0.9.3) + tty-cursor (~> 0.7) + uber (0.1.0) + unicode-display_width (2.6.0) + word_wrap (1.0.0) + xcodeproj (1.27.0) + CFPropertyList (>= 2.3.3, < 4.0) + atomos (~> 0.1.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.4.0) + rexml (>= 3.3.6, < 4.0) + xcpretty (0.3.0) + rouge (~> 2.0.7) + xcpretty-travis-formatter (1.0.1) + xcpretty (~> 0.2, >= 0.0.7) + +PLATFORMS + x64-mingw-ucrt + +DEPENDENCIES + fastlane + +BUNDLED WITH + 2.5.23 diff --git a/README.md b/README.md index 0d0310b..14f16fb 100644 --- a/README.md +++ b/README.md @@ -6,17 +6,38 @@ Client for [Audiobookshelf](https://github.com/advplyr/audiobookshelf) server made with Flutter. +## Features + +* Functional Player: Speed Control, Sleep Timer, Shake to Control Player +* Save data with Offline listening and caching +* Material Design +* Extensive Settings to customize the every tiny detail + +## Download + +### Android + -[Get it on GitHub](https://github.com/Dr-Blank/Vaani/releases/latest) [Get it on Obtainium](http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/Dr-Blank/Vaani) +[Get it on Obtainium](http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/Dr-Blank/Vaani) +[Get it on Google Play](https://play.google.com/store/apps/details?id=dr.blank.vaani) +[Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/dr.blank.vaani) +[Get it on GitHub](https://github.com/Dr-Blank/Vaani/releases/latest/download/app-universal-release.apk) + +*Play Store version is paid if you want to support the development.* + +### Linux + +[Download Linux (.deb)](https://github.com/Dr-Blank/Vaani/releases/latest/download/vaani-linux-amd64.deb) +[Download Linux (AppImage)](https://github.com/Dr-Blank/Vaani/releases/latest/download/vaani-linux-amd64.AppImage) ## Screencaps https://github.com/user-attachments/assets/2ac9ace2-4a3c-40fc-adde-55914e4cf62d -|||| -|:---:|:---:|:---:| -|Home|Book View|Player| +| | | | +| :-----------------------------------------------------------: | :---------------------------------------------------------------: | :-------------------------------------------------------------: | +| Home | Book View | Player | Currently, the app is in development and is not ready for production use. -Plan is to have support for android, ios, and desktop. +Plan is to have support for android, and desktop. diff --git a/analysis_options.yaml b/analysis_options.yaml index 2c5752a..1a27822 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -25,6 +25,10 @@ linter: # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule require_trailing_commas: true analyzer: + exclude: + - '**.freezed.dart' + - '**.g.dart' + - '**.gr.dart' errors: invalid_annotation_target: ignore plugins: diff --git a/android/app/build.gradle b/android/app/build.gradle index 0759e63..49b2a91 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -22,10 +22,20 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file('key.properties') +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + android { namespace "dr.blank.vaani" compileSdk flutter.compileSdkVersion ndkVersion flutter.ndkVersion + // The NDK version is set to a specific version since it was not building + // TODO remove when https://github.com/flutter/flutter/issues/139427 is closed + // ndkVersion = "29.0.13113456" + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -40,22 +50,40 @@ android { main.java.srcDirs += 'src/main/kotlin' } + // see: https://gitlab.com/IzzyOnDroid/repo/-/issues/623#note_2149548690 + // https://android.izzysoft.de/articles/named/iod-scan-apkchecks#blobs + dependenciesInfo { + // Disables dependency metadata when building APKs. + includeInApk = false + // Disables dependency metadata when building Android App Bundles. + includeInBundle = false + } + defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "dr.blank.vaani" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion 23 + minSdkVersion flutter.minSdkVersion targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName } + signingConfigs { + release { + keyAlias = keystoreProperties['keyAlias'] + keyPassword = keystoreProperties['keyPassword'] + storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword = keystoreProperties['storePassword'] + } + } + buildTypes { release { - // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + // signingConfig signingConfigs.debug + signingConfig = signingConfigs.release } } } @@ -65,3 +93,11 @@ flutter { } dependencies {} + +// https://stackoverflow.com/questions/78626580/how-to-resolve-app-execution-failure-due-to-androidx-corecore1-15-0-alpha +configurations.all { + resolutionStrategy { + force "androidx.core:core:1.13.1" + force "androidx.core:core-ktx:1.13.1" + } +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 41b686e..3856060 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,9 +6,14 @@ + + + + \ No newline at end of file diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e1ca574..e2847c8 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/android/settings.gradle b/android/settings.gradle index c9fb5ba..4529834 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,8 +19,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false - id "org.jetbrains.kotlin.android" version "2.0.20" apply false + id "com.android.application" version '8.10.0' apply false + id "org.jetbrains.kotlin.android" version "2.1.10" apply false } include ":app" diff --git a/assets/fonts/AbsIcons.ttf b/assets/fonts/AbsIcons.ttf new file mode 100644 index 0000000..6f970dc Binary files /dev/null and b/assets/fonts/AbsIcons.ttf differ diff --git a/assets/images/vaani_logo_foreground.png b/assets/images/vaani_logo_foreground.png new file mode 100644 index 0000000..cb8014f Binary files /dev/null and b/assets/images/vaani_logo_foreground.png differ diff --git a/assets/sounds/beep.mp3 b/assets/sounds/beep.mp3 new file mode 100644 index 0000000..4657727 Binary files /dev/null and b/assets/sounds/beep.mp3 differ diff --git a/distribute_options.yaml b/distribute_options.yaml new file mode 100644 index 0000000..a04cb34 --- /dev/null +++ b/distribute_options.yaml @@ -0,0 +1,8 @@ +output: dist/ +releases: + - name: dev + jobs: + - name: release-dev-linux-deb + package: + platform: linux + target: deb diff --git a/docs/images_and_logos.md b/docs/images_and_logos.md new file mode 100644 index 0000000..55dd562 --- /dev/null +++ b/docs/images_and_logos.md @@ -0,0 +1,8 @@ +this is how i converted my png to svg + +`convert -background White vaani_logo.png vaani_logo.pbm` + + +`potrace -b svg -i vaani_logo.pbm -o vaani_logo.svg` + +`-i` flag was needed so that it took white as the svgs and black as background \ No newline at end of file diff --git a/docs/linux_build_guide.md b/docs/linux_build_guide.md new file mode 100644 index 0000000..a1c729b --- /dev/null +++ b/docs/linux_build_guide.md @@ -0,0 +1,45 @@ +# Linux Build Guide + +## Determining Package Size +To determine the installed size for your Linux package configuration, you can use the following script: + +```bash +#!/bin/bash + +# Build the Linux app +flutter build linux + +# Get size in KB and add 17% buffer for runtime dependencies +SIZE_KB=$(du -sk build/linux/x64/release/bundle | cut -f1) +BUFFER_SIZE_KB=$(($SIZE_KB + ($SIZE_KB * 17 / 100))) + +echo "Actual bundle size: $SIZE_KB KB" +echo "Recommended installed_size (with 17% buffer): $BUFFER_SIZE_KB KB" +``` + +Save this as `get_package_size.sh` in your project root and make it executable: +```bash +chmod +x get_package_size.sh +``` + +### Usage +1. Run the script: + ```bash + ./get_package_size.sh + ``` +2. Use the output value for `installed_size` in your `linux/packaging/deb/make_config.yaml` file: + ```yaml + installed_size: 75700 # Replace with the value from the script + ``` + +### Why add a buffer? +The 17% buffer is added to account for: +- Runtime dependencies +- Future updates +- Potential additional assets +- Prevent installation issues on systems with limited space + +### Notes +- The installed size should be specified in kilobytes (KB) +- Always round up the buffer size to be safe +- Re-run this script after significant changes to your app (new assets, dependencies, etc.) \ No newline at end of file diff --git a/docs/linux_deeplink.md b/docs/linux_deeplink.md new file mode 100644 index 0000000..d6e5880 --- /dev/null +++ b/docs/linux_deeplink.md @@ -0,0 +1,2 @@ +to test deeplink +`xdg-open vaani://test?code=123&state=abc` \ No newline at end of file diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..28f043a --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,2 @@ +json_key_file("./secrets/play-store-credentials.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one +package_name("dr.blank.vaani") # e.g. com.krausefx.app diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..ce200d8 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,38 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:android) + +platform :android do + desc "Runs all the tests" + lane :test do + gradle(task: "test", project_dir: 'android/') + end + + desc "Submit a new Beta Build to Crashlytics Beta" + lane :beta do + gradle(task: "clean assembleRelease", project_dir: 'android/') + crashlytics + + # sh "your_script.sh" + # You can also use other beta testing services here + end + + desc "Deploy a new version to the Google Play" + lane :deploy do + gradle(task: "clean assembleRelease", project_dir: 'android/') + upload_to_play_store + end +end diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..7ec1207 --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,48 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## Android + +### android test + +```sh +[bundle exec] fastlane android test +``` + +Runs all the tests + +### android beta + +```sh +[bundle exec] fastlane android beta +``` + +Submit a new Beta Build to Crashlytics Beta + +### android deploy + +```sh +[bundle exec] fastlane android deploy +``` + +Deploy a new version to the Google Play + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 0000000..26dcd16 --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,10 @@ +Vaani is a client for your (self-hosted) Audiobookshelf server. + +Features: + +- Functional Player: Speed Control, Sleep Timer, Shake to Control Player +- Save data with Offline listening and caching +- Material Design +- Extensive Settings to customize the every tiny detail + +Note: you need an Audiobookshelf server setup for this app to work. Please see https://www.audiobookshelf.org/ on how to setup one if not already. \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png new file mode 100644 index 0000000..ed9c09d Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000..b260956 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.jpeg b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.jpeg new file mode 100644 index 0000000..a84ddb6 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.jpeg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.jpeg b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.jpeg new file mode 100644 index 0000000..7b6c86b Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.jpeg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.jpeg b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.jpeg new file mode 100644 index 0000000..3c1dbeb Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.jpeg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.jpeg b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.jpeg new file mode 100644 index 0000000..afce5bb Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.jpeg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.jpeg b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.jpeg new file mode 100644 index 0000000..4a51186 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.jpeg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.jpeg b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.jpeg new file mode 100644 index 0000000..e45f00a Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.jpeg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.jpeg b/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.jpeg new file mode 100644 index 0000000..3e625ed Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.jpeg differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.jpeg b/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.jpeg new file mode 100644 index 0000000..3714195 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.jpeg differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 0000000..0d9933e --- /dev/null +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +Beautiful, Fast and Functional Audiobook Player for your Audiobookshelf server. \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 0000000..ed0326a --- /dev/null +++ b/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +Vaani \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/video.txt b/fastlane/metadata/android/en-US/video.txt new file mode 100644 index 0000000..e69de29 diff --git a/fastlane/report.xml b/fastlane/report.xml new file mode 100644 index 0000000..9f24822 --- /dev/null +++ b/fastlane/report.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/images/vaani_logo.svg b/images/vaani_logo.svg new file mode 100644 index 0000000..6c79c5a --- /dev/null +++ b/images/vaani_logo.svg @@ -0,0 +1,36 @@ + + + + + Created by potrace 1.16, written by Peter Selinger 2001-2019 + + + + + + + + + + + + + \ No newline at end of file diff --git a/lib/api/api_provider.dart b/lib/api/api_provider.dart index daffff8..8994644 100644 --- a/lib/api/api_provider.dart +++ b/lib/api/api_provider.dart @@ -2,20 +2,22 @@ import 'dart:convert'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:http/http.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/db/cache_manager.dart'; +import 'package:vaani/models/error_response.dart'; import 'package:vaani/settings/api_settings_provider.dart'; +import 'package:vaani/settings/models/authenticated_user.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart'; part 'api_provider.g.dart'; // TODO: workaround for https://github.com/rrousselGit/riverpod/issues/3718 -typedef ResponseErrorHandler = void Function( - Response response, [ - Object? error, -]); +typedef ResponseErrorHandler = + void Function(Response response, [Object? error]); final _logger = Logger('api_provider'); @@ -31,23 +33,21 @@ Uri makeBaseUrl(String address) { /// get the api instance for the given base url @riverpod -AudiobookshelfApi audiobookshelfApi(AudiobookshelfApiRef ref, Uri? baseUrl) { +AudiobookshelfApi audiobookshelfApi(Ref ref, Uri? baseUrl) { // try to get the base url from app settings final apiSettings = ref.watch(apiSettingsProvider); baseUrl ??= apiSettings.activeServer?.serverUrl; - return AudiobookshelfApi( - baseUrl: makeBaseUrl(baseUrl.toString()), - ); + return AudiobookshelfApi(baseUrl: makeBaseUrl(baseUrl.toString())); } /// get the api instance for the authenticated user /// /// if the user is not authenticated throw an error @Riverpod(keepAlive: true) -AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) { - final apiSettings = ref.watch(apiSettingsProvider); - final user = apiSettings.activeUser; +AudiobookshelfApi authenticatedApi(Ref ref) { + final user = ref.watch(apiSettingsProvider.select((s) => s.activeUser)); if (user == null) { + _logger.severe('No active user can not provide authenticated api'); throw StateError('No active user'); } return AudiobookshelfApi( @@ -58,15 +58,15 @@ AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) { /// ping the server to check if it is reachable @riverpod -FutureOr isServerAlive(IsServerAliveRef ref, String address) async { +FutureOr isServerAlive(Ref ref, String address) async { if (address.isEmpty) { return false; } try { - return await AudiobookshelfApi(baseUrl: makeBaseUrl(address)) - .server - .ping() ?? + return await AudiobookshelfApi( + baseUrl: makeBaseUrl(address), + ).server.ping() ?? false; } catch (e) { return false; @@ -76,14 +76,15 @@ FutureOr isServerAlive(IsServerAliveRef ref, String address) async { /// fetch status of server @riverpod FutureOr serverStatus( - ServerStatusRef ref, + Ref ref, Uri baseUrl, [ ResponseErrorHandler? responseErrorHandler, ]) async { - _logger.fine('fetching server status: $baseUrl'); + _logger.fine('fetching server status: ${baseUrl.obfuscate()}'); final api = ref.watch(audiobookshelfApiProvider(baseUrl)); - final res = - await api.server.status(responseErrorHandler: responseErrorHandler); + final res = await api.server.status( + responseErrorHandler: responseErrorHandler, + ); _logger.fine('server status: $res'); return res; } @@ -96,20 +97,32 @@ class PersonalizedView extends _$PersonalizedView { final api = ref.watch(authenticatedApiProvider); final apiSettings = ref.watch(apiSettingsProvider); final user = apiSettings.activeUser; + if (user == null) { + _logger.warning('no active user'); + yield []; + return; + } if (apiSettings.activeLibraryId == null) { // set it to default user library by logging in and getting the library id - final login = - await api.login(username: user!.username!, password: user.password!); - ref.read(apiSettingsProvider.notifier).updateState( - apiSettings.copyWith(activeLibraryId: login!.userDefaultLibraryId), + final login = await ref.read(loginProvider().future); + if (login == null) { + _logger.shout('failed to login, not building personalized view'); + yield []; + return; + } + ref + .read(apiSettingsProvider.notifier) + .updateState( + apiSettings.copyWith(activeLibraryId: login.userDefaultLibraryId), ); + yield []; + return; } // try to find in cache // final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}'; - var key = 'personalizedView:${apiSettings.activeLibraryId! + user!.id!}'; - final cachedRes = await apiResponseCacheManager.getFileFromMemory( - key, - ) ?? + final key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}'; + final cachedRes = + await apiResponseCacheManager.getFileFromMemory(key) ?? await apiResponseCacheManager.getFileFromCache(key); if (cachedRes != null) { _logger.fine('reading from cache: $cachedRes for key: $key'); @@ -126,10 +139,11 @@ class PersonalizedView extends _$PersonalizedView { } } - // ! exagerated delay + // ! exaggerated delay // await Future.delayed(const Duration(seconds: 2)); - final res = await api.libraries - .getPersonalized(libraryId: apiSettings.activeLibraryId!); + final res = await api.libraries.getPersonalized( + libraryId: apiSettings.activeLibraryId!, + ); // debugPrint('personalizedView: ${res!.map((e) => e).toSet()}'); // save to cache if (res != null) { @@ -145,21 +159,19 @@ class PersonalizedView extends _$PersonalizedView { _logger.warning('failed to fetch personalized view'); yield []; } - } // method to force refresh the view and ignore the cache Future forceRefresh() async { // clear the cache + // TODO: find a better way to clear the cache for only personalized view key return apiResponseCacheManager.emptyCache(); } } /// fetch continue listening audiobooks @riverpod -FutureOr fetchContinueListening( - FetchContinueListeningRef ref, -) async { +FutureOr fetchContinueListening(Ref ref) async { final api = ref.watch(authenticatedApiProvider); final res = await api.me.getSessions(); // debugPrint( @@ -169,10 +181,46 @@ FutureOr fetchContinueListening( } @riverpod -FutureOr me( - MeRef ref, -) async { +FutureOr me(Ref ref) async { final api = ref.watch(authenticatedApiProvider); - final res = await api.me.getUser(); - return res!; + final errorResponseHandler = ErrorResponseHandler(); + final res = await api.me.getUser( + responseErrorHandler: errorResponseHandler.storeError, + ); + if (res == null) { + _logger.severe( + 'me failed, got response: ${errorResponseHandler.response.obfuscate()}', + ); + throw StateError('me failed'); + } + return res; +} + +@riverpod +FutureOr login(Ref ref, {AuthenticatedUser? user}) async { + if (user == null) { + // try to get the user from settings + final apiSettings = ref.watch(apiSettingsProvider); + user = apiSettings.activeUser; + if (user == null) { + _logger.severe('no active user to login'); + return null; + } + _logger.fine('no user provided, using active user: ${user.obfuscate()}'); + } + final api = ref.watch(audiobookshelfApiProvider(user.server.serverUrl)); + api.token = user.authToken; + var errorResponseHandler = ErrorResponseHandler(); + _logger.fine('logging in with authenticated api'); + final res = await api.misc.authorize( + responseErrorHandler: errorResponseHandler.storeError, + ); + if (res == null) { + _logger.severe( + 'login failed, got response: ${errorResponseHandler.response.obfuscate()}', + ); + return null; + } + _logger.fine('login response: ${res.obfuscate()}'); + return res; } diff --git a/lib/api/api_provider.g.dart b/lib/api/api_provider.g.dart index 67e86e2..dabe437 100644 --- a/lib/api/api_provider.g.dart +++ b/lib/api/api_provider.g.dart @@ -6,538 +6,536 @@ part of 'api_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$audiobookshelfApiHash() => r'2c310ea77fea9918ccf96180a92075acd037bd95'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning /// get the api instance for the given base url -/// -/// Copied from [audiobookshelfApi]. + @ProviderFor(audiobookshelfApi) -const audiobookshelfApiProvider = AudiobookshelfApiFamily(); +final audiobookshelfApiProvider = AudiobookshelfApiFamily._(); /// get the api instance for the given base url -/// -/// Copied from [audiobookshelfApi]. -class AudiobookshelfApiFamily extends Family { - /// get the api instance for the given base url - /// - /// Copied from [audiobookshelfApi]. - const AudiobookshelfApiFamily(); +final class AudiobookshelfApiProvider + extends + $FunctionalProvider< + AudiobookshelfApi, + AudiobookshelfApi, + AudiobookshelfApi + > + with $Provider { /// get the api instance for the given base url - /// - /// Copied from [audiobookshelfApi]. - AudiobookshelfApiProvider call( - Uri? baseUrl, - ) { - return AudiobookshelfApiProvider( - baseUrl, - ); + AudiobookshelfApiProvider._({ + required AudiobookshelfApiFamily super.from, + required Uri? super.argument, + }) : super( + retry: null, + name: r'audiobookshelfApiProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$audiobookshelfApiHash(); + + @override + String toString() { + return r'audiobookshelfApiProvider' + '' + '($argument)'; } + @$internal @override - AudiobookshelfApiProvider getProviderOverride( - covariant AudiobookshelfApiProvider provider, - ) { - return call( - provider.baseUrl, - ); + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + AudiobookshelfApi create(Ref ref) { + final argument = this.argument as Uri?; + return audiobookshelfApi(ref, argument); } - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'audiobookshelfApiProvider'; -} - -/// get the api instance for the given base url -/// -/// Copied from [audiobookshelfApi]. -class AudiobookshelfApiProvider extends AutoDisposeProvider { - /// get the api instance for the given base url - /// - /// Copied from [audiobookshelfApi]. - AudiobookshelfApiProvider( - Uri? baseUrl, - ) : this._internal( - (ref) => audiobookshelfApi( - ref as AudiobookshelfApiRef, - baseUrl, - ), - from: audiobookshelfApiProvider, - name: r'audiobookshelfApiProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$audiobookshelfApiHash, - dependencies: AudiobookshelfApiFamily._dependencies, - allTransitiveDependencies: - AudiobookshelfApiFamily._allTransitiveDependencies, - baseUrl: baseUrl, - ); - - AudiobookshelfApiProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.baseUrl, - }) : super.internal(); - - final Uri? baseUrl; - - @override - Override overrideWith( - AudiobookshelfApi Function(AudiobookshelfApiRef provider) create, - ) { - return ProviderOverride( + /// {@macro riverpod.override_with_value} + Override overrideWithValue(AudiobookshelfApi value) { + return $ProviderOverride( origin: this, - override: AudiobookshelfApiProvider._internal( - (ref) => create(ref as AudiobookshelfApiRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - baseUrl: baseUrl, - ), + providerOverride: $SyncValueProvider(value), ); } - @override - AutoDisposeProviderElement createElement() { - return _AudiobookshelfApiProviderElement(this); - } - @override bool operator ==(Object other) { - return other is AudiobookshelfApiProvider && other.baseUrl == baseUrl; + return other is AudiobookshelfApiProvider && other.argument == argument; } @override int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, baseUrl.hashCode); - - return _SystemHash.finish(hash); + return argument.hashCode; } } -mixin AudiobookshelfApiRef on AutoDisposeProviderRef { - /// The parameter `baseUrl` of this provider. - Uri? get baseUrl; -} +String _$audiobookshelfApiHash() => r'f23a06c404e11867a7f796877eaca99b8ff25458'; -class _AudiobookshelfApiProviderElement - extends AutoDisposeProviderElement - with AudiobookshelfApiRef { - _AudiobookshelfApiProviderElement(super.provider); +/// get the api instance for the given base url + +final class AudiobookshelfApiFamily extends $Family + with $FunctionalFamilyOverride { + AudiobookshelfApiFamily._() + : super( + retry: null, + name: r'audiobookshelfApiProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// get the api instance for the given base url + + AudiobookshelfApiProvider call(Uri? baseUrl) => + AudiobookshelfApiProvider._(argument: baseUrl, from: this); @override - Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl; + String toString() => r'audiobookshelfApiProvider'; } -String _$authenticatedApiHash() => r'f555efb6eede590b5a8d60cad2e6bfc2847e2d14'; - /// get the api instance for the authenticated user /// /// if the user is not authenticated throw an error -/// -/// Copied from [authenticatedApi]. + @ProviderFor(authenticatedApi) -final authenticatedApiProvider = Provider.internal( - authenticatedApi, - name: r'authenticatedApiProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$authenticatedApiHash, - dependencies: null, - allTransitiveDependencies: null, -); +final authenticatedApiProvider = AuthenticatedApiProvider._(); -typedef AuthenticatedApiRef = ProviderRef; -String _$isServerAliveHash() => r'6ff90b6e0febd2cd4a4d3a5209a59afc778cd3b6'; - -/// ping the server to check if it is reachable +/// get the api instance for the authenticated user /// -/// Copied from [isServerAlive]. -@ProviderFor(isServerAlive) -const isServerAliveProvider = IsServerAliveFamily(); +/// if the user is not authenticated throw an error -/// ping the server to check if it is reachable -/// -/// Copied from [isServerAlive]. -class IsServerAliveFamily extends Family> { - /// ping the server to check if it is reachable +final class AuthenticatedApiProvider + extends + $FunctionalProvider< + AudiobookshelfApi, + AudiobookshelfApi, + AudiobookshelfApi + > + with $Provider { + /// get the api instance for the authenticated user /// - /// Copied from [isServerAlive]. - const IsServerAliveFamily(); + /// if the user is not authenticated throw an error + AuthenticatedApiProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'authenticatedApiProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); - /// ping the server to check if it is reachable - /// - /// Copied from [isServerAlive]. - IsServerAliveProvider call( - String address, - ) { - return IsServerAliveProvider( - address, - ); + @override + String debugGetCreateSourceHash() => _$authenticatedApiHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + AudiobookshelfApi create(Ref ref) { + return authenticatedApi(ref); } - @override - IsServerAliveProvider getProviderOverride( - covariant IsServerAliveProvider provider, - ) { - return call( - provider.address, + /// {@macro riverpod.override_with_value} + Override overrideWithValue(AudiobookshelfApi value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), ); } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'isServerAliveProvider'; } +String _$authenticatedApiHash() => r'284be2c39823c20fb70035a136c430862c28fa27'; + /// ping the server to check if it is reachable -/// -/// Copied from [isServerAlive]. -class IsServerAliveProvider extends AutoDisposeFutureProvider { + +@ProviderFor(isServerAlive) +final isServerAliveProvider = IsServerAliveFamily._(); + +/// ping the server to check if it is reachable + +final class IsServerAliveProvider + extends $FunctionalProvider, bool, FutureOr> + with $FutureModifier, $FutureProvider { /// ping the server to check if it is reachable - /// - /// Copied from [isServerAlive]. - IsServerAliveProvider( - String address, - ) : this._internal( - (ref) => isServerAlive( - ref as IsServerAliveRef, - address, - ), - from: isServerAliveProvider, - name: r'isServerAliveProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$isServerAliveHash, - dependencies: IsServerAliveFamily._dependencies, - allTransitiveDependencies: - IsServerAliveFamily._allTransitiveDependencies, - address: address, - ); - - IsServerAliveProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.address, - }) : super.internal(); - - final String address; + IsServerAliveProvider._({ + required IsServerAliveFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'isServerAliveProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); @override - Override overrideWith( - FutureOr Function(IsServerAliveRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: IsServerAliveProvider._internal( - (ref) => create(ref as IsServerAliveRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - address: address, - ), - ); + String debugGetCreateSourceHash() => _$isServerAliveHash(); + + @override + String toString() { + return r'isServerAliveProvider' + '' + '($argument)'; } + @$internal @override - AutoDisposeFutureProviderElement createElement() { - return _IsServerAliveProviderElement(this); + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as String; + return isServerAlive(ref, argument); } @override bool operator ==(Object other) { - return other is IsServerAliveProvider && other.address == address; + return other is IsServerAliveProvider && other.argument == argument; } @override int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, address.hashCode); - - return _SystemHash.finish(hash); + return argument.hashCode; } } -mixin IsServerAliveRef on AutoDisposeFutureProviderRef { - /// The parameter `address` of this provider. - String get address; -} +String _$isServerAliveHash() => r'bb3a53cae1eb64b8760a56864feed47b7a3f1c29'; -class _IsServerAliveProviderElement - extends AutoDisposeFutureProviderElement with IsServerAliveRef { - _IsServerAliveProviderElement(super.provider); +/// ping the server to check if it is reachable + +final class IsServerAliveFamily extends $Family + with $FunctionalFamilyOverride, String> { + IsServerAliveFamily._() + : super( + retry: null, + name: r'isServerAliveProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// ping the server to check if it is reachable + + IsServerAliveProvider call(String address) => + IsServerAliveProvider._(argument: address, from: this); @override - String get address => (origin as IsServerAliveProvider).address; + String toString() => r'isServerAliveProvider'; } -String _$serverStatusHash() => r'2739906a1862d09b098588ebd16749a09032ee99'; - /// fetch status of server -/// -/// Copied from [serverStatus]. + @ProviderFor(serverStatus) -const serverStatusProvider = ServerStatusFamily(); +final serverStatusProvider = ServerStatusFamily._(); /// fetch status of server -/// -/// Copied from [serverStatus]. -class ServerStatusFamily extends Family> { + +final class ServerStatusProvider + extends + $FunctionalProvider< + AsyncValue, + ServerStatusResponse?, + FutureOr + > + with + $FutureModifier, + $FutureProvider { /// fetch status of server - /// - /// Copied from [serverStatus]. - const ServerStatusFamily(); + ServerStatusProvider._({ + required ServerStatusFamily super.from, + required (Uri, ResponseErrorHandler?) super.argument, + }) : super( + retry: null, + name: r'serverStatusProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$serverStatusHash(); + + @override + String toString() { + return r'serverStatusProvider' + '' + '$argument'; + } + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as (Uri, ResponseErrorHandler?); + return serverStatus(ref, argument.$1, argument.$2); + } + + @override + bool operator ==(Object other) { + return other is ServerStatusProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$serverStatusHash() => r'2d9c5d6f970caec555e5322d43a388ea8572619f'; + +/// fetch status of server + +final class ServerStatusFamily extends $Family + with + $FunctionalFamilyOverride< + FutureOr, + (Uri, ResponseErrorHandler?) + > { + ServerStatusFamily._() + : super( + retry: null, + name: r'serverStatusProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); /// fetch status of server - /// - /// Copied from [serverStatus]. + ServerStatusProvider call( Uri baseUrl, [ - void Function(Response, [Object?])? responseErrorHandler, - ]) { - return ServerStatusProvider( - baseUrl, - responseErrorHandler, - ); - } + ResponseErrorHandler? responseErrorHandler, + ]) => ServerStatusProvider._( + argument: (baseUrl, responseErrorHandler), + from: this, + ); @override - ServerStatusProvider getProviderOverride( - covariant ServerStatusProvider provider, - ) { - return call( - provider.baseUrl, - provider.responseErrorHandler, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'serverStatusProvider'; + String toString() => r'serverStatusProvider'; } -/// fetch status of server -/// -/// Copied from [serverStatus]. -class ServerStatusProvider - extends AutoDisposeFutureProvider { - /// fetch status of server - /// - /// Copied from [serverStatus]. - ServerStatusProvider( - Uri baseUrl, [ - void Function(Response, [Object?])? responseErrorHandler, - ]) : this._internal( - (ref) => serverStatus( - ref as ServerStatusRef, - baseUrl, - responseErrorHandler, - ), - from: serverStatusProvider, - name: r'serverStatusProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$serverStatusHash, - dependencies: ServerStatusFamily._dependencies, - allTransitiveDependencies: - ServerStatusFamily._allTransitiveDependencies, - baseUrl: baseUrl, - responseErrorHandler: responseErrorHandler, - ); +/// fetch the personalized view - ServerStatusProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.baseUrl, - required this.responseErrorHandler, - }) : super.internal(); +@ProviderFor(PersonalizedView) +final personalizedViewProvider = PersonalizedViewProvider._(); - final Uri baseUrl; - final void Function(Response, [Object?])? responseErrorHandler; - - @override - Override overrideWith( - FutureOr Function(ServerStatusRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: ServerStatusProvider._internal( - (ref) => create(ref as ServerStatusRef), - from: from, - name: null, +/// fetch the personalized view +final class PersonalizedViewProvider + extends $StreamNotifierProvider> { + /// fetch the personalized view + PersonalizedViewProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'personalizedViewProvider', + isAutoDispose: true, dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - baseUrl: baseUrl, - responseErrorHandler: responseErrorHandler, - ), - ); - } + $allTransitiveDependencies: null, + ); @override - AutoDisposeFutureProviderElement createElement() { - return _ServerStatusProviderElement(this); - } + String debugGetCreateSourceHash() => _$personalizedViewHash(); + @$internal @override - bool operator ==(Object other) { - return other is ServerStatusProvider && - other.baseUrl == baseUrl && - other.responseErrorHandler == responseErrorHandler; - } + PersonalizedView create() => PersonalizedView(); +} +String _$personalizedViewHash() => r'425e89d99d7e4712b4d6a688f3a12442bd66584f'; + +/// fetch the personalized view + +abstract class _$PersonalizedView extends $StreamNotifier> { + Stream> build(); + @$mustCallSuper @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, baseUrl.hashCode); - hash = _SystemHash.combine(hash, responseErrorHandler.hashCode); - - return _SystemHash.finish(hash); + void runBuild() { + final ref = this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleCreate(ref, build); } } -mixin ServerStatusRef on AutoDisposeFutureProviderRef { - /// The parameter `baseUrl` of this provider. - Uri get baseUrl; +/// fetch continue listening audiobooks - /// The parameter `responseErrorHandler` of this provider. - void Function(Response, [Object?])? get responseErrorHandler; -} +@ProviderFor(fetchContinueListening) +final fetchContinueListeningProvider = FetchContinueListeningProvider._(); -class _ServerStatusProviderElement - extends AutoDisposeFutureProviderElement - with ServerStatusRef { - _ServerStatusProviderElement(super.provider); +/// fetch continue listening audiobooks + +final class FetchContinueListeningProvider + extends + $FunctionalProvider< + AsyncValue, + GetUserSessionsResponse, + FutureOr + > + with + $FutureModifier, + $FutureProvider { + /// fetch continue listening audiobooks + FetchContinueListeningProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'fetchContinueListeningProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); @override - Uri get baseUrl => (origin as ServerStatusProvider).baseUrl; + String debugGetCreateSourceHash() => _$fetchContinueListeningHash(); + + @$internal @override - void Function(Response, [Object?])? get responseErrorHandler => - (origin as ServerStatusProvider).responseErrorHandler; + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return fetchContinueListening(ref); + } } String _$fetchContinueListeningHash() => - r'f65fe3ac3a31b8ac074330525c5d2cc4b526802d'; + r'50aeb77369eda38d496b2f56f3df2aea135dab45'; -/// fetch continue listening audiobooks -/// -/// Copied from [fetchContinueListening]. -@ProviderFor(fetchContinueListening) -final fetchContinueListeningProvider = - AutoDisposeFutureProvider.internal( - fetchContinueListening, - name: r'fetchContinueListeningProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$fetchContinueListeningHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef FetchContinueListeningRef - = AutoDisposeFutureProviderRef; -String _$meHash() => r'bdc664c4fd867ad13018fa769ce7a6913248c44f'; - -/// See also [me]. @ProviderFor(me) -final meProvider = AutoDisposeFutureProvider.internal( - me, - name: r'meProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$meHash, - dependencies: null, - allTransitiveDependencies: null, -); +final meProvider = MeProvider._(); -typedef MeRef = AutoDisposeFutureProviderRef; -String _$personalizedViewHash() => r'4c392ece4650bdc36d7195a0ddb8810e8fe4caa9'; +final class MeProvider + extends $FunctionalProvider, User, FutureOr> + with $FutureModifier, $FutureProvider { + MeProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'meProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); -/// fetch the personalized view -/// -/// Copied from [PersonalizedView]. -@ProviderFor(PersonalizedView) -final personalizedViewProvider = - AutoDisposeStreamNotifierProvider>.internal( - PersonalizedView.new, - name: r'personalizedViewProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$personalizedViewHash, - dependencies: null, - allTransitiveDependencies: null, -); + @override + String debugGetCreateSourceHash() => _$meHash(); -typedef _$PersonalizedView = AutoDisposeStreamNotifier>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return me(ref); + } +} + +String _$meHash() => r'b3b6d6d940b465c60d0c29cd6e81ba2fcccab186'; + +@ProviderFor(login) +final loginProvider = LoginFamily._(); + +final class LoginProvider + extends + $FunctionalProvider< + AsyncValue, + LoginResponse?, + FutureOr + > + with $FutureModifier, $FutureProvider { + LoginProvider._({ + required LoginFamily super.from, + required AuthenticatedUser? super.argument, + }) : super( + retry: null, + name: r'loginProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$loginHash(); + + @override + String toString() { + return r'loginProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as AuthenticatedUser?; + return login(ref, user: argument); + } + + @override + bool operator ==(Object other) { + return other is LoginProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$loginHash() => r'99410c2bed9c8f412c7b47c4e655db64e0054be2'; + +final class LoginFamily extends $Family + with + $FunctionalFamilyOverride< + FutureOr, + AuthenticatedUser? + > { + LoginFamily._() + : super( + retry: null, + name: r'loginProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + LoginProvider call({AuthenticatedUser? user}) => + LoginProvider._(argument: user, from: this); + + @override + String toString() => r'loginProvider'; +} diff --git a/lib/api/authenticated_user_provider.g.dart b/lib/api/authenticated_user_provider.g.dart deleted file mode 100644 index 7fca094..0000000 --- a/lib/api/authenticated_user_provider.g.dart +++ /dev/null @@ -1,28 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'authenticated_user_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$authenticatedUserHash() => r'308f19b33ae04af6340fb83167fa64aa23400a09'; - -/// provides with a set of authenticated users -/// -/// Copied from [AuthenticatedUser]. -@ProviderFor(AuthenticatedUser) -final authenticatedUserProvider = AutoDisposeNotifierProvider>.internal( - AuthenticatedUser.new, - name: r'authenticatedUserProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$authenticatedUserHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$AuthenticatedUser = AutoDisposeNotifier>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/api/authenticated_user_provider.dart b/lib/api/authenticated_users_provider.dart similarity index 69% rename from lib/api/authenticated_user_provider.dart rename to lib/api/authenticated_users_provider.dart index c10f799..6d437e0 100644 --- a/lib/api/authenticated_user_provider.dart +++ b/lib/api/authenticated_users_provider.dart @@ -5,21 +5,21 @@ import 'package:vaani/api/server_provider.dart' import 'package:vaani/db/storage.dart'; import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/models/audiobookshelf_server.dart'; -import 'package:vaani/settings/models/authenticated_user.dart' - as model; +import 'package:vaani/settings/models/authenticated_user.dart' as model; +import 'package:vaani/shared/extensions/obfuscation.dart'; -part 'authenticated_user_provider.g.dart'; +part 'authenticated_users_provider.g.dart'; final _box = AvailableHiveBoxes.authenticatedUserBox; -final _logger = Logger('authenticated_user_provider'); +final _logger = Logger('authenticated_users_provider'); /// provides with a set of authenticated users @riverpod -class AuthenticatedUser extends _$AuthenticatedUser { +class AuthenticatedUsers extends _$AuthenticatedUsers { @override Set build() { - ref.listenSelf((_, __) { + listenSelf((_, __) { writeStateToBox(); }); // get the app settings @@ -35,7 +35,7 @@ class AuthenticatedUser extends _$AuthenticatedUser { Set readFromBoxOrCreate() { if (_box.isNotEmpty) { final foundData = _box.getRange(0, _box.length); - _logger.fine('found users in box: $foundData'); + _logger.fine('found users in box: ${foundData.obfuscate()}'); return foundData.toSet(); } else { _logger.fine('no settings found in box'); @@ -49,18 +49,17 @@ class AuthenticatedUser extends _$AuthenticatedUser { return; } _box.addAll(state); - _logger.fine('writing state to box: $state'); + _logger.fine('writing state to box: ${state.obfuscate()}'); } void addUser(model.AuthenticatedUser user, {bool setActive = false}) { state = state..add(user); + ref.invalidateSelf(); if (setActive) { final apiSettings = ref.read(apiSettingsProvider); - ref.read(apiSettingsProvider.notifier).updateState( - apiSettings.copyWith( - activeUser: user, - ), - ); + ref + .read(apiSettingsProvider.notifier) + .updateState(apiSettings.copyWith(activeUser: user)); } } @@ -80,11 +79,12 @@ class AuthenticatedUser extends _$AuthenticatedUser { // also remove the user from the active user final apiSettings = ref.read(apiSettingsProvider); if (apiSettings.activeUser == user) { - ref.read(apiSettingsProvider.notifier).updateState( - apiSettings.copyWith( - activeUser: null, - ), - ); + // replace the active user with the first user in the list + // or null if there are no users left + final newActiveUser = state.isNotEmpty ? state.first : null; + ref + .read(apiSettingsProvider.notifier) + .updateState(apiSettings.copyWith(activeUser: newActiveUser)); } } } diff --git a/lib/api/authenticated_users_provider.g.dart b/lib/api/authenticated_users_provider.g.dart new file mode 100644 index 0000000..8e3021f --- /dev/null +++ b/lib/api/authenticated_users_provider.g.dart @@ -0,0 +1,75 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'authenticated_users_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// provides with a set of authenticated users + +@ProviderFor(AuthenticatedUsers) +final authenticatedUsersProvider = AuthenticatedUsersProvider._(); + +/// provides with a set of authenticated users +final class AuthenticatedUsersProvider + extends + $NotifierProvider> { + /// provides with a set of authenticated users + AuthenticatedUsersProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'authenticatedUsersProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$authenticatedUsersHash(); + + @$internal + @override + AuthenticatedUsers create() => AuthenticatedUsers(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Set value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + +String _$authenticatedUsersHash() => + r'c5e82cc70ffc31a0d315e3db9e07a141c583471e'; + +/// provides with a set of authenticated users + +abstract class _$AuthenticatedUsers + extends $Notifier> { + Set build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref, Set>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + Set, + Set + >, + Set, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/api/image_provider.dart b/lib/api/image_provider.dart index 42b9e4b..79fac06 100644 --- a/lib/api/image_provider.dart +++ b/lib/api/image_provider.dart @@ -4,6 +4,7 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; +import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/db/cache_manager.dart'; /// provides cover images for the audiobooks @@ -19,52 +20,52 @@ final _logger = Logger('cover_image_provider'); @Riverpod(keepAlive: true) class CoverImage extends _$CoverImage { @override - Stream build(LibraryItem libraryItem) async* { + Stream build(String itemId) async* { final api = ref.watch(authenticatedApiProvider); // ! artifical delay for testing // await Future.delayed(const Duration(seconds: 2)); // try to get the image from the cache - final file = await imageCacheManager.getFileFromMemory(libraryItem.id) ?? - await imageCacheManager.getFileFromCache(libraryItem.id); + final file = + await imageCacheManager.getFileFromMemory(itemId) ?? + await imageCacheManager.getFileFromCache(itemId); if (file != null) { // if the image is in the cache, yield it _logger.fine( - 'cover image found in cache for ${libraryItem.id} at ${file.file.path}', + 'cover image found in cache for $itemId at ${file.file.path}', ); yield await file.file.readAsBytes(); + final libraryItem = await ref.watch(libraryItemProvider(itemId).future); // return if no need to fetch from the server if (libraryItem.updatedAt.isBefore(await file.file.lastModified())) { _logger.fine( - 'cover image is up to date for ${libraryItem.id}, no need to fetch from the server', + 'cover image is up to date for $itemId, no need to fetch from the server', ); return; } else { - _logger.fine( - 'cover image stale for ${libraryItem.id}, fetching from the server', - ); + _logger.fine('cover image stale for $itemId, fetching from the server'); } } else { - _logger.fine('cover image not found in cache for ${libraryItem.id}'); + _logger.fine('cover image not found in cache for $itemId'); } // check if the image is in the cache final coverImage = await api.items.getCover( - libraryItemId: libraryItem.id, + libraryItemId: itemId, parameters: const GetImageReqParams(width: 1200), ); // save the image to the cache if (coverImage != null) { final newFile = await imageCacheManager.putFile( - libraryItem.id, + itemId, coverImage, - key: libraryItem.id, + key: itemId, fileExtension: 'jpg', ); _logger.fine( - 'cover image fetched for for ${libraryItem.id}, file time: ${await newFile.lastModified()}', + 'cover image fetched for for $itemId, file time: ${await newFile.lastModified()}', ); } diff --git a/lib/api/image_provider.g.dart b/lib/api/image_provider.g.dart index 74f9d8e..02ccad8 100644 --- a/lib/api/image_provider.g.dart +++ b/lib/api/image_provider.g.dart @@ -6,167 +6,94 @@ part of 'image_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$coverImageHash() => r'702afafa217dfcbb290837caf21cc1ef54defd55'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$CoverImage extends BuildlessStreamNotifier { - late final LibraryItem libraryItem; - - Stream build( - LibraryItem libraryItem, - ); -} - -/// See also [CoverImage]. @ProviderFor(CoverImage) -const coverImageProvider = CoverImageFamily(); +final coverImageProvider = CoverImageFamily._(); -/// See also [CoverImage]. -class CoverImageFamily extends Family> { - /// See also [CoverImage]. - const CoverImageFamily(); +final class CoverImageProvider + extends $StreamNotifierProvider { + CoverImageProvider._({ + required CoverImageFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'coverImageProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); - /// See also [CoverImage]. - CoverImageProvider call( - LibraryItem libraryItem, - ) { - return CoverImageProvider( - libraryItem, - ); + @override + String debugGetCreateSourceHash() => _$coverImageHash(); + + @override + String toString() { + return r'coverImageProvider' + '' + '($argument)'; } + @$internal @override - CoverImageProvider getProviderOverride( - covariant CoverImageProvider provider, - ) { - return call( - provider.libraryItem, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'coverImageProvider'; -} - -/// See also [CoverImage]. -class CoverImageProvider - extends StreamNotifierProviderImpl { - /// See also [CoverImage]. - CoverImageProvider( - LibraryItem libraryItem, - ) : this._internal( - () => CoverImage()..libraryItem = libraryItem, - from: coverImageProvider, - name: r'coverImageProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$coverImageHash, - dependencies: CoverImageFamily._dependencies, - allTransitiveDependencies: - CoverImageFamily._allTransitiveDependencies, - libraryItem: libraryItem, - ); - - CoverImageProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.libraryItem, - }) : super.internal(); - - final LibraryItem libraryItem; - - @override - Stream runNotifierBuild( - covariant CoverImage notifier, - ) { - return notifier.build( - libraryItem, - ); - } - - @override - Override overrideWith(CoverImage Function() create) { - return ProviderOverride( - origin: this, - override: CoverImageProvider._internal( - () => create()..libraryItem = libraryItem, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - libraryItem: libraryItem, - ), - ); - } - - @override - StreamNotifierProviderElement createElement() { - return _CoverImageProviderElement(this); - } + CoverImage create() => CoverImage(); @override bool operator ==(Object other) { - return other is CoverImageProvider && other.libraryItem == libraryItem; + return other is CoverImageProvider && other.argument == argument; } @override int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, libraryItem.hashCode); - - return _SystemHash.finish(hash); + return argument.hashCode; } } -mixin CoverImageRef on StreamNotifierProviderRef { - /// The parameter `libraryItem` of this provider. - LibraryItem get libraryItem; -} +String _$coverImageHash() => r'89cc4783cbc76bb41beae34384d92fb277135c75'; -class _CoverImageProviderElement - extends StreamNotifierProviderElement - with CoverImageRef { - _CoverImageProviderElement(super.provider); +final class CoverImageFamily extends $Family + with + $ClassFamilyOverride< + CoverImage, + AsyncValue, + Uint8List, + Stream, + String + > { + CoverImageFamily._() + : super( + retry: null, + name: r'coverImageProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: false, + ); + + CoverImageProvider call(String itemId) => + CoverImageProvider._(argument: itemId, from: this); @override - LibraryItem get libraryItem => (origin as CoverImageProvider).libraryItem; + String toString() => r'coverImageProvider'; +} + +abstract class _$CoverImage extends $StreamNotifier { + late final _$args = ref.$arg as String; + String get itemId => _$args; + + Stream build(String itemId); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, Uint8List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, Uint8List>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } } -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/api/library_item_provider.dart b/lib/api/library_item_provider.dart index cf81ebd..5a4b1a3 100644 --- a/lib/api/library_item_provider.dart +++ b/lib/api/library_item_provider.dart @@ -13,7 +13,7 @@ part 'library_item_provider.g.dart'; final _logger = Logger('LibraryItemProvider'); /// provides the library item for the given id -@riverpod +@Riverpod(keepAlive: true) class LibraryItem extends _$LibraryItem { @override Stream build(String id) async* { @@ -22,11 +22,12 @@ class LibraryItem extends _$LibraryItem { _logger.fine('LibraryItemProvider fetching library item: $id'); // ! this is a mock delay - // await Future.delayed(const Duration(seconds: 10)); + // await Future.delayed(const Duration(seconds: 150)); // look for the item in the cache final key = CacheKey.libraryItem(id); - final cachedFile = await apiResponseCacheManager.getFileFromMemory(key) ?? + final cachedFile = + await apiResponseCacheManager.getFileFromMemory(key) ?? await apiResponseCacheManager.getFileFromCache(key); if (cachedFile != null) { _logger.fine( diff --git a/lib/api/library_item_provider.g.dart b/lib/api/library_item_provider.g.dart index 80370c6..df8fe0e 100644 --- a/lib/api/library_item_provider.g.dart +++ b/lib/api/library_item_provider.g.dart @@ -6,183 +6,112 @@ part of 'library_item_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$libraryItemHash() => r'fa3f8309349c5b1b777f1bc919616e51c3f5b520'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$LibraryItem - extends BuildlessAutoDisposeStreamNotifier { - late final String id; - - Stream build( - String id, - ); -} - +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning /// provides the library item for the given id -/// -/// Copied from [LibraryItem]. + @ProviderFor(LibraryItem) -const libraryItemProvider = LibraryItemFamily(); +final libraryItemProvider = LibraryItemFamily._(); /// provides the library item for the given id -/// -/// Copied from [LibraryItem]. -class LibraryItemFamily - extends Family> { +final class LibraryItemProvider + extends $StreamNotifierProvider { /// provides the library item for the given id - /// - /// Copied from [LibraryItem]. - const LibraryItemFamily(); + LibraryItemProvider._({ + required LibraryItemFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'libraryItemProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); - /// provides the library item for the given id - /// - /// Copied from [LibraryItem]. - LibraryItemProvider call( - String id, - ) { - return LibraryItemProvider( - id, - ); + @override + String debugGetCreateSourceHash() => _$libraryItemHash(); + + @override + String toString() { + return r'libraryItemProvider' + '' + '($argument)'; } + @$internal @override - LibraryItemProvider getProviderOverride( - covariant LibraryItemProvider provider, - ) { - return call( - provider.id, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'libraryItemProvider'; -} - -/// provides the library item for the given id -/// -/// Copied from [LibraryItem]. -class LibraryItemProvider extends AutoDisposeStreamNotifierProviderImpl< - LibraryItem, shelfsdk.LibraryItemExpanded> { - /// provides the library item for the given id - /// - /// Copied from [LibraryItem]. - LibraryItemProvider( - String id, - ) : this._internal( - () => LibraryItem()..id = id, - from: libraryItemProvider, - name: r'libraryItemProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$libraryItemHash, - dependencies: LibraryItemFamily._dependencies, - allTransitiveDependencies: - LibraryItemFamily._allTransitiveDependencies, - id: id, - ); - - LibraryItemProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.id, - }) : super.internal(); - - final String id; - - @override - Stream runNotifierBuild( - covariant LibraryItem notifier, - ) { - return notifier.build( - id, - ); - } - - @override - Override overrideWith(LibraryItem Function() create) { - return ProviderOverride( - origin: this, - override: LibraryItemProvider._internal( - () => create()..id = id, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - id: id, - ), - ); - } - - @override - AutoDisposeStreamNotifierProviderElement createElement() { - return _LibraryItemProviderElement(this); - } + LibraryItem create() => LibraryItem(); @override bool operator ==(Object other) { - return other is LibraryItemProvider && other.id == id; + return other is LibraryItemProvider && other.argument == argument; } @override int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, id.hashCode); - - return _SystemHash.finish(hash); + return argument.hashCode; } } -mixin LibraryItemRef - on AutoDisposeStreamNotifierProviderRef { - /// The parameter `id` of this provider. - String get id; -} +String _$libraryItemHash() => r'a3cfa7f912e9498a70b5782899018b6964d6445c'; -class _LibraryItemProviderElement - extends AutoDisposeStreamNotifierProviderElement with LibraryItemRef { - _LibraryItemProviderElement(super.provider); +/// provides the library item for the given id + +final class LibraryItemFamily extends $Family + with + $ClassFamilyOverride< + LibraryItem, + AsyncValue, + shelfsdk.LibraryItemExpanded, + Stream, + String + > { + LibraryItemFamily._() + : super( + retry: null, + name: r'libraryItemProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: false, + ); + + /// provides the library item for the given id + + LibraryItemProvider call(String id) => + LibraryItemProvider._(argument: id, from: this); @override - String get id => (origin as LibraryItemProvider).id; + String toString() => r'libraryItemProvider'; +} + +/// provides the library item for the given id + +abstract class _$LibraryItem + extends $StreamNotifier { + late final _$args = ref.$arg as String; + String get id => _$args; + + Stream build(String id); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref< + AsyncValue, + shelfsdk.LibraryItemExpanded + >; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + AsyncValue, + shelfsdk.LibraryItemExpanded + >, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } } -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/api/library_provider.dart b/lib/api/library_provider.dart new file mode 100644 index 0000000..3ba5399 --- /dev/null +++ b/lib/api/library_provider.dart @@ -0,0 +1,60 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart' + show Ref, ProviderListenableSelect; +import 'package:logging/logging.dart' show Logger; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import 'package:shelfsdk/audiobookshelf_api.dart' show Library; +import 'package:vaani/api/api_provider.dart' show authenticatedApiProvider; +import 'package:vaani/settings/api_settings_provider.dart' + show apiSettingsProvider; +part 'library_provider.g.dart'; + +final _logger = Logger('LibraryProvider'); + +@riverpod +Future library(Ref ref, String id) async { + final api = ref.watch(authenticatedApiProvider); + final library = await api.libraries.get(libraryId: id); + if (library == null) { + _logger.warning('No library found through id: $id'); + // try to get the library from the list of libraries + final libraries = await ref.watch(librariesProvider.future); + for (final lib in libraries) { + if (lib.id == id) { + return lib; + } + } + _logger.warning('No library found in the list of libraries'); + return null; + } + _logger.fine('Fetched library: $library'); + return library.library; +} + +@riverpod +Future currentLibrary(Ref ref) async { + final libraryId = ref.watch( + apiSettingsProvider.select((s) => s.activeLibraryId), + ); + if (libraryId == null) { + _logger.warning('No active library id found'); + return null; + } + return await ref.watch(libraryProvider(libraryId).future); +} + +@riverpod +class Libraries extends _$Libraries { + @override + FutureOr> build() async { + final api = ref.watch(authenticatedApiProvider); + final libraries = await api.libraries.getAll(); + if (libraries == null) { + _logger.warning('Failed to fetch libraries'); + return []; + } + _logger.fine('Fetched ${libraries.length} libraries'); + ref.keepAlive(); + return libraries; + } +} diff --git a/lib/api/library_provider.g.dart b/lib/api/library_provider.g.dart new file mode 100644 index 0000000..df8fbfd --- /dev/null +++ b/lib/api/library_provider.g.dart @@ -0,0 +1,158 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'library_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(library) +final libraryProvider = LibraryFamily._(); + +final class LibraryProvider + extends + $FunctionalProvider, Library?, FutureOr> + with $FutureModifier, $FutureProvider { + LibraryProvider._({ + required LibraryFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'libraryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$libraryHash(); + + @override + String toString() { + return r'libraryProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as String; + return library(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is LibraryProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$libraryHash() => r'f8a34100acb58f02fa958c71a629577bf815710e'; + +final class LibraryFamily extends $Family + with $FunctionalFamilyOverride, String> { + LibraryFamily._() + : super( + retry: null, + name: r'libraryProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + LibraryProvider call(String id) => + LibraryProvider._(argument: id, from: this); + + @override + String toString() => r'libraryProvider'; +} + +@ProviderFor(currentLibrary) +final currentLibraryProvider = CurrentLibraryProvider._(); + +final class CurrentLibraryProvider + extends + $FunctionalProvider, Library?, FutureOr> + with $FutureModifier, $FutureProvider { + CurrentLibraryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'currentLibraryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$currentLibraryHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return currentLibrary(ref); + } +} + +String _$currentLibraryHash() => r'658498a531e04a01e2b3915a3319101285601118'; + +@ProviderFor(Libraries) +final librariesProvider = LibrariesProvider._(); + +final class LibrariesProvider + extends $AsyncNotifierProvider> { + LibrariesProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'librariesProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$librariesHash(); + + @$internal + @override + Libraries create() => Libraries(); +} + +String _$librariesHash() => r'95ebd4d1ac0cc2acf7617dc22895eff0ca30600f'; + +abstract class _$Libraries extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/api/server_provider.dart b/lib/api/server_provider.dart index 4ad4b1e..41c82f1 100644 --- a/lib/api/server_provider.dart +++ b/lib/api/server_provider.dart @@ -1,16 +1,17 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:vaani/api/authenticated_user_provider.dart'; +import 'package:vaani/api/authenticated_users_provider.dart'; import 'package:vaani/db/storage.dart'; import 'package:vaani/settings/api_settings_provider.dart'; -import 'package:vaani/settings/models/audiobookshelf_server.dart' - as model; +import 'package:vaani/settings/models/audiobookshelf_server.dart' as model; +import 'package:vaani/shared/extensions/obfuscation.dart'; part 'server_provider.g.dart'; final _box = AvailableHiveBoxes.serverBox; +final _logger = Logger('AudiobookShelfServerProvider'); + class ServerAlreadyExistsException implements Exception { final model.AudiobookShelfServer server; @@ -27,7 +28,7 @@ class ServerAlreadyExistsException implements Exception { class AudiobookShelfServer extends _$AudiobookShelfServer { @override Set build() { - ref.listenSelf((_, __) { + listenSelf((_, __) { writeStateToBox(); }); // get the app settings @@ -47,10 +48,10 @@ class AudiobookShelfServer extends _$AudiobookShelfServer { Set readFromBoxOrCreate() { if (_box.isNotEmpty) { final foundServers = _box.getRange(0, _box.length); - debugPrint('found servers in box: $foundServers'); - return foundServers.whereNotNull().toSet(); + _logger.info('found servers in box: ${foundServers.obfuscate()}'); + return foundServers.nonNulls.toSet(); } else { - debugPrint('no settings found in box'); + _logger.info('no settings found in box'); return {}; } } @@ -61,7 +62,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer { return; } _box.addAll(state); - debugPrint('writing state to box: $state'); + _logger.info('writing state to box: ${state.obfuscate()}'); } void addServer(model.AudiobookShelfServer server) { @@ -71,23 +72,21 @@ class AudiobookShelfServer extends _$AudiobookShelfServer { state = {...state, server}; } - void removeServer(model.AudiobookShelfServer server, - { + void removeServer( + model.AudiobookShelfServer server, { bool removeUsers = false, }) { state = state.where((s) => s != server).toSet(); // remove the server from the active server final apiSettings = ref.read(apiSettingsProvider); if (apiSettings.activeServer == server) { - ref.read(apiSettingsProvider.notifier).updateState( - apiSettings.copyWith( - activeServer: null, - ), - ); + ref + .read(apiSettingsProvider.notifier) + .updateState(apiSettings.copyWith(activeServer: null)); } // remove the users of this server if (removeUsers) { - ref.read(authenticatedUserProvider.notifier).removeUsersOfServer(server); + ref.read(authenticatedUsersProvider.notifier).removeUsersOfServer(server); } } diff --git a/lib/api/server_provider.g.dart b/lib/api/server_provider.g.dart index 69478cc..16eddd0 100644 --- a/lib/api/server_provider.g.dart +++ b/lib/api/server_provider.g.dart @@ -6,25 +6,78 @@ part of 'server_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$audiobookShelfServerHash() => - r'f0d645bb42233c59886bc43fdc473897484ceca1'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// provides with a set of servers added by the user + +@ProviderFor(AudiobookShelfServer) +final audiobookShelfServerProvider = AudiobookShelfServerProvider._(); /// provides with a set of servers added by the user -/// -/// Copied from [AudiobookShelfServer]. -@ProviderFor(AudiobookShelfServer) -final audiobookShelfServerProvider = AutoDisposeNotifierProvider< - AudiobookShelfServer, Set>.internal( - AudiobookShelfServer.new, - name: r'audiobookShelfServerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$audiobookShelfServerHash, - dependencies: null, - allTransitiveDependencies: null, -); +final class AudiobookShelfServerProvider + extends + $NotifierProvider< + AudiobookShelfServer, + Set + > { + /// provides with a set of servers added by the user + AudiobookShelfServerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'audiobookShelfServerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); -typedef _$AudiobookShelfServer - = AutoDisposeNotifier>; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member + @override + String debugGetCreateSourceHash() => _$audiobookShelfServerHash(); + + @$internal + @override + AudiobookShelfServer create() => AudiobookShelfServer(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Set value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>( + value, + ), + ); + } +} + +String _$audiobookShelfServerHash() => + r'144817dcb3704b80c5b60763167fcf932f00c29c'; + +/// provides with a set of servers added by the user + +abstract class _$AudiobookShelfServer + extends $Notifier> { + Set build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref< + Set, + Set + >; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + Set, + Set + >, + Set, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/constants/hero_tag_conventions.dart b/lib/constants/hero_tag_conventions.dart index 6934c00..4ec40a9 100644 --- a/lib/constants/hero_tag_conventions.dart +++ b/lib/constants/hero_tag_conventions.dart @@ -10,5 +10,4 @@ class HeroTagPrefixes { static const String bookTitle = 'book_title_'; static const String narratorName = 'narrator_name_'; static const String libraryItemPlayButton = 'library_item_play_button_'; - } diff --git a/lib/db/available_boxes.dart b/lib/db/available_boxes.dart index ff01acc..77b0c3f 100644 --- a/lib/db/available_boxes.dart +++ b/lib/db/available_boxes.dart @@ -1,5 +1,5 @@ import 'package:flutter/foundation.dart' show immutable; -import 'package:hive/hive.dart'; +import 'package:hive_plus_secure/hive_plus_secure.dart'; import 'package:vaani/features/per_book_settings/models/book_settings.dart'; import 'package:vaani/settings/models/models.dart'; @@ -14,14 +14,17 @@ class AvailableHiveBoxes { static final apiSettingsBox = Hive.box(name: 'apiSettings'); /// stores the a list of [AudiobookShelfServer] - static final serverBox = - Hive.box(name: 'audiobookShelfServer'); + static final serverBox = Hive.box( + name: 'audiobookShelfServer', + ); /// stores the a list of [AuthenticatedUser] - static final authenticatedUserBox = - Hive.box(name: 'authenticatedUser'); + static final authenticatedUserBox = Hive.box( + name: 'authenticatedUser', + ); /// stores the a list of [BookSettings] - static final individualBookSettingsBox = - Hive.box(name: 'bookSettings'); + static final individualBookSettingsBox = Hive.box( + name: 'bookSettings', + ); } diff --git a/lib/db/cache/schemas/image.dart b/lib/db/cache/schemas/image.dart index 9e93073..fef8025 100644 --- a/lib/db/cache/schemas/image.dart +++ b/lib/db/cache/schemas/image.dart @@ -1,39 +1,39 @@ -import 'package:isar/isar.dart'; +// import 'package:isar/isar.dart'; -part 'image.g.dart'; +// part 'image.g.dart'; -/// Represents a cover image for a library item -/// -/// stores 2 paths, one is thumbnail and the other is the full size image -/// both are optional -/// also stores last fetched date for the image -/// Id is passed as a parameter to the collection annotation (the lib_item_id) -/// also index the id -/// This is because the image is a part of the library item and the library item -/// is the parent of the image -@Collection(ignore: {'path'}) -@Name('CacheImage') -class Image { - @Id() - int id; +// /// Represents a cover image for a library item +// /// +// /// stores 2 paths, one is thumbnail and the other is the full size image +// /// both are optional +// /// also stores last fetched date for the image +// /// Id is passed as a parameter to the collection annotation (the lib_item_id) +// /// also index the id +// /// This is because the image is a part of the library item and the library item +// /// is the parent of the image +// @Collection(ignore: {'path'}) +// @Name('CacheImage') +// class Image { +// @Id() +// int id; - String? thumbnailPath; - String? imagePath; - DateTime lastSaved; +// String? thumbnailPath; +// String? imagePath; +// DateTime lastSaved; - Image({ - required this.id, - this.thumbnailPath, - this.imagePath, - }) : lastSaved = DateTime.now(); +// Image({ +// required this.id, +// this.thumbnailPath, +// this.imagePath, +// }) : lastSaved = DateTime.now(); - /// returns the path to the image - String? get path => thumbnailPath ?? imagePath; +// /// returns the path to the image +// String? get path => thumbnailPath ?? imagePath; - /// automatically updates the last fetched date when saving a new path - void updatePath(String? thumbnailPath, String? imagePath) async { - this.thumbnailPath = thumbnailPath; - this.imagePath = imagePath; - lastSaved = DateTime.now(); - } -} +// /// automatically updates the last fetched date when saving a new path +// void updatePath(String? thumbnailPath, String? imagePath) async { +// this.thumbnailPath = thumbnailPath; +// this.imagePath = imagePath; +// lastSaved = DateTime.now(); +// } +// } diff --git a/lib/db/cache/schemas/image.g.dart b/lib/db/cache/schemas/image.g.dart deleted file mode 100644 index 555ce5e..0000000 --- a/lib/db/cache/schemas/image.g.dart +++ /dev/null @@ -1,1009 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'image.dart'; - -// ************************************************************************** -// _IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, invalid_use_of_protected_member, lines_longer_than_80_chars, constant_identifier_names, avoid_js_rounded_ints, no_leading_underscores_for_local_identifiers, require_trailing_commas, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_in_if_null_operators, library_private_types_in_public_api, prefer_const_constructors -// ignore_for_file: type=lint - -extension GetImageCollection on Isar { - IsarCollection get images => this.collection(); -} - -const ImageSchema = IsarGeneratedSchema( - schema: IsarSchema( - name: 'CacheImage', - idName: 'id', - embedded: false, - properties: [ - IsarPropertySchema( - name: 'thumbnailPath', - type: IsarType.string, - ), - IsarPropertySchema( - name: 'imagePath', - type: IsarType.string, - ), - IsarPropertySchema( - name: 'lastSaved', - type: IsarType.dateTime, - ), - ], - indexes: [], - ), - converter: IsarObjectConverter( - serialize: serializeImage, - deserialize: deserializeImage, - deserializeProperty: deserializeImageProp, - ), - embeddedSchemas: [], -); - -@isarProtected -int serializeImage(IsarWriter writer, Image object) { - { - final value = object.thumbnailPath; - if (value == null) { - IsarCore.writeNull(writer, 1); - } else { - IsarCore.writeString(writer, 1, value); - } - } - { - final value = object.imagePath; - if (value == null) { - IsarCore.writeNull(writer, 2); - } else { - IsarCore.writeString(writer, 2, value); - } - } - IsarCore.writeLong( - writer, 3, object.lastSaved.toUtc().microsecondsSinceEpoch); - return object.id; -} - -@isarProtected -Image deserializeImage(IsarReader reader) { - final int _id; - _id = IsarCore.readId(reader); - final String? _thumbnailPath; - _thumbnailPath = IsarCore.readString(reader, 1); - final String? _imagePath; - _imagePath = IsarCore.readString(reader, 2); - final object = Image( - id: _id, - thumbnailPath: _thumbnailPath, - imagePath: _imagePath, - ); - { - final value = IsarCore.readLong(reader, 3); - if (value == -9223372036854775808) { - object.lastSaved = - DateTime.fromMillisecondsSinceEpoch(0, isUtc: true).toLocal(); - } else { - object.lastSaved = - DateTime.fromMicrosecondsSinceEpoch(value, isUtc: true).toLocal(); - } - } - return object; -} - -@isarProtected -dynamic deserializeImageProp(IsarReader reader, int property) { - switch (property) { - case 0: - return IsarCore.readId(reader); - case 1: - return IsarCore.readString(reader, 1); - case 2: - return IsarCore.readString(reader, 2); - case 3: - { - final value = IsarCore.readLong(reader, 3); - if (value == -9223372036854775808) { - return DateTime.fromMillisecondsSinceEpoch(0, isUtc: true).toLocal(); - } else { - return DateTime.fromMicrosecondsSinceEpoch(value, isUtc: true) - .toLocal(); - } - } - default: - throw ArgumentError('Unknown property: $property'); - } -} - -sealed class _ImageUpdate { - bool call({ - required int id, - String? thumbnailPath, - String? imagePath, - DateTime? lastSaved, - }); -} - -class _ImageUpdateImpl implements _ImageUpdate { - const _ImageUpdateImpl(this.collection); - - final IsarCollection collection; - - @override - bool call({ - required int id, - Object? thumbnailPath = ignore, - Object? imagePath = ignore, - Object? lastSaved = ignore, - }) { - return collection.updateProperties([ - id - ], { - if (thumbnailPath != ignore) 1: thumbnailPath as String?, - if (imagePath != ignore) 2: imagePath as String?, - if (lastSaved != ignore) 3: lastSaved as DateTime?, - }) > - 0; - } -} - -sealed class _ImageUpdateAll { - int call({ - required List id, - String? thumbnailPath, - String? imagePath, - DateTime? lastSaved, - }); -} - -class _ImageUpdateAllImpl implements _ImageUpdateAll { - const _ImageUpdateAllImpl(this.collection); - - final IsarCollection collection; - - @override - int call({ - required List id, - Object? thumbnailPath = ignore, - Object? imagePath = ignore, - Object? lastSaved = ignore, - }) { - return collection.updateProperties(id, { - if (thumbnailPath != ignore) 1: thumbnailPath as String?, - if (imagePath != ignore) 2: imagePath as String?, - if (lastSaved != ignore) 3: lastSaved as DateTime?, - }); - } -} - -extension ImageUpdate on IsarCollection { - _ImageUpdate get update => _ImageUpdateImpl(this); - - _ImageUpdateAll get updateAll => _ImageUpdateAllImpl(this); -} - -sealed class _ImageQueryUpdate { - int call({ - String? thumbnailPath, - String? imagePath, - DateTime? lastSaved, - }); -} - -class _ImageQueryUpdateImpl implements _ImageQueryUpdate { - const _ImageQueryUpdateImpl(this.query, {this.limit}); - - final IsarQuery query; - final int? limit; - - @override - int call({ - Object? thumbnailPath = ignore, - Object? imagePath = ignore, - Object? lastSaved = ignore, - }) { - return query.updateProperties(limit: limit, { - if (thumbnailPath != ignore) 1: thumbnailPath as String?, - if (imagePath != ignore) 2: imagePath as String?, - if (lastSaved != ignore) 3: lastSaved as DateTime?, - }); - } -} - -extension ImageQueryUpdate on IsarQuery { - _ImageQueryUpdate get updateFirst => _ImageQueryUpdateImpl(this, limit: 1); - - _ImageQueryUpdate get updateAll => _ImageQueryUpdateImpl(this); -} - -class _ImageQueryBuilderUpdateImpl implements _ImageQueryUpdate { - const _ImageQueryBuilderUpdateImpl(this.query, {this.limit}); - - final QueryBuilder query; - final int? limit; - - @override - int call({ - Object? thumbnailPath = ignore, - Object? imagePath = ignore, - Object? lastSaved = ignore, - }) { - final q = query.build(); - try { - return q.updateProperties(limit: limit, { - if (thumbnailPath != ignore) 1: thumbnailPath as String?, - if (imagePath != ignore) 2: imagePath as String?, - if (lastSaved != ignore) 3: lastSaved as DateTime?, - }); - } finally { - q.close(); - } - } -} - -extension ImageQueryBuilderUpdate on QueryBuilder { - _ImageQueryUpdate get updateFirst => - _ImageQueryBuilderUpdateImpl(this, limit: 1); - - _ImageQueryUpdate get updateAll => _ImageQueryBuilderUpdateImpl(this); -} - -extension ImageQueryFilter on QueryBuilder { - QueryBuilder idEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 0, - value: value, - ), - ); - }); - } - - QueryBuilder idGreaterThan( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 0, - value: value, - ), - ); - }); - } - - QueryBuilder idGreaterThanOrEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 0, - value: value, - ), - ); - }); - } - - QueryBuilder idLessThan( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 0, - value: value, - ), - ); - }); - } - - QueryBuilder idLessThanOrEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 0, - value: value, - ), - ); - }); - } - - QueryBuilder idBetween( - int lower, - int upper, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 0, - lower: lower, - upper: upper, - ), - ); - }); - } - - QueryBuilder thumbnailPathIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const IsNullCondition(property: 1)); - }); - } - - QueryBuilder thumbnailPathIsNotNull() { - return QueryBuilder.apply(not(), (query) { - return query.addFilterCondition(const IsNullCondition(property: 1)); - }); - } - - QueryBuilder thumbnailPathEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbnailPathGreaterThan( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - thumbnailPathGreaterThanOrEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbnailPathLessThan( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - thumbnailPathLessThanOrEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbnailPathBetween( - String? lower, - String? upper, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 1, - lower: lower, - upper: upper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbnailPathStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - StartsWithCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbnailPathEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EndsWithCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbnailPathContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - ContainsCondition( - property: 1, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbnailPathMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - MatchesCondition( - property: 1, - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder thumbnailPathIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const EqualCondition( - property: 1, - value: '', - ), - ); - }); - } - - QueryBuilder thumbnailPathIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const GreaterCondition( - property: 1, - value: '', - ), - ); - }); - } - - QueryBuilder imagePathIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const IsNullCondition(property: 2)); - }); - } - - QueryBuilder imagePathIsNotNull() { - return QueryBuilder.apply(not(), (query) { - return query.addFilterCondition(const IsNullCondition(property: 2)); - }); - } - - QueryBuilder imagePathEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder imagePathGreaterThan( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder - imagePathGreaterThanOrEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder imagePathLessThan( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder imagePathLessThanOrEqualTo( - String? value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder imagePathBetween( - String? lower, - String? upper, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 2, - lower: lower, - upper: upper, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder imagePathStartsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - StartsWithCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder imagePathEndsWith( - String value, { - bool caseSensitive = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EndsWithCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder imagePathContains( - String value, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - ContainsCondition( - property: 2, - value: value, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder imagePathMatches( - String pattern, - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - MatchesCondition( - property: 2, - wildcard: pattern, - caseSensitive: caseSensitive, - ), - ); - }); - } - - QueryBuilder imagePathIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const EqualCondition( - property: 2, - value: '', - ), - ); - }); - } - - QueryBuilder imagePathIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - const GreaterCondition( - property: 2, - value: '', - ), - ); - }); - } - - QueryBuilder lastSavedEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 3, - value: value, - ), - ); - }); - } - - QueryBuilder lastSavedGreaterThan( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 3, - value: value, - ), - ); - }); - } - - QueryBuilder - lastSavedGreaterThanOrEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 3, - value: value, - ), - ); - }); - } - - QueryBuilder lastSavedLessThan( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 3, - value: value, - ), - ); - }); - } - - QueryBuilder lastSavedLessThanOrEqualTo( - DateTime value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 3, - value: value, - ), - ); - }); - } - - QueryBuilder lastSavedBetween( - DateTime lower, - DateTime upper, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 3, - lower: lower, - upper: upper, - ), - ); - }); - } -} - -extension ImageQueryObject on QueryBuilder {} - -extension ImageQuerySortBy on QueryBuilder { - QueryBuilder sortById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(0); - }); - } - - QueryBuilder sortByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(0, sort: Sort.desc); - }); - } - - QueryBuilder sortByThumbnailPath( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 1, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortByThumbnailPathDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 1, - sort: Sort.desc, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortByImagePath( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 2, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortByImagePathDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy( - 2, - sort: Sort.desc, - caseSensitive: caseSensitive, - ); - }); - } - - QueryBuilder sortByLastSaved() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(3); - }); - } - - QueryBuilder sortByLastSavedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(3, sort: Sort.desc); - }); - } -} - -extension ImageQuerySortThenBy on QueryBuilder { - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(0); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(0, sort: Sort.desc); - }); - } - - QueryBuilder thenByThumbnailPath( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(1, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenByThumbnailPathDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(1, sort: Sort.desc, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenByImagePath( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(2, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenByImagePathDesc( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(2, sort: Sort.desc, caseSensitive: caseSensitive); - }); - } - - QueryBuilder thenByLastSaved() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(3); - }); - } - - QueryBuilder thenByLastSavedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(3, sort: Sort.desc); - }); - } -} - -extension ImageQueryWhereDistinct on QueryBuilder { - QueryBuilder distinctByThumbnailPath( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(1, caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByImagePath( - {bool caseSensitive = true}) { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(2, caseSensitive: caseSensitive); - }); - } - - QueryBuilder distinctByLastSaved() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(3); - }); - } -} - -extension ImageQueryProperty1 on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(0); - }); - } - - QueryBuilder thumbnailPathProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(1); - }); - } - - QueryBuilder imagePathProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(2); - }); - } - - QueryBuilder lastSavedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(3); - }); - } -} - -extension ImageQueryProperty2 on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(0); - }); - } - - QueryBuilder thumbnailPathProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(1); - }); - } - - QueryBuilder imagePathProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(2); - }); - } - - QueryBuilder lastSavedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(3); - }); - } -} - -extension ImageQueryProperty3 - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(0); - }); - } - - QueryBuilder thumbnailPathProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(1); - }); - } - - QueryBuilder imagePathProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(2); - }); - } - - QueryBuilder lastSavedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(3); - }); - } -} diff --git a/lib/db/init.dart b/lib/db/init.dart index ad875db..280144e 100644 --- a/lib/db/init.dart +++ b/lib/db/init.dart @@ -1,28 +1,23 @@ -// does the initial setup of the storage - import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:hive/hive.dart'; +import 'package:hive_plus_secure/hive_plus_secure.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; +import 'package:vaani/main.dart'; import 'package:vaani/settings/constants.dart'; import 'register_models.dart'; +// does the initial setup of the storage Future initStorage() async { final dir = await getApplicationDocumentsDirectory(); // use vaani as the directory for hive - final storageDir = Directory(p.join( - dir.path, - AppMetadata.appNameLowerCase, - ), - ); + final storageDir = Directory(p.join(dir.path, AppMetadata.appNameLowerCase)); await storageDir.create(recursive: true); Hive.defaultDirectory = storageDir.path; - debugPrint('Hive storage directory init: ${Hive.defaultDirectory}'); + appLogger.config('Hive storage directory init: ${Hive.defaultDirectory}'); await registerModels(); } diff --git a/lib/db/player_prefs/book_prefs.dart b/lib/db/player_prefs/book_prefs.dart index b4e3807..ca610c8 100644 --- a/lib/db/player_prefs/book_prefs.dart +++ b/lib/db/player_prefs/book_prefs.dart @@ -1,29 +1,29 @@ -// a table to track preferences of player for each book -import 'package:isar/isar.dart'; +// // a table to track preferences of player for each book +// import 'package:isar/isar.dart'; -part 'book_prefs.g.dart'; +// part 'book_prefs.g.dart'; -/// stores the preferences of the player for a book -@Collection() -@Name('BookPrefs') -class BookPrefs { - @Id() - int libItemId; +// /// stores the preferences of the player for a book +// @Collection() +// @Name('BookPrefs') +// class BookPrefs { +// @Id() +// int libItemId; - double? speed; - // double? volume; - // Duration? sleepTimer; - // bool? showTotalProgress; - // bool? showChapterProgress; - // bool? useChapterInfo; +// double? speed; +// // double? volume; +// // Duration? sleepTimer; +// // bool? showTotalProgress; +// // bool? showChapterProgress; +// // bool? useChapterInfo; - BookPrefs({ - required this.libItemId, - this.speed, - // this.volume, - // this.sleepTimer, - // this.showTotalProgress, - // this.showChapterProgress, - // this.useChapterInfo, - }); -} +// BookPrefs({ +// required this.libItemId, +// this.speed, +// // this.volume, +// // this.sleepTimer, +// // this.showTotalProgress, +// // this.showChapterProgress, +// // this.useChapterInfo, +// }); +// } diff --git a/lib/db/player_prefs/book_prefs.g.dart b/lib/db/player_prefs/book_prefs.g.dart deleted file mode 100644 index ee012a5..0000000 --- a/lib/db/player_prefs/book_prefs.g.dart +++ /dev/null @@ -1,496 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'book_prefs.dart'; - -// ************************************************************************** -// _IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, invalid_use_of_protected_member, lines_longer_than_80_chars, constant_identifier_names, avoid_js_rounded_ints, no_leading_underscores_for_local_identifiers, require_trailing_commas, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_in_if_null_operators, library_private_types_in_public_api, prefer_const_constructors -// ignore_for_file: type=lint - -extension GetBookPrefsCollection on Isar { - IsarCollection get bookPrefs => this.collection(); -} - -const BookPrefsSchema = IsarGeneratedSchema( - schema: IsarSchema( - name: 'BookPrefs', - idName: 'libItemId', - embedded: false, - properties: [ - IsarPropertySchema( - name: 'speed', - type: IsarType.double, - ), - ], - indexes: [], - ), - converter: IsarObjectConverter( - serialize: serializeBookPrefs, - deserialize: deserializeBookPrefs, - deserializeProperty: deserializeBookPrefsProp, - ), - embeddedSchemas: [], -); - -@isarProtected -int serializeBookPrefs(IsarWriter writer, BookPrefs object) { - IsarCore.writeDouble(writer, 1, object.speed ?? double.nan); - return object.libItemId; -} - -@isarProtected -BookPrefs deserializeBookPrefs(IsarReader reader) { - final int _libItemId; - _libItemId = IsarCore.readId(reader); - final double? _speed; - { - final value = IsarCore.readDouble(reader, 1); - if (value.isNaN) { - _speed = null; - } else { - _speed = value; - } - } - final object = BookPrefs( - libItemId: _libItemId, - speed: _speed, - ); - return object; -} - -@isarProtected -dynamic deserializeBookPrefsProp(IsarReader reader, int property) { - switch (property) { - case 0: - return IsarCore.readId(reader); - case 1: - { - final value = IsarCore.readDouble(reader, 1); - if (value.isNaN) { - return null; - } else { - return value; - } - } - default: - throw ArgumentError('Unknown property: $property'); - } -} - -sealed class _BookPrefsUpdate { - bool call({ - required int libItemId, - double? speed, - }); -} - -class _BookPrefsUpdateImpl implements _BookPrefsUpdate { - const _BookPrefsUpdateImpl(this.collection); - - final IsarCollection collection; - - @override - bool call({ - required int libItemId, - Object? speed = ignore, - }) { - return collection.updateProperties([ - libItemId - ], { - if (speed != ignore) 1: speed as double?, - }) > - 0; - } -} - -sealed class _BookPrefsUpdateAll { - int call({ - required List libItemId, - double? speed, - }); -} - -class _BookPrefsUpdateAllImpl implements _BookPrefsUpdateAll { - const _BookPrefsUpdateAllImpl(this.collection); - - final IsarCollection collection; - - @override - int call({ - required List libItemId, - Object? speed = ignore, - }) { - return collection.updateProperties(libItemId, { - if (speed != ignore) 1: speed as double?, - }); - } -} - -extension BookPrefsUpdate on IsarCollection { - _BookPrefsUpdate get update => _BookPrefsUpdateImpl(this); - - _BookPrefsUpdateAll get updateAll => _BookPrefsUpdateAllImpl(this); -} - -sealed class _BookPrefsQueryUpdate { - int call({ - double? speed, - }); -} - -class _BookPrefsQueryUpdateImpl implements _BookPrefsQueryUpdate { - const _BookPrefsQueryUpdateImpl(this.query, {this.limit}); - - final IsarQuery query; - final int? limit; - - @override - int call({ - Object? speed = ignore, - }) { - return query.updateProperties(limit: limit, { - if (speed != ignore) 1: speed as double?, - }); - } -} - -extension BookPrefsQueryUpdate on IsarQuery { - _BookPrefsQueryUpdate get updateFirst => - _BookPrefsQueryUpdateImpl(this, limit: 1); - - _BookPrefsQueryUpdate get updateAll => _BookPrefsQueryUpdateImpl(this); -} - -class _BookPrefsQueryBuilderUpdateImpl implements _BookPrefsQueryUpdate { - const _BookPrefsQueryBuilderUpdateImpl(this.query, {this.limit}); - - final QueryBuilder query; - final int? limit; - - @override - int call({ - Object? speed = ignore, - }) { - final q = query.build(); - try { - return q.updateProperties(limit: limit, { - if (speed != ignore) 1: speed as double?, - }); - } finally { - q.close(); - } - } -} - -extension BookPrefsQueryBuilderUpdate - on QueryBuilder { - _BookPrefsQueryUpdate get updateFirst => - _BookPrefsQueryBuilderUpdateImpl(this, limit: 1); - - _BookPrefsQueryUpdate get updateAll => _BookPrefsQueryBuilderUpdateImpl(this); -} - -extension BookPrefsQueryFilter - on QueryBuilder { - QueryBuilder libItemIdEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 0, - value: value, - ), - ); - }); - } - - QueryBuilder - libItemIdGreaterThan( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 0, - value: value, - ), - ); - }); - } - - QueryBuilder - libItemIdGreaterThanOrEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 0, - value: value, - ), - ); - }); - } - - QueryBuilder libItemIdLessThan( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 0, - value: value, - ), - ); - }); - } - - QueryBuilder - libItemIdLessThanOrEqualTo( - int value, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 0, - value: value, - ), - ); - }); - } - - QueryBuilder libItemIdBetween( - int lower, - int upper, - ) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 0, - lower: lower, - upper: upper, - ), - ); - }); - } - - QueryBuilder speedIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const IsNullCondition(property: 1)); - }); - } - - QueryBuilder speedIsNotNull() { - return QueryBuilder.apply(not(), (query) { - return query.addFilterCondition(const IsNullCondition(property: 1)); - }); - } - - QueryBuilder speedEqualTo( - double? value, { - double epsilon = Filter.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - EqualCondition( - property: 1, - value: value, - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder speedGreaterThan( - double? value, { - double epsilon = Filter.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterCondition( - property: 1, - value: value, - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - speedGreaterThanOrEqualTo( - double? value, { - double epsilon = Filter.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - GreaterOrEqualCondition( - property: 1, - value: value, - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder speedLessThan( - double? value, { - double epsilon = Filter.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessCondition( - property: 1, - value: value, - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder - speedLessThanOrEqualTo( - double? value, { - double epsilon = Filter.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - LessOrEqualCondition( - property: 1, - value: value, - epsilon: epsilon, - ), - ); - }); - } - - QueryBuilder speedBetween( - double? lower, - double? upper, { - double epsilon = Filter.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition( - BetweenCondition( - property: 1, - lower: lower, - upper: upper, - epsilon: epsilon, - ), - ); - }); - } -} - -extension BookPrefsQueryObject - on QueryBuilder {} - -extension BookPrefsQuerySortBy on QueryBuilder { - QueryBuilder sortByLibItemId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(0); - }); - } - - QueryBuilder sortByLibItemIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(0, sort: Sort.desc); - }); - } - - QueryBuilder sortBySpeed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(1); - }); - } - - QueryBuilder sortBySpeedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(1, sort: Sort.desc); - }); - } -} - -extension BookPrefsQuerySortThenBy - on QueryBuilder { - QueryBuilder thenByLibItemId() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(0); - }); - } - - QueryBuilder thenByLibItemIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(0, sort: Sort.desc); - }); - } - - QueryBuilder thenBySpeed() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(1); - }); - } - - QueryBuilder thenBySpeedDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(1, sort: Sort.desc); - }); - } -} - -extension BookPrefsQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctBySpeed() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(1); - }); - } -} - -extension BookPrefsQueryProperty1 - on QueryBuilder { - QueryBuilder libItemIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(0); - }); - } - - QueryBuilder speedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(1); - }); - } -} - -extension BookPrefsQueryProperty2 - on QueryBuilder { - QueryBuilder libItemIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(0); - }); - } - - QueryBuilder speedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(1); - }); - } -} - -extension BookPrefsQueryProperty3 - on QueryBuilder { - QueryBuilder libItemIdProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(0); - }); - } - - QueryBuilder speedProperty() { - return QueryBuilder.apply(this, (query) { - return query.addProperty(1); - }); - } -} diff --git a/lib/db/register_models.dart b/lib/db/register_models.dart index e185e33..8695ab1 100644 --- a/lib/db/register_models.dart +++ b/lib/db/register_models.dart @@ -1,4 +1,4 @@ -import 'package:hive/hive.dart'; +import 'package:hive_plus_secure/hive_plus_secure.dart'; import 'package:vaani/features/per_book_settings/models/book_settings.dart'; import 'package:vaani/settings/models/models.dart'; diff --git a/lib/features/downloads/core/download_manager.dart b/lib/features/downloads/core/download_manager.dart index 99f9f1c..e6db077 100644 --- a/lib/features/downloads/core/download_manager.dart +++ b/lib/features/downloads/core/download_manager.dart @@ -1,5 +1,6 @@ // download manager to handle download tasks of files +import 'dart:async'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; @@ -7,6 +8,7 @@ import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart'; final _logger = Logger('AudiobookDownloadManager'); final tq = MemoryTaskQueue(); @@ -34,10 +36,13 @@ class AudiobookDownloadManager { FileDownloader().addTaskQueue(tq); - _logger.fine('initialized with baseUrl: $baseUrl, token: $token'); + _logger.fine( + 'initialized with baseUrl: ${Uri.parse(baseUrl).obfuscate()} and token: ${token.obfuscate()}', + ); _logger.fine( 'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause', ); + initDownloadManager(); } // the base url for the audio files @@ -55,17 +60,20 @@ class AudiobookDownloadManager { // whether to allow pausing of downloads final bool allowPause; - // final List _downloadTasks = []; + final StreamController _taskStatusController = + StreamController.broadcast(); + + Stream get taskUpdateStream => _taskStatusController.stream; + + late StreamSubscription _updatesSubscription; Future queueAudioBookDownload(LibraryItemExpanded item) async { - _logger.info('queuing download for item: ${item.toJson()}'); + _logger.info('queuing download for item: ${item.id}'); // create a download task for each file in the item final directory = await getApplicationSupportDirectory(); for (final file in item.libraryFiles) { // check if the file is already downloaded - if (isFileDownloaded( - constructFilePath(directory, item, file), - )) { + if (isFileDownloaded(constructFilePath(directory, item, file))) { _logger.info('file already downloaded: ${file.metadata.filename}'); continue; } @@ -80,61 +88,56 @@ class AudiobookDownloadManager { allowPause: allowPause, group: item.id, baseDirectory: downloadDirectory, + updates: Updates.statusAndProgress, // metaData: token ); // _downloadTasks.add(task); tq.add(task); - _logger.info('queued task: ${task.toJson()}'); + _logger.info('queued task: ${task.taskId}'); } - - FileDownloader().registerCallbacks( - group: item.id, - taskProgressCallback: (update) { - _logger.info('Group: ${item.id}, Progress Update: ${update.progress}'); - }, - taskStatusCallback: (update) { - switch (update.status) { - case TaskStatus.complete: - _logger.info('Group: ${item.id}, Download Complete'); - break; - case TaskStatus.failed: - _logger.warning('Group: ${item.id}, Download Failed'); - break; - default: - _logger - .info('Group: ${item.id}, Download Status: ${update.status}'); - } - }, - taskNotificationTapCallback: (task, notificationType) { - _logger.info('Group: ${item.id}, Task: ${task.toJson()}'); - }, - ); } String constructFilePath( Directory directory, LibraryItemExpanded item, LibraryFile file, - ) => - '${directory.path}/${item.relPath}/${file.metadata.filename}'; - - // void startDownload() { - // for (final task in _downloadTasks) { - // _logger.fine('enqueuing task: $task'); - // FileDownloader().enqueue(task); - // } - // } + ) => '${directory.path}/${item.relPath}/${file.metadata.filename}'; void dispose() { - // tq.removeAll(); - _logger.fine('disposed'); + _updatesSubscription.cancel(); + FileDownloader().destroy(); + _logger.fine('Destroyed download manager'); + } + + bool isItemDownloading(String id) { + return tq.enqueued.any((task) => task.group == id); } bool isFileDownloaded(String filePath) { - // Check if the file exists - final fileExists = File(filePath).existsSync(); + return File(filePath).existsSync(); + } - return fileExists; + Future> getDownloadedFilesMetadata( + LibraryItemExpanded item, + ) async { + final directory = await getApplicationSupportDirectory(); + final downloadedFiles = []; + for (final file in item.libraryFiles) { + final filePath = constructFilePath(directory, item, file); + if (isFileDownloaded(filePath)) { + downloadedFiles.add(file); + } + } + + return downloadedFiles; + } + + Future getDownloadedSize(LibraryItemExpanded item) async { + final files = await getDownloadedFilesMetadata(item); + return files.fold( + 0, + (previousValue, element) => previousValue + element.metadata.size, + ); } Future isItemDownloaded(LibraryItemExpanded item) async { @@ -145,11 +148,12 @@ class AudiobookDownloadManager { return false; } } - + _logger.info('all files downloaded for item id: ${item.id}'); return true; } Future deleteDownloadedItem(LibraryItemExpanded item) async { + _logger.info('deleting downloaded item with id: ${item.id}'); final directory = await getApplicationSupportDirectory(); for (final file in item.libraryFiles) { final filePath = constructFilePath(directory, item, file); @@ -160,7 +164,7 @@ class AudiobookDownloadManager { } } - Future> getDownloadedFiles(LibraryItemExpanded item) async { + Future> getDownloadedFilesUri(LibraryItemExpanded item) async { final directory = await getApplicationSupportDirectory(); final files = []; for (final file in item.libraryFiles) { @@ -172,14 +176,28 @@ class AudiobookDownloadManager { return files; } -} -Future initDownloadManager() async { - // initialize the download manager - var fileDownloader = FileDownloader(); - fileDownloader.configureNotification( - running: const TaskNotification('Downloading', 'file: {filename}'), - progressBar: true, - ); - await fileDownloader.trackTasks(); + Future initDownloadManager() async { + // initialize the download manager + _logger.info('Initializing download manager'); + final fileDownloader = FileDownloader(); + + _logger.info('Configuring Notification'); + fileDownloader.configureNotification( + running: const TaskNotification('Downloading', 'file: {filename}'), + progressBar: true, + ); + + await fileDownloader.trackTasks(); + + try { + _updatesSubscription = fileDownloader.updates.listen((event) { + _logger.finer('Got event: $event'); + _taskStatusController.add(event); + }); + _logger.info('Listening to download manager updates'); + } catch (e) { + _logger.warning('Error when listening to download manager updates: $e'); + } + } } diff --git a/lib/features/downloads/providers/download_manager.dart b/lib/features/downloads/providers/download_manager.dart index 6a04a8f..7dba395 100644 --- a/lib/features/downloads/providers/download_manager.dart +++ b/lib/features/downloads/providers/download_manager.dart @@ -1,13 +1,18 @@ import 'package:background_downloader/background_downloader.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/features/downloads/core/download_manager.dart' - as core; +import 'package:vaani/api/library_item_provider.dart'; +import 'package:vaani/features/downloads/core/download_manager.dart' as core; import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/shared/extensions/item_files.dart'; part 'download_manager.g.dart'; +final _logger = Logger('AudiobookDownloadManagerProvider'); + @Riverpod(keepAlive: true) class SimpleDownloadManager extends _$SimpleDownloadManager { @override @@ -25,27 +30,107 @@ class SimpleDownloadManager extends _$SimpleDownloadManager { core.tq.maxConcurrent = downloadSettings.maxConcurrent; core.tq.maxConcurrentByHost = downloadSettings.maxConcurrentByHost; core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup; + ref.onDispose(() { + _logger.info('disposing download manager'); manager.dispose(); }); + _logger.config('initialized download manager'); return manager; } } +@Riverpod(keepAlive: true) +class DownloadManager extends _$DownloadManager { + @override + core.AudiobookDownloadManager build() { + final manager = ref.watch(simpleDownloadManagerProvider); + manager.taskUpdateStream.listen((_) { + ref.notifyListeners(); + }); + return manager; + } + + Future queueAudioBookDownload(LibraryItemExpanded item) async { + _logger.fine('queueing download for ${item.id}'); + await state.queueAudioBookDownload(item); + } + + Future deleteDownloadedItem(LibraryItemExpanded item) async { + _logger.fine('deleting downloaded item ${item.id}'); + await state.deleteDownloadedItem(item); + ref.notifyListeners(); + } +} + @riverpod -FutureOr> downloadHistory( - DownloadHistoryRef ref, { - String? group, -}) async { +class IsItemDownloading extends _$IsItemDownloading { + @override + bool build(String id) { + final manager = ref.watch(downloadManagerProvider); + return manager.isItemDownloading(id); + } +} + +@riverpod +class ItemDownloadProgress extends _$ItemDownloadProgress { + @override + Future build(String id) async { + final item = await ref.watch(libraryItemProvider(id).future); + final manager = ref.read(downloadManagerProvider); + manager.taskUpdateStream + .map((taskUpdate) { + if (taskUpdate is! TaskProgressUpdate) { + return null; + } + if (taskUpdate.task.group == id) { + return taskUpdate; + } + }) + .listen((task) async { + if (task != null) { + final totalSize = item.totalSize; + // if total size is 0, return 0 + if (totalSize == 0) { + state = const AsyncValue.data(0.0); + return; + } + final downloadedFiles = await manager.getDownloadedFilesMetadata( + item, + ); + // calculate total size of downloaded files and total size of item, then divide + // to get percentage + final downloadedSize = downloadedFiles.fold( + 0, + (previousValue, element) => previousValue + element.metadata.size, + ); + + final inProgressFileSize = task.progress * task.expectedFileSize; + final totalDownloadedSize = downloadedSize + inProgressFileSize; + final progress = totalDownloadedSize / totalSize; + // if current progress is more than calculated progress, do not update + if (progress < (state.value ?? 0.0)) { + return; + } + + state = AsyncValue.data(progress.clamp(0.0, 1.0)); + } + }); + return null; + } +} + +@riverpod +FutureOr> downloadHistory(Ref ref, {String? group}) async { return await FileDownloader().database.allRecords(group: group); } -@Riverpod(keepAlive: false) -FutureOr downloadStatus( - DownloadStatusRef ref, - LibraryItemExpanded item, -) async { - final manager = ref.read(simpleDownloadManagerProvider); - return manager.isItemDownloaded(item); +@riverpod +class IsItemDownloaded extends _$IsItemDownloaded { + @override + FutureOr build(LibraryItemExpanded item) { + final manager = ref.watch(downloadManagerProvider); + return manager.isItemDownloaded(item); + } } diff --git a/lib/features/downloads/providers/download_manager.g.dart b/lib/features/downloads/providers/download_manager.g.dart index 7a21240..0d0879e 100644 --- a/lib/features/downloads/providers/download_manager.g.dart +++ b/lib/features/downloads/providers/download_manager.g.dart @@ -6,301 +6,480 @@ part of 'download_manager.dart'; // RiverpodGenerator // ************************************************************************** -String _$downloadHistoryHash() => r'76c449e8abfa61d57566991686f534a06dc7fef7'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); +@ProviderFor(SimpleDownloadManager) +final simpleDownloadManagerProvider = SimpleDownloadManagerProvider._(); - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -/// See also [downloadHistory]. -@ProviderFor(downloadHistory) -const downloadHistoryProvider = DownloadHistoryFamily(); - -/// See also [downloadHistory]. -class DownloadHistoryFamily extends Family>> { - /// See also [downloadHistory]. - const DownloadHistoryFamily(); - - /// See also [downloadHistory]. - DownloadHistoryProvider call({ - String? group, - }) { - return DownloadHistoryProvider( - group: group, - ); - } - - @override - DownloadHistoryProvider getProviderOverride( - covariant DownloadHistoryProvider provider, - ) { - return call( - group: provider.group, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'downloadHistoryProvider'; -} - -/// See also [downloadHistory]. -class DownloadHistoryProvider - extends AutoDisposeFutureProvider> { - /// See also [downloadHistory]. - DownloadHistoryProvider({ - String? group, - }) : this._internal( - (ref) => downloadHistory( - ref as DownloadHistoryRef, - group: group, - ), - from: downloadHistoryProvider, - name: r'downloadHistoryProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$downloadHistoryHash, - dependencies: DownloadHistoryFamily._dependencies, - allTransitiveDependencies: - DownloadHistoryFamily._allTransitiveDependencies, - group: group, - ); - - DownloadHistoryProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.group, - }) : super.internal(); - - final String? group; - - @override - Override overrideWith( - FutureOr> Function(DownloadHistoryRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: DownloadHistoryProvider._internal( - (ref) => create(ref as DownloadHistoryRef), - from: from, - name: null, +final class SimpleDownloadManagerProvider + extends + $NotifierProvider< + SimpleDownloadManager, + core.AudiobookDownloadManager + > { + SimpleDownloadManagerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'simpleDownloadManagerProvider', + isAutoDispose: false, dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - group: group, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$simpleDownloadManagerHash(); + + @$internal + @override + SimpleDownloadManager create() => SimpleDownloadManager(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(core.AudiobookDownloadManager value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider( + value, ), ); } - - @override - AutoDisposeFutureProviderElement> createElement() { - return _DownloadHistoryProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is DownloadHistoryProvider && other.group == group; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, group.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin DownloadHistoryRef on AutoDisposeFutureProviderRef> { - /// The parameter `group` of this provider. - String? get group; -} - -class _DownloadHistoryProviderElement - extends AutoDisposeFutureProviderElement> - with DownloadHistoryRef { - _DownloadHistoryProviderElement(super.provider); - - @override - String? get group => (origin as DownloadHistoryProvider).group; -} - -String _$downloadStatusHash() => r'f37b4678d3c2a7c6e985b0149d72ea0f9b1b42ca'; - -/// See also [downloadStatus]. -@ProviderFor(downloadStatus) -const downloadStatusProvider = DownloadStatusFamily(); - -/// See also [downloadStatus]. -class DownloadStatusFamily extends Family> { - /// See also [downloadStatus]. - const DownloadStatusFamily(); - - /// See also [downloadStatus]. - DownloadStatusProvider call( - LibraryItemExpanded item, - ) { - return DownloadStatusProvider( - item, - ); - } - - @override - DownloadStatusProvider getProviderOverride( - covariant DownloadStatusProvider provider, - ) { - return call( - provider.item, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'downloadStatusProvider'; -} - -/// See also [downloadStatus]. -class DownloadStatusProvider extends AutoDisposeFutureProvider { - /// See also [downloadStatus]. - DownloadStatusProvider( - LibraryItemExpanded item, - ) : this._internal( - (ref) => downloadStatus( - ref as DownloadStatusRef, - item, - ), - from: downloadStatusProvider, - name: r'downloadStatusProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$downloadStatusHash, - dependencies: DownloadStatusFamily._dependencies, - allTransitiveDependencies: - DownloadStatusFamily._allTransitiveDependencies, - item: item, - ); - - DownloadStatusProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.item, - }) : super.internal(); - - final LibraryItemExpanded item; - - @override - Override overrideWith( - FutureOr Function(DownloadStatusRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: DownloadStatusProvider._internal( - (ref) => create(ref as DownloadStatusRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - item: item, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _DownloadStatusProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is DownloadStatusProvider && other.item == item; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, item.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin DownloadStatusRef on AutoDisposeFutureProviderRef { - /// The parameter `item` of this provider. - LibraryItemExpanded get item; -} - -class _DownloadStatusProviderElement - extends AutoDisposeFutureProviderElement with DownloadStatusRef { - _DownloadStatusProviderElement(super.provider); - - @override - LibraryItemExpanded get item => (origin as DownloadStatusProvider).item; } String _$simpleDownloadManagerHash() => - r'cec95717c86e422f88f78aa014d29e800e5a2089'; + r'8ab13f06ec5f2f73b73064bd285813dc890b7f36'; -/// See also [SimpleDownloadManager]. -@ProviderFor(SimpleDownloadManager) -final simpleDownloadManagerProvider = NotifierProvider.internal( - SimpleDownloadManager.new, - name: r'simpleDownloadManagerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$simpleDownloadManagerHash, - dependencies: null, - allTransitiveDependencies: null, -); +abstract class _$SimpleDownloadManager + extends $Notifier { + core.AudiobookDownloadManager build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref< + core.AudiobookDownloadManager, + core.AudiobookDownloadManager + >; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + core.AudiobookDownloadManager, + core.AudiobookDownloadManager + >, + core.AudiobookDownloadManager, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} -typedef _$SimpleDownloadManager = 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 +@ProviderFor(DownloadManager) +final downloadManagerProvider = DownloadManagerProvider._(); + +final class DownloadManagerProvider + extends $NotifierProvider { + DownloadManagerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'downloadManagerProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$downloadManagerHash(); + + @$internal + @override + DownloadManager create() => DownloadManager(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(core.AudiobookDownloadManager value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider( + value, + ), + ); + } +} + +String _$downloadManagerHash() => r'852012e32e613f86445afc7f7e4e85bec808e982'; + +abstract class _$DownloadManager + extends $Notifier { + core.AudiobookDownloadManager build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref< + core.AudiobookDownloadManager, + core.AudiobookDownloadManager + >; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + core.AudiobookDownloadManager, + core.AudiobookDownloadManager + >, + core.AudiobookDownloadManager, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} + +@ProviderFor(IsItemDownloading) +final isItemDownloadingProvider = IsItemDownloadingFamily._(); + +final class IsItemDownloadingProvider + extends $NotifierProvider { + IsItemDownloadingProvider._({ + required IsItemDownloadingFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'isItemDownloadingProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$isItemDownloadingHash(); + + @override + String toString() { + return r'isItemDownloadingProvider' + '' + '($argument)'; + } + + @$internal + @override + IsItemDownloading create() => IsItemDownloading(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(bool value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } + + @override + bool operator ==(Object other) { + return other is IsItemDownloadingProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$isItemDownloadingHash() => r'ea43c06393beec828134e08d5f896ddbcfbac8f0'; + +final class IsItemDownloadingFamily extends $Family + with $ClassFamilyOverride { + IsItemDownloadingFamily._() + : super( + retry: null, + name: r'isItemDownloadingProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + IsItemDownloadingProvider call(String id) => + IsItemDownloadingProvider._(argument: id, from: this); + + @override + String toString() => r'isItemDownloadingProvider'; +} + +abstract class _$IsItemDownloading extends $Notifier { + late final _$args = ref.$arg as String; + String get id => _$args; + + bool build(String id); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + bool, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} + +@ProviderFor(ItemDownloadProgress) +final itemDownloadProgressProvider = ItemDownloadProgressFamily._(); + +final class ItemDownloadProgressProvider + extends $AsyncNotifierProvider { + ItemDownloadProgressProvider._({ + required ItemDownloadProgressFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'itemDownloadProgressProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$itemDownloadProgressHash(); + + @override + String toString() { + return r'itemDownloadProgressProvider' + '' + '($argument)'; + } + + @$internal + @override + ItemDownloadProgress create() => ItemDownloadProgress(); + + @override + bool operator ==(Object other) { + return other is ItemDownloadProgressProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$itemDownloadProgressHash() => + r'95f6ec0945f73d9156bf89bdb1865f3b2c9ffcaa'; + +final class ItemDownloadProgressFamily extends $Family + with + $ClassFamilyOverride< + ItemDownloadProgress, + AsyncValue, + double?, + FutureOr, + String + > { + ItemDownloadProgressFamily._() + : super( + retry: null, + name: r'itemDownloadProgressProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + ItemDownloadProgressProvider call(String id) => + ItemDownloadProgressProvider._(argument: id, from: this); + + @override + String toString() => r'itemDownloadProgressProvider'; +} + +abstract class _$ItemDownloadProgress extends $AsyncNotifier { + late final _$args = ref.$arg as String; + String get id => _$args; + + FutureOr build(String id); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, double?>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, double?>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} + +@ProviderFor(downloadHistory) +final downloadHistoryProvider = DownloadHistoryFamily._(); + +final class DownloadHistoryProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with $FutureModifier>, $FutureProvider> { + DownloadHistoryProvider._({ + required DownloadHistoryFamily super.from, + required String? super.argument, + }) : super( + retry: null, + name: r'downloadHistoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$downloadHistoryHash(); + + @override + String toString() { + return r'downloadHistoryProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + final argument = this.argument as String?; + return downloadHistory(ref, group: argument); + } + + @override + bool operator ==(Object other) { + return other is DownloadHistoryProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$downloadHistoryHash() => r'4d8b84e30f7ff5ae69d23c8e03ff24af1234a1ad'; + +final class DownloadHistoryFamily extends $Family + with $FunctionalFamilyOverride>, String?> { + DownloadHistoryFamily._() + : super( + retry: null, + name: r'downloadHistoryProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + DownloadHistoryProvider call({String? group}) => + DownloadHistoryProvider._(argument: group, from: this); + + @override + String toString() => r'downloadHistoryProvider'; +} + +@ProviderFor(IsItemDownloaded) +final isItemDownloadedProvider = IsItemDownloadedFamily._(); + +final class IsItemDownloadedProvider + extends $AsyncNotifierProvider { + IsItemDownloadedProvider._({ + required IsItemDownloadedFamily super.from, + required LibraryItemExpanded super.argument, + }) : super( + retry: null, + name: r'isItemDownloadedProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$isItemDownloadedHash(); + + @override + String toString() { + return r'isItemDownloadedProvider' + '' + '($argument)'; + } + + @$internal + @override + IsItemDownloaded create() => IsItemDownloaded(); + + @override + bool operator ==(Object other) { + return other is IsItemDownloadedProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$isItemDownloadedHash() => r'9bb7ba28bdb73e1ba706e849fedc9c7bd67f4b67'; + +final class IsItemDownloadedFamily extends $Family + with + $ClassFamilyOverride< + IsItemDownloaded, + AsyncValue, + bool, + FutureOr, + LibraryItemExpanded + > { + IsItemDownloadedFamily._() + : super( + retry: null, + name: r'isItemDownloadedProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + IsItemDownloadedProvider call(LibraryItemExpanded item) => + IsItemDownloadedProvider._(argument: item, from: this); + + @override + String toString() => r'isItemDownloadedProvider'; +} + +abstract class _$IsItemDownloaded extends $AsyncNotifier { + late final _$args = ref.$arg as LibraryItemExpanded; + LibraryItemExpanded get item => _$args; + + FutureOr build(LibraryItemExpanded item); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, bool>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, bool>, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } +} diff --git a/lib/features/downloads/view/downloads_page.dart b/lib/features/downloads/view/downloads_page.dart index e8c0e9c..7d2b2cb 100644 --- a/lib/features/downloads/view/downloads_page.dart +++ b/lib/features/downloads/view/downloads_page.dart @@ -11,10 +11,7 @@ class DownloadsPage extends HookConsumerWidget { final downloadHistory = ref.watch(downloadHistoryProvider()); return Scaffold( - appBar: AppBar( - title: const Text('Downloads'), - backgroundColor: Colors.transparent, - ), + appBar: AppBar(title: const Text('Downloads')), body: Center( // history of downloads child: downloadHistory.when( diff --git a/lib/features/explore/providers/search_controller.g.dart b/lib/features/explore/providers/search_controller.g.dart index 4e6fa28..a44d078 100644 --- a/lib/features/explore/providers/search_controller.g.dart +++ b/lib/features/explore/providers/search_controller.g.dart @@ -6,24 +6,64 @@ part of 'search_controller.dart'; // RiverpodGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// The controller for the search bar. + +@ProviderFor(GlobalSearchController) +final globalSearchControllerProvider = GlobalSearchControllerProvider._(); + +/// The controller for the search bar. +final class GlobalSearchControllerProvider + extends $NotifierProvider> { + /// The controller for the search bar. + GlobalSearchControllerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'globalSearchControllerProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$globalSearchControllerHash(); + + @$internal + @override + GlobalSearchController create() => GlobalSearchController(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Raw value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + String _$globalSearchControllerHash() => r'd854ace6f2e00a10fc33aba63051375f82ad1b10'; /// The controller for the search bar. -/// -/// Copied from [GlobalSearchController]. -@ProviderFor(GlobalSearchController) -final globalSearchControllerProvider = - NotifierProvider>.internal( - GlobalSearchController.new, - name: r'globalSearchControllerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$globalSearchControllerHash, - dependencies: null, - allTransitiveDependencies: null, -); -typedef _$GlobalSearchController = 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 +abstract class _$GlobalSearchController + extends $Notifier> { + Raw build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, Raw>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, Raw>, + Raw, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/features/explore/providers/search_result_provider.dart b/lib/features/explore/providers/search_result_provider.dart index 1552a69..2c903f7 100644 --- a/lib/features/explore/providers/search_result_provider.dart +++ b/lib/features/explore/providers/search_result_provider.dart @@ -1,3 +1,4 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; @@ -8,7 +9,7 @@ part 'search_result_provider.g.dart'; /// The provider for the search result. @riverpod FutureOr searchResult( - SearchResultRef ref, + Ref ref, String query, { int limit = 25, }) async { diff --git a/lib/features/explore/providers/search_result_provider.g.dart b/lib/features/explore/providers/search_result_provider.g.dart index 20d8c40..51b077b 100644 --- a/lib/features/explore/providers/search_result_provider.g.dart +++ b/lib/features/explore/providers/search_result_provider.g.dart @@ -6,184 +6,94 @@ part of 'search_result_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$searchResultHash() => r'9baa643cce24f3a5e022f42202e423373939ef95'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning /// The provider for the search result. -/// -/// Copied from [searchResult]. + @ProviderFor(searchResult) -const searchResultProvider = SearchResultFamily(); +final searchResultProvider = SearchResultFamily._(); /// The provider for the search result. -/// -/// Copied from [searchResult]. -class SearchResultFamily extends Family> { - /// The provider for the search result. - /// - /// Copied from [searchResult]. - const SearchResultFamily(); +final class SearchResultProvider + extends + $FunctionalProvider< + AsyncValue, + LibrarySearchResponse?, + FutureOr + > + with + $FutureModifier, + $FutureProvider { /// The provider for the search result. - /// - /// Copied from [searchResult]. - SearchResultProvider call( - String query, { - int limit = 25, - }) { - return SearchResultProvider( - query, - limit: limit, - ); + SearchResultProvider._({ + required SearchResultFamily super.from, + required (String, {int limit}) super.argument, + }) : super( + retry: null, + name: r'searchResultProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$searchResultHash(); + + @override + String toString() { + return r'searchResultProvider' + '' + '$argument'; } + @$internal @override - SearchResultProvider getProviderOverride( - covariant SearchResultProvider provider, - ) { - return call( - provider.query, - limit: provider.limit, - ); - } - - static const Iterable? _dependencies = null; + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'searchResultProvider'; -} - -/// The provider for the search result. -/// -/// Copied from [searchResult]. -class SearchResultProvider - extends AutoDisposeFutureProvider { - /// The provider for the search result. - /// - /// Copied from [searchResult]. - SearchResultProvider( - String query, { - int limit = 25, - }) : this._internal( - (ref) => searchResult( - ref as SearchResultRef, - query, - limit: limit, - ), - from: searchResultProvider, - name: r'searchResultProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$searchResultHash, - dependencies: SearchResultFamily._dependencies, - allTransitiveDependencies: - SearchResultFamily._allTransitiveDependencies, - query: query, - limit: limit, - ); - - SearchResultProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.query, - required this.limit, - }) : super.internal(); - - final String query; - final int limit; - - @override - Override overrideWith( - FutureOr Function(SearchResultRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: SearchResultProvider._internal( - (ref) => create(ref as SearchResultRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - query: query, - limit: limit, - ), - ); - } - - @override - AutoDisposeFutureProviderElement createElement() { - return _SearchResultProviderElement(this); + FutureOr create(Ref ref) { + final argument = this.argument as (String, {int limit}); + return searchResult(ref, argument.$1, limit: argument.limit); } @override bool operator ==(Object other) { - return other is SearchResultProvider && - other.query == query && - other.limit == limit; + return other is SearchResultProvider && other.argument == argument; } @override int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, query.hashCode); - hash = _SystemHash.combine(hash, limit.hashCode); - - return _SystemHash.finish(hash); + return argument.hashCode; } } -mixin SearchResultRef on AutoDisposeFutureProviderRef { - /// The parameter `query` of this provider. - String get query; +String _$searchResultHash() => r'33785de298ad0d53c9d21e8fec88ba2f22f1363f'; - /// The parameter `limit` of this provider. - int get limit; -} +/// The provider for the search result. -class _SearchResultProviderElement - extends AutoDisposeFutureProviderElement - with SearchResultRef { - _SearchResultProviderElement(super.provider); +final class SearchResultFamily extends $Family + with + $FunctionalFamilyOverride< + FutureOr, + (String, {int limit}) + > { + SearchResultFamily._() + : super( + retry: null, + name: r'searchResultProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// The provider for the search result. + + SearchResultProvider call(String query, {int limit = 25}) => + SearchResultProvider._(argument: (query, limit: limit), from: this); @override - String get query => (origin as SearchResultProvider).query; - @override - int get limit => (origin as SearchResultProvider).limit; + String toString() => r'searchResultProvider'; } -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/features/explore/view/explore_page.dart b/lib/features/explore/view/explore_page.dart index e25db0e..e747d26 100644 --- a/lib/features/explore/view/explore_page.dart +++ b/lib/features/explore/view/explore_page.dart @@ -28,19 +28,14 @@ class ExplorePage extends HookConsumerWidget { final settings = ref.watch(appSettingsProvider); final api = ref.watch(authenticatedApiProvider); return Scaffold( - appBar: AppBar( - title: const Text('Explore'), - backgroundColor: Colors.transparent, - ), + appBar: AppBar(title: const Text('Explore')), body: const MySearchBar(), ); } } class MySearchBar extends HookConsumerWidget { - const MySearchBar({ - super.key, - }); + const MySearchBar({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -62,8 +57,11 @@ class MySearchBar extends HookConsumerWidget { currentQuery = query; // In a real application, there should be some error handling here. - final options = await api.libraries - .search(libraryId: settings.activeLibraryId!, query: query, limit: 3); + final options = await api.libraries.search( + libraryId: settings.activeLibraryId!, + query: query, + limit: 3, + ); // If another search happened after this one, throw away these options. if (currentQuery != query) { @@ -98,9 +96,10 @@ class MySearchBar extends HookConsumerWidget { // opacity: 0.5 for the hint text hintStyle: WidgetStatePropertyAll( Theme.of(context).textTheme.bodyMedium!.copyWith( - color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.5), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.5), + ), ), textInputAction: TextInputAction.search, onTapOutside: (_) { @@ -119,12 +118,7 @@ class MySearchBar extends HookConsumerWidget { ); }, viewOnSubmitted: (value) { - context.pushNamed( - Routes.search.name, - queryParameters: { - 'q': value, - }, - ); + context.pushNamed(Routes.search.name, queryParameters: {'q': value}); }, suggestionsBuilder: (context, controller) async { // check if the search controller is empty @@ -190,14 +184,12 @@ List buildBookSearchResult( SearchResultMiniSection( // title: 'Books', category: SearchResultCategory.books, - options: options.book.map( - (result) { - // convert result to a book object - final book = result.libraryItem.media.asBookExpanded; - final metadata = book.metadata.asBookMetadataExpanded; - return BookSearchResultMini(book: book, metadata: metadata); - }, - ), + options: options.book.map((result) { + // convert result to a book object + final book = result.libraryItem.media.asBookExpanded; + final metadata = book.metadata.asBookMetadataExpanded; + return BookSearchResultMini(book: book, metadata: metadata); + }), ), ); } @@ -206,11 +198,9 @@ List buildBookSearchResult( SearchResultMiniSection( // title: 'Authors', category: SearchResultCategory.authors, - options: options.authors.map( - (result) { - return ListTile(title: Text(result.name)); - }, - ), + options: options.authors.map((result) { + return ListTile(title: Text(result.name)); + }), ), ); } @@ -231,10 +221,10 @@ class BookSearchResultMini extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final item = ref.watch(libraryItemProvider(book.libraryItemId)).valueOrNull; + final item = ref.watch(libraryItemProvider(book.libraryItemId)).value; final image = item == null ? const AsyncValue.loading() - : ref.watch(coverImageProvider(item)); + : ref.watch(coverImageProvider(item.id)); return ListTile( leading: SizedBox( width: 50, @@ -244,10 +234,7 @@ class BookSearchResultMini extends HookConsumerWidget { child: ClipRRect( borderRadius: BorderRadius.circular(5), child: image.when( - data: (bytes) => Image.memory( - bytes, - fit: BoxFit.cover, - ), + data: (bytes) => Image.memory(bytes, fit: BoxFit.cover), loading: () => const BookCoverSkeleton(), error: (error, _) => const Icon(Icons.error), ), @@ -258,11 +245,7 @@ class BookSearchResultMini extends HookConsumerWidget { subtitle: Text( maxLines: 1, overflow: TextOverflow.ellipsis, - metadata.authors - .map( - (author) => author.name, - ) - .join(', '), + metadata.authors.map((author) => author.name).join(', '), ), onTap: () { // navigate to the book details page diff --git a/lib/features/explore/view/search_result_page.dart b/lib/features/explore/view/search_result_page.dart index 10a8a39..fef097e 100644 --- a/lib/features/explore/view/search_result_page.dart +++ b/lib/features/explore/view/search_result_page.dart @@ -5,13 +5,7 @@ import 'package:vaani/features/explore/providers/search_result_provider.dart'; import 'package:vaani/features/explore/view/explore_page.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; -enum SearchResultCategory { - books, - authors, - series, - tags, - narrators, -} +enum SearchResultCategory { books, authors, series, tags, narrators } class SearchResultPage extends HookConsumerWidget { const SearchResultPage({ @@ -41,9 +35,7 @@ class SearchResultPage extends HookConsumerWidget { body: results.when( data: (options) { if (options == null) { - return Container( - child: const Text('No data found'), - ); + return Container(child: const Text('No data found')); } if (options is BookLibrarySearchResponse) { if (category == null) { @@ -51,18 +43,15 @@ class SearchResultPage extends HookConsumerWidget { } return switch (category!) { SearchResultCategory.books => ListView.builder( - itemCount: options.book.length, - itemBuilder: (context, index) { - final book = - options.book[index].libraryItem.media.asBookExpanded; - final metadata = book.metadata.asBookMetadataExpanded; + itemCount: options.book.length, + itemBuilder: (context, index) { + final book = + options.book[index].libraryItem.media.asBookExpanded; + final metadata = book.metadata.asBookMetadataExpanded; - return BookSearchResultMini( - book: book, - metadata: metadata, - ); - }, - ), + return BookSearchResultMini(book: book, metadata: metadata); + }, + ), SearchResultCategory.authors => Container(), SearchResultCategory.series => Container(), SearchResultCategory.tags => Container(), @@ -71,12 +60,8 @@ class SearchResultPage extends HookConsumerWidget { } return null; }, - loading: () => const Center( - child: CircularProgressIndicator(), - ), - error: (error, stackTrace) => Center( - child: Text('Error: $error'), - ), + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center(child: Text('Error: $error')), ), ); } diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index f92d32d..50ef9c2 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -1,5 +1,6 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; @@ -8,7 +9,10 @@ import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/features/downloads/providers/download_manager.dart' show downloadHistoryProvider, - downloadStatusProvider, + downloadManagerProvider, + isItemDownloadedProvider, + isItemDownloadingProvider, + itemDownloadProgressProvider, simpleDownloadManagerProvider; import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; @@ -22,21 +26,17 @@ import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/utils.dart'; class LibraryItemActions extends HookConsumerWidget { - LibraryItemActions({ - super.key, - required this.item, - }) { - book = item.media.asBookExpanded; - } + const LibraryItemActions({super.key, required this.id}); + + final String id; - final shelfsdk.LibraryItemExpanded item; - late final shelfsdk.BookExpanded book; @override Widget build(BuildContext context, WidgetRef ref) { - final manager = ref.read(simpleDownloadManagerProvider); + final item = ref.watch(libraryItemProvider(id)).value; + if (item == null) { + return const SizedBox.shrink(); + } final downloadHistory = ref.watch(downloadHistoryProvider(group: item.id)); - final isItemDownloaded = ref.watch(downloadStatusProvider(item)); - final isBookPlaying = ref.watch(audiobookPlayerProvider).book != null; final apiSettings = ref.watch(apiSettingsProvider); return Padding( @@ -65,9 +65,7 @@ class LibraryItemActions extends HookConsumerWidget { // read list button IconButton( onPressed: () {}, - icon: const Icon( - Icons.playlist_add_rounded, - ), + icon: const Icon(Icons.playlist_add_rounded), ), // share button IconButton( @@ -76,8 +74,9 @@ class LibraryItemActions extends HookConsumerWidget { var currentServerUrl = apiSettings.activeServer!.serverUrl; if (!currentServerUrl.hasScheme) { - currentServerUrl = - Uri.https(currentServerUrl.toString()); + currentServerUrl = Uri.https( + currentServerUrl.toString(), + ); } handleLaunchUrl( Uri.parse( @@ -93,64 +92,9 @@ class LibraryItemActions extends HookConsumerWidget { }, icon: const Icon(Icons.share_rounded), ), - // check if the book is downloaded using manager.isItemDownloaded - isItemDownloaded.when( - data: (isDownloaded) { - if (isDownloaded) { - // already downloaded button - return IconButton( - onPressed: () { - appLogger - .fine('Pressed already downloaded button'); - // manager.openDownloadedFile(item); - // open popup menu to open or delete the file - showModalBottomSheet( - useRootNavigator: false, - context: context, - builder: (context) { - return Padding( - padding: EdgeInsets.only( - top: 8.0, - bottom: (isBookPlaying - ? playerMinHeight - : 0) + - 8, - ), - child: DownloadSheet( - item: item, - ), - ); - }, - ); - }, - icon: const Icon( - Icons.download_done_rounded, - ), - ); - } - // download button - return IconButton( - onPressed: () { - appLogger.fine('Pressed download button'); - manager.queueAudioBookDownload(item); - }, - icon: const Icon( - Icons.download_rounded, - ), - ); - }, - loading: () => const CircularProgressIndicator(), - error: (error, stackTrace) { - return IconButton( - onPressed: () { - appLogger.warning( - 'Error checking download status: $error', - ); - }, - icon: const Icon(Icons.error_rounded), - ); - }, - ), + // download button + LibItemDownloadButton(item: item), + // more button IconButton( onPressed: () { @@ -192,7 +136,8 @@ class LibraryItemActions extends HookConsumerWidget { .database .deleteRecordWithId( record - .task.taskId, + .task + .taskId, ); Navigator.pop(context); }, @@ -213,8 +158,8 @@ class LibraryItemActions extends HookConsumerWidget { // open the file location final didOpen = await FileDownloader().openFile( - task: record.task, - ); + task: record.task, + ); if (!didOpen) { appLogger.warning( @@ -234,16 +179,13 @@ class LibraryItemActions extends HookConsumerWidget { loading: () => const Center( child: CircularProgressIndicator(), ), - error: (error, stackTrace) => Center( - child: Text('Error: $error'), - ), + error: (error, stackTrace) => + Center(child: Text('Error: $error')), ); }, ); }, - icon: const Icon( - Icons.more_vert_rounded, - ), + icon: const Icon(Icons.more_vert_rounded), ), ], ), @@ -257,17 +199,118 @@ class LibraryItemActions extends HookConsumerWidget { } } -class DownloadSheet extends HookConsumerWidget { - const DownloadSheet({ - super.key, - required this.item, - }); +class LibItemDownloadButton extends HookConsumerWidget { + const LibItemDownloadButton({super.key, required this.item}); final shelfsdk.LibraryItemExpanded item; @override Widget build(BuildContext context, WidgetRef ref) { - final manager = ref.read(simpleDownloadManagerProvider); + final isItemDownloaded = ref.watch(isItemDownloadedProvider(item)); + if (isItemDownloaded.value ?? false) { + return AlreadyItemDownloadedButton(item: item); + } + final isItemDownloading = ref.watch(isItemDownloadingProvider(item.id)); + + return isItemDownloading + ? ItemCurrentlyInDownloadQueue(item: item) + : IconButton( + onPressed: () { + appLogger.fine('Pressed download button'); + + ref + .read(downloadManagerProvider.notifier) + .queueAudioBookDownload(item); + }, + icon: const Icon(Icons.download_rounded), + ); + } +} + +class ItemCurrentlyInDownloadQueue extends HookConsumerWidget { + const ItemCurrentlyInDownloadQueue({super.key, required this.item}); + + final shelfsdk.LibraryItemExpanded item; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final progress = ref + .watch(itemDownloadProgressProvider(item.id)) + .value + ?.clamp(0.05, 1.0); + + if (progress == 1) { + return AlreadyItemDownloadedButton(item: item); + } + + const shimmerDuration = Duration(milliseconds: 1000); + return Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator(value: progress, strokeWidth: 2), + const Icon( + Icons.download, + // color: Theme.of(context).progressIndicatorTheme.color, + ) + .animate(onPlay: (controller) => controller.repeat()) + .fade( + duration: shimmerDuration, + end: 1, + begin: 0.6, + curve: Curves.linearToEaseOut, + ) + .fade( + duration: shimmerDuration, + end: 0.7, + begin: 1, + curve: Curves.easeInToLinear, + ), + ], + ); + } +} + +class AlreadyItemDownloadedButton extends HookConsumerWidget { + const AlreadyItemDownloadedButton({super.key, required this.item}); + + final shelfsdk.LibraryItemExpanded item; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isBookPlaying = ref.watch(audiobookPlayerProvider).book != null; + + return IconButton( + onPressed: () { + appLogger.fine('Pressed already downloaded button'); + // manager.openDownloadedFile(item); + // open popup menu to open or delete the file + showModalBottomSheet( + useRootNavigator: false, + context: context, + builder: (context) { + return Padding( + padding: EdgeInsets.only( + top: 8.0, + bottom: (isBookPlaying ? playerMinHeight : 0) + 8, + ), + child: DownloadSheet(item: item), + ); + }, + ); + }, + icon: const Icon(Icons.download_done_rounded), + ); + } +} + +class DownloadSheet extends HookConsumerWidget { + const DownloadSheet({super.key, required this.item}); + + final shelfsdk.LibraryItemExpanded item; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final manager = ref.watch(downloadManagerProvider); return Column( mainAxisSize: MainAxisSize.min, @@ -293,12 +336,10 @@ class DownloadSheet extends HookConsumerWidget { // ), ListTile( title: const Text('Delete'), - leading: const Icon( - Icons.delete_rounded, - ), - onTap: () { + leading: const Icon(Icons.delete_rounded), + onTap: () async { // show the delete dialog - showDialog( + final wasDeleted = await showDialog( useRootNavigator: false, context: context, builder: (context) { @@ -311,16 +352,16 @@ class DownloadSheet extends HookConsumerWidget { TextButton( onPressed: () { // delete the file - manager.deleteDownloadedItem( - item, - ); - GoRouter.of(context).pop(); + ref + .read(downloadManagerProvider.notifier) + .deleteDownloadedItem(item); + GoRouter.of(context).pop(true); }, child: const Text('Yes'), ), TextButton( onPressed: () { - GoRouter.of(context).pop(); + GoRouter.of(context).pop(false); }, child: const Text('No'), ), @@ -328,6 +369,14 @@ class DownloadSheet extends HookConsumerWidget { ); }, ); + + if (wasDeleted ?? false) { + appLogger.fine('Deleted ${item.media.metadata.title}'); + GoRouter.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Deleted ${item.media.metadata.title}')), + ); + } }, ), ], @@ -336,10 +385,7 @@ class DownloadSheet extends HookConsumerWidget { } class _LibraryItemPlayButton extends HookConsumerWidget { - const _LibraryItemPlayButton({ - super.key, - required this.item, - }); + const _LibraryItemPlayButton({required this.item}); final shelfsdk.LibraryItemExpanded item; @@ -390,9 +436,7 @@ class _LibraryItemPlayButton extends HookConsumerWidget { ), label: Text(getPlayDisplayText()), style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), ), ); } @@ -415,11 +459,11 @@ class DynamicItemPlayIcon extends StatelessWidget { return Icon( isCurrentBookSetInPlayer ? isPlayingThisBook - ? Icons.pause_rounded - : Icons.play_arrow_rounded + ? Icons.pause_rounded + : Icons.play_arrow_rounded : isBookCompleted - ? Icons.replay_rounded - : Icons.play_arrow_rounded, + ? Icons.replay_rounded + : Icons.play_arrow_rounded, ); } } @@ -430,7 +474,7 @@ Future libraryItemPlayButtonOnPressed({ required shelfsdk.BookExpanded book, shelfsdk.MediaProgress? userMediaProgress, }) async { - debugPrint('Pressed play/resume button'); + appLogger.info('Pressed play/resume button'); final player = ref.watch(audiobookPlayerProvider); final isCurrentBookSetInPlayer = player.book == book; @@ -439,28 +483,30 @@ Future libraryItemPlayButtonOnPressed({ Future? setSourceFuture; // set the book to the player if not already set if (!isCurrentBookSetInPlayer) { - debugPrint('Setting the book ${book.libraryItemId}'); - debugPrint('Initial position: ${userMediaProgress?.currentTime}'); + appLogger.info('Setting the book ${book.libraryItemId}'); + appLogger.info('Initial position: ${userMediaProgress?.currentTime}'); final downloadManager = ref.watch(simpleDownloadManagerProvider); - final libItem = - await ref.read(libraryItemProvider(book.libraryItemId).future); - final downloadedUris = await downloadManager.getDownloadedFiles(libItem); + final libItem = await ref.read( + libraryItemProvider(book.libraryItemId).future, + ); + final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); setSourceFuture = player.setSourceAudiobook( book, initialPosition: userMediaProgress?.currentTime, downloadedUris: downloadedUris, ); } else { - debugPrint('Book was already set'); + appLogger.info('Book was already set'); if (isPlayingThisBook) { - debugPrint('Pausing the book'); + appLogger.info('Pausing the book'); await player.pause(); return; } } // set the volume as this is the first time playing and dismissing causes the volume to go to 0 - var bookPlayerSettings = - ref.read(bookSettingsProvider(book.libraryItemId)).playerSettings; + var bookPlayerSettings = ref + .read(bookSettingsProvider(book.libraryItemId)) + .playerSettings; var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; var configurePlayerForEveryBook = @@ -472,14 +518,14 @@ Future libraryItemPlayButtonOnPressed({ player.setVolume( configurePlayerForEveryBook ? bookPlayerSettings.preferredDefaultVolume ?? - appPlayerSettings.preferredDefaultVolume + appPlayerSettings.preferredDefaultVolume : appPlayerSettings.preferredDefaultVolume, ), // set the speed player.setSpeed( configurePlayerForEveryBook ? bookPlayerSettings.preferredDefaultSpeed ?? - appPlayerSettings.preferredDefaultSpeed + appPlayerSettings.preferredDefaultSpeed : appPlayerSettings.preferredDefaultSpeed, ), ]); diff --git a/lib/features/item_viewer/view/library_item_hero_section.dart b/lib/features/item_viewer/view/library_item_hero_section.dart index 88ef7b0..a103b7a 100644 --- a/lib/features/item_viewer/view/library_item_hero_section.dart +++ b/lib/features/item_viewer/view/library_item_hero_section.dart @@ -5,6 +5,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; import 'package:vaani/api/image_provider.dart'; +import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; @@ -14,141 +15,128 @@ import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/duration_format.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; +import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; class LibraryItemHeroSection extends HookConsumerWidget { const LibraryItemHeroSection({ super.key, required this.itemId, required this.extraMap, - required this.providedCacheImage, - required this.item, - required this.itemBookMetadata, - required this.bookDetailsCached, - required this.coverColorScheme, }); final String itemId; final LibraryItemExtras? extraMap; - final Image? providedCacheImage; - final AsyncValue item; - final shelfsdk.BookMetadataExpanded? itemBookMetadata; - final shelfsdk.BookMinified? bookDetailsCached; - final AsyncValue coverColorScheme; @override Widget build(BuildContext context, WidgetRef ref) { return SliverToBoxAdapter( - child: LayoutBuilder( - builder: (context, constraints) { - return Container( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - // book cover - LayoutBuilder( - builder: (context, constraints) { - return SizedBox( - width: calculateWidth(context, constraints), - child: Column( - children: [ - Hero( - tag: HeroTagPrefixes.bookCover + - itemId + - (extraMap?.heroTagSuffix ?? ''), - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: _BookCover( - itemId: itemId, - extraMap: extraMap, - providedCacheImage: providedCacheImage, - coverColorScheme: coverColorScheme.valueOrNull, - item: item, - ), - ), - ), - // a progress bar if available - item.when( - data: (libraryItem) { - return Padding( - padding: const EdgeInsets.only( - top: 8.0, - right: 8.0, - left: 8.0, - ), - child: _LibraryItemProgressIndicator( - libraryItem: libraryItem, - ), - ); - }, - loading: () => const SizedBox.shrink(), - error: (error, stack) => const SizedBox.shrink(), - ), - ], + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // book cover + LayoutBuilder( + builder: (context, constraints) { + return SizedBox( + width: calculateWidth(context, constraints), + child: Column( + children: [ + Hero( + tag: + HeroTagPrefixes.bookCover + + itemId + + (extraMap?.heroTagSuffix ?? ''), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: _BookCover(itemId: itemId), ), - ); - }, - ), - // book details - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _BookTitle( - extraMap: extraMap, - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - ), - Container( - margin: const EdgeInsets.symmetric(vertical: 16), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - // authors info if available - _BookAuthors( - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - ), - // narrators info if available - _BookNarrators( - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - ), - // series info if available - _BookSeries( - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - ), - ], - ), - ), - ], ), - ), + // a progress bar + Padding( + padding: const EdgeInsets.only( + top: 8.0, + right: 8.0, + left: 8.0, + ), + child: _LibraryItemProgressIndicator(id: itemId), + ), + ], ), - ], + ); + }, + ), + // book details + _BookDetails(id: itemId, extraMap: extraMap), + ], + ), + ); + } +} + +class _BookDetails extends HookConsumerWidget { + const _BookDetails({required this.id, this.extraMap}); + + final String id; + final LibraryItemExtras? extraMap; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final itemFromApi = ref.watch(libraryItemProvider(id)); + + final itemBookMetadata = + itemFromApi.value?.media.metadata.asBookMetadataExpanded; + + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _BookTitle(extraMap: extraMap, itemBookMetadata: itemBookMetadata), + Container( + margin: const EdgeInsets.symmetric(vertical: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // authors info if available + _BookAuthors( + itemBookMetadata: itemBookMetadata, + bookDetailsCached: extraMap?.book, + ), + // narrators info if available + _BookNarrators( + itemBookMetadata: itemBookMetadata, + bookDetailsCached: extraMap?.book, + ), + // series info if available + _BookSeries( + itemBookMetadata: itemBookMetadata, + bookDetailsCached: extraMap?.book, + ), + ], + ), ), - ); - }, + ], + ), ), ); } } class _LibraryItemProgressIndicator extends HookConsumerWidget { - const _LibraryItemProgressIndicator({ - super.key, - required this.libraryItem, - }); + const _LibraryItemProgressIndicator({required this.id}); - final shelfsdk.LibraryItemExpanded libraryItem; + final String id; @override Widget build(BuildContext context, WidgetRef ref) { final player = ref.watch(audiobookPlayerProvider); + final libraryItem = ref.watch(libraryItemProvider(id)).value; + if (libraryItem == null) { + return const SizedBox.shrink(); + } + final mediaProgress = libraryItem.userMediaProgress; if (mediaProgress == null && player.book?.libraryItemId != libraryItem.id) { return const SizedBox.shrink(); @@ -158,13 +146,15 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget { Duration remainingTime; if (player.book?.libraryItemId == libraryItem.id) { // final positionStream = useStream(player.slowPositionStream); - progress = (player.positionInBook).inSeconds / + progress = + (player.positionInBook).inSeconds / libraryItem.media.asBookExpanded.duration.inSeconds; remainingTime = libraryItem.media.asBookExpanded.duration - player.positionInBook; } else { progress = mediaProgress?.progress ?? 0; - remainingTime = (libraryItem.media.asBookExpanded.duration - + remainingTime = + (libraryItem.media.asBookExpanded.duration - mediaProgress!.currentTime); } @@ -188,19 +178,20 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget { LinearProgressIndicator( value: progress.clamp(0.03, 1), borderRadius: BorderRadius.circular(8), + semanticsLabel: 'Book progress', + semanticsValue: '${progressInPercent.toStringAsFixed(2)}%', ), - const SizedBox.square( - dimension: 4.0, - ), + const SizedBox.square(dimension: 4.0), // time remaining Text( // only show 2 decimal places '${remainingTime.smartBinaryFormat} left', style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: - Theme.of(context).colorScheme.onSurface.withOpacity(0.75), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.75), + ), ), ], ), @@ -209,11 +200,7 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget { } class _HeroSectionSubLabelWithIcon extends HookConsumerWidget { - const _HeroSectionSubLabelWithIcon({ - super.key, - required this.icon, - required this.text, - }); + const _HeroSectionSubLabelWithIcon({required this.icon, required this.text}); final IconData icon; final Widget text; @@ -223,11 +210,13 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget { final themeData = Theme.of(context); final useFontAwesome = icon.runtimeType == FontAwesomeIcons.book.runtimeType; - final useMaterialThemeOnItemPage = - ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage; + final useMaterialThemeOnItemPage = ref + .watch(appSettingsProvider) + .themeSettings + .useMaterialThemeOnItemPage; final color = useMaterialThemeOnItemPage ? themeData.colorScheme.primary - : themeData.colorScheme.onSurface.withOpacity(0.75); + : themeData.colorScheme.onSurface.withValues(alpha: 0.75); return Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Row( @@ -235,20 +224,10 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget { Container( margin: const EdgeInsets.only(right: 8, top: 2), child: useFontAwesome - ? FaIcon( - icon, - size: 16, - color: color, - ) - : Icon( - icon, - size: 16, - color: color, - ), - ), - Expanded( - child: text, + ? FaIcon(icon, size: 16, color: color) + : Icon(icon, size: 16, color: color), ), + Expanded(child: text), ], ), ); @@ -257,7 +236,6 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget { class _BookSeries extends StatelessWidget { const _BookSeries({ - super.key, required this.itemBookMetadata, required this.bookDetailsCached, }); @@ -303,7 +281,6 @@ class _BookSeries extends StatelessWidget { class _BookNarrators extends StatelessWidget { const _BookNarrators({ - super.key, required this.itemBookMetadata, required this.bookDetailsCached, }); @@ -338,98 +315,77 @@ class _BookNarrators extends StatelessWidget { } class _BookCover extends HookConsumerWidget { - const _BookCover({ - super.key, - required this.itemId, - required this.extraMap, - required this.providedCacheImage, - required this.item, - this.coverColorScheme, - }); + const _BookCover({required this.itemId}); final String itemId; - final LibraryItemExtras? extraMap; - final Image? providedCacheImage; - final AsyncValue item; - final ColorScheme? coverColorScheme; @override Widget build(BuildContext context, WidgetRef ref) { + final coverImage = ref.watch(coverImageProvider(itemId)); final themeData = Theme.of(context); - final useMaterialThemeOnItemPage = - ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage; + // final item = ref.watch(libraryItemProvider(itemId)); + final themeSettings = ref.watch(appSettingsProvider).themeSettings; + + ColorScheme? coverColorScheme; + if (themeSettings.useMaterialThemeOnItemPage) { + coverColorScheme = ref + .watch( + themeOfLibraryItemProvider( + itemId, + brightness: Theme.of(context).brightness, + highContrast: + themeSettings.highContrast || + MediaQuery.of(context).highContrast, + ), + ) + .value; + } return ThemeSwitcher( builder: (context) { // change theme after 2 seconds - if (useMaterialThemeOnItemPage) { + if (themeSettings.useMaterialThemeOnItemPage) { Future.delayed(150.ms, () { try { ThemeSwitcher.of(context).changeTheme( theme: coverColorScheme != null ? ThemeData.from( - colorScheme: coverColorScheme!, + colorScheme: coverColorScheme, textTheme: themeData.textTheme, ) : themeData, ); } catch (e) { - appLogger.shout('Error changing theme: $e'); + appLogger.severe('Error changing theme: $e'); } }); } - return providedCacheImage ?? - item.when( - data: (libraryItem) { - final coverImage = ref.watch(coverImageProvider(libraryItem)); - return Stack( - children: [ - coverImage.when( - data: (image) { - // return const BookCoverSkeleton(); - if (image.isEmpty) { - return const Icon(Icons.error); - } - // cover 80% of parent height - return Image.memory( - image, - fit: BoxFit.cover, - // cacheWidth: (height * - // MediaQuery.of(context).devicePixelRatio) - // .round(), - ); - }, - loading: () { - return const Center( - child: BookCoverSkeleton(), - ); - }, - error: (error, stack) { - return const Icon(Icons.error); - }, - ), - ], - ); - }, - error: (error, stack) => const Icon(Icons.error), - loading: () => const Center(child: BookCoverSkeleton()), - ); + return coverImage.when( + data: (image) { + // return const BookCoverSkeleton(); + if (image.isEmpty) { + return const Icon(Icons.error); + } + + return Image.memory(image, fit: BoxFit.cover); + }, + loading: () { + return const Center(child: BookCoverSkeleton()); + }, + error: (error, stack) { + return const Center(child: Icon(Icons.error)); + }, + ); }, ); } } class _BookTitle extends StatelessWidget { - const _BookTitle({ - super.key, - required this.extraMap, - required this.itemBookMetadata, - required this.bookDetailsCached, - }); + const _BookTitle({required this.extraMap, required this.itemBookMetadata}); final LibraryItemExtras? extraMap; final shelfsdk.BookMetadataExpanded? itemBookMetadata; - final shelfsdk.BookMinified? bookDetailsCached; @override Widget build(BuildContext context) { @@ -438,7 +394,8 @@ class _BookTitle extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Hero( - tag: HeroTagPrefixes.bookTitle + + tag: + HeroTagPrefixes.bookTitle + // itemId + (extraMap?.heroTagSuffix ?? ''), child: Text( @@ -449,7 +406,7 @@ class _BookTitle extends StatelessWidget { // pauseBetween: 150.ms, // numberOfReps: 3, style: themeData.textTheme.headlineLarge, - itemBookMetadata?.title ?? bookDetailsCached?.metadata.title ?? '', + itemBookMetadata?.title ?? extraMap?.book?.metadata.title ?? '', ), ), // subtitle if available @@ -457,7 +414,7 @@ class _BookTitle extends StatelessWidget { ? const SizedBox.shrink() : Text( style: themeData.textTheme.titleSmall?.copyWith( - color: themeData.colorScheme.onSurface.withOpacity(0.8), + color: themeData.colorScheme.onSurface.withValues(alpha: 0.8), ), itemBookMetadata?.subtitle ?? '', ), @@ -468,7 +425,6 @@ class _BookTitle extends StatelessWidget { class _BookAuthors extends StatelessWidget { const _BookAuthors({ - super.key, required this.itemBookMetadata, required this.bookDetailsCached, }); @@ -482,7 +438,7 @@ class _BookAuthors extends StatelessWidget { String generateAuthorsString() { final authors = (itemBookMetadata)?.authors ?? []; if (authors.isEmpty) { - return (bookDetailsCached?.metadata as shelfsdk.BookMetadataMinified?) + return (bookDetailsCached?.metadata.asBookMetadataMinified) ?.authorName ?? ''; } diff --git a/lib/features/item_viewer/view/library_item_metadata.dart b/lib/features/item_viewer/view/library_item_metadata.dart index 1eef261..e12a997 100644 --- a/lib/features/item_viewer/view/library_item_metadata.dart +++ b/lib/features/item_viewer/view/library_item_metadata.dart @@ -1,20 +1,62 @@ import 'package:flutter/material.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/api/library_item_provider.dart'; +import 'package:vaani/shared/extensions/model_conversions.dart'; -class LibraryItemMetadata extends StatelessWidget { - const LibraryItemMetadata({ - super.key, - required this.item, - this.itemBookMetadata, - this.bookDetailsCached, - }); +class LibraryItemMetadata extends HookConsumerWidget { + const LibraryItemMetadata({super.key, required this.id}); - final shelfsdk.LibraryItemExpanded item; - final shelfsdk.BookMetadataExpanded? itemBookMetadata; - final shelfsdk.BookMinified? bookDetailsCached; + final String id; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final item = ref.watch(libraryItemProvider(id)).value; + + /// formats the duration of the book as `10h 30m` + /// + /// will add up all the durations of the audio files first + /// then convert them to the given format + String? getDurationFormatted() { + final book = (item?.media.asBookExpanded); + if (book == null) { + return null; + } + final duration = book.audioFiles + .map((e) => e.duration) + .reduce((value, element) => value + element); + final hours = duration.inHours; + final minutes = duration.inMinutes.remainder(60); + return '${hours}h ${minutes}m'; + } + + /// will return the size of the book in MB + /// + /// will add up all the sizes of the audio files first + /// then convert them to MB + String? getSizeFormatted() { + final book = (item?.media.asBookExpanded); + if (book == null) { + return null; + } + final size = book.audioFiles + .map((e) => e.metadata.size) + .reduce((value, element) => value + element); + return '${size / 1024 ~/ 1024} MB'; + } + + /// will return the codec and bitrate of the book + String? getCodecAndBitrate() { + final book = (item?.media.asBookExpanded); + if (book == null) { + return null; + } + final codec = book.audioFiles.first.codec.toUpperCase(); + // final bitrate = book.audioFiles.first.bitRate; + return codec; + } + + final itemBookMetadata = item?.media.metadata.asBookMetadataExpanded; + final children = [ // duration of the book _MetadataItem( @@ -27,7 +69,8 @@ class LibraryItemMetadata extends StatelessWidget { ), _MetadataItem( title: 'Published', - value: itemBookMetadata?.publishedDate ?? + value: + itemBookMetadata?.publishedDate ?? itemBookMetadata?.publishedYear ?? 'Unknown', ), @@ -42,75 +85,27 @@ class LibraryItemMetadata extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, // alternate between metadata and vertical divider - children: List.generate( - children.length * 2 - 1, - (index) { - if (index.isEven) { - return children[index ~/ 2]; - } - return VerticalDivider( - indent: 6, - endIndent: 6, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), - ); - }, - ), + children: List.generate(children.length * 2 - 1, (index) { + if (index.isEven) { + return children[index ~/ 2]; + } + return VerticalDivider( + indent: 6, + endIndent: 6, + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ); + }), ), ), ); } - - /// formats the duration of the book as `10h 30m` - /// - /// will add up all the durations of the audio files first - /// then convert them to the given format - String? getDurationFormatted() { - final book = (item.media as shelfsdk.BookExpanded?); - if (book == null) { - return null; - } - final duration = book.audioFiles - .map((e) => e.duration) - .reduce((value, element) => value + element); - final hours = duration.inHours; - final minutes = duration.inMinutes.remainder(60); - return '${hours}h ${minutes}m'; - } - - /// will return the size of the book in MB - /// - /// will add up all the sizes of the audio files first - /// then convert them to MB - String? getSizeFormatted() { - final book = (item.media as shelfsdk.BookExpanded?); - if (book == null) { - return null; - } - final size = book.audioFiles - .map((e) => e.metadata.size) - .reduce((value, element) => value + element); - return '${size / 1024 ~/ 1024} MB'; - } - - /// will return the codec and bitrate of the book - String? getCodecAndBitrate() { - final book = (item.media as shelfsdk.BookExpanded?); - if (book == null) { - return null; - } - final codec = book.audioFiles.first.codec.toUpperCase(); - // final bitrate = book.audioFiles.first.bitRate; - return codec; - } } /// key-value pair to display as column class _MetadataItem extends StatelessWidget { - const _MetadataItem({ - super.key, - required this.title, - required this.value, - }); + const _MetadataItem({required this.title, required this.value}); final String title; final String value; @@ -124,7 +119,7 @@ class _MetadataItem extends StatelessWidget { children: [ Text( style: themeData.textTheme.titleMedium?.copyWith( - color: themeData.colorScheme.onSurface.withOpacity(0.90), + color: themeData.colorScheme.onSurface.withValues(alpha: 0.90), ), value, maxLines: 1, @@ -132,7 +127,7 @@ class _MetadataItem extends StatelessWidget { ), Text( style: themeData.textTheme.bodySmall?.copyWith( - color: themeData.colorScheme.onSurface.withOpacity(0.7), + color: themeData.colorScheme.onSurface.withValues(alpha: 0.7), ), title, maxLines: 1, diff --git a/lib/features/item_viewer/view/library_item_page.dart b/lib/features/item_viewer/view/library_item_page.dart index d682f4a..6974bc6 100644 --- a/lib/features/item_viewer/view/library_item_page.dart +++ b/lib/features/item_viewer/view/library_item_page.dart @@ -3,109 +3,114 @@ import 'dart:math'; import 'package:animated_theme_switcher/animated_theme_switcher.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/features/item_viewer/view/library_item_sliver_app_bar.dart'; -import 'package:vaani/features/player/providers/player_form.dart'; +import 'package:vaani/features/player/view/mini_player_bottom_padding.dart'; import 'package:vaani/router/models/library_item_extras.dart'; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/expandable_description.dart'; -import 'package:vaani/theme/theme_from_cover_provider.dart'; import 'library_item_actions.dart'; import 'library_item_hero_section.dart'; import 'library_item_metadata.dart'; class LibraryItemPage extends HookConsumerWidget { - const LibraryItemPage({ - super.key, - required this.itemId, - this.extra, - }); + const LibraryItemPage({super.key, required this.itemId, this.extra}); final String itemId; final Object? extra; + static const double _showFabThreshold = 300.0; @override Widget build(BuildContext context, WidgetRef ref) { - final extraMap = - extra is LibraryItemExtras ? extra as LibraryItemExtras : null; - final bookDetailsCached = extraMap?.book; - final providedCacheImage = extraMap?.coverImage != null - ? Image.memory(extraMap!.coverImage!) + final additionalItemData = extra is LibraryItemExtras + ? extra as LibraryItemExtras : null; + final scrollController = useScrollController(); + final showFab = useState(false); - final itemFromApi = ref.watch(libraryItemProvider(itemId)); + // Effect to listen to scroll changes and update FAB visibility + useEffect(() { + void listener() { + if (!scrollController.hasClients) { + return; // Ensure controller is attached + } + final shouldShow = scrollController.offset > _showFabThreshold; + // Update state only if it changes and widget is still mounted + if (showFab.value != shouldShow && context.mounted) { + showFab.value = shouldShow; + } + } - var itemBookMetadata = - itemFromApi.valueOrNull?.media.metadata.asBookMetadataExpanded; + scrollController.addListener(listener); + // Initial check in case the view starts scrolled (less likely but safe) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients && context.mounted) { + listener(); + } + }); - final useMaterialThemeOnItemPage = - ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage; - AsyncValue coverColorScheme = const AsyncValue.loading(); - if (useMaterialThemeOnItemPage) { - coverColorScheme = ref.watch( - themeOfLibraryItemProvider( - itemFromApi.valueOrNull, - brightness: Theme.of(context).brightness, - ), - ); - debugPrint('ColorScheme: ${coverColorScheme.valueOrNull}'); - } else { - debugPrint('useMaterialThemeOnItemPage is false'); - // AsyncValue coverColorScheme = const AsyncValue.loading(); + // Cleanup: remove the listener when the widget is disposed + return () => scrollController.removeListener(listener); + }, [scrollController]); // Re-run effect if scrollController changes + + // --- FAB Scroll-to-Top Logic --- + void scrollToTop() { + if (scrollController.hasClients) { + scrollController.animateTo( + 0.0, // Target offset (top) + duration: 300.ms, + curve: Curves.easeInOut, + ); + } } + return ThemeProvider( initTheme: Theme.of(context), duration: 200.ms, child: ThemeSwitchingArea( child: Scaffold( + floatingActionButton: AnimatedSwitcher( + duration: 250.ms, + // A common transition for FABs (fade + scale) + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, + child: FadeTransition(opacity: animation, child: child), + ); + }, + child: showFab.value + ? FloatingActionButton( + // Key is important for AnimatedSwitcher to differentiate + key: const ValueKey('fab-scroll-top'), + onPressed: scrollToTop, + tooltip: 'Scroll to top', + child: const Icon(Icons.arrow_upward), + ) + : const SizedBox.shrink(key: ValueKey('fab-empty')), + ), body: CustomScrollView( + controller: scrollController, slivers: [ - const LibraryItemSliverAppBar(), + LibraryItemSliverAppBar( + id: itemId, + scrollController: scrollController, + ), SliverPadding( padding: const EdgeInsets.all(8), sliver: LibraryItemHeroSection( itemId: itemId, - extraMap: extraMap, - providedCacheImage: providedCacheImage, - item: itemFromApi, - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - coverColorScheme: coverColorScheme, + extraMap: additionalItemData, ), ), // a horizontal display with dividers of metadata - SliverToBoxAdapter( - child: itemFromApi.valueOrNull != null - ? LibraryItemMetadata( - item: itemFromApi.valueOrNull!, - itemBookMetadata: itemBookMetadata, - bookDetailsCached: bookDetailsCached, - ) - : const SizedBox.shrink(), - ), + SliverToBoxAdapter(child: LibraryItemMetadata(id: itemId)), // a row of actions like play, download, share, etc - SliverToBoxAdapter( - child: itemFromApi.valueOrNull != null - ? LibraryItemActions(item: itemFromApi.valueOrNull!) - : const SizedBox.shrink(), - ), + SliverToBoxAdapter(child: LibraryItemActions(id: itemId)), // a expandable section for book description - SliverToBoxAdapter( - child: - itemFromApi.valueOrNull?.media.metadata.description != null - ? ExpandableDescription( - title: 'About the Book', - content: itemFromApi - .valueOrNull!.media.metadata.description!, - ) - : const SizedBox.shrink(), - ), + SliverToBoxAdapter(child: LibraryItemDescription(id: itemId)), // a padding at the bottom to make sure the last item is not hidden by mini player - const SliverToBoxAdapter( - child: SizedBox(height: playerMinHeight), - ), + const SliverToBoxAdapter(child: MiniPlayerBottomPadding()), ], ), ), @@ -114,20 +119,42 @@ class LibraryItemPage extends HookConsumerWidget { } } +class LibraryItemDescription extends HookConsumerWidget { + const LibraryItemDescription({super.key, required this.id}); + + final String id; + @override + Widget build(BuildContext context, WidgetRef ref) { + final item = ref.watch(libraryItemProvider(id)).value; + if (item == null) { + return const SizedBox(); + } + return ExpandableDescription( + title: 'About the Book', + content: item.media.metadata.description ?? 'Sorry, no description found', + ); + } +} + /// Calculate the width of the book cover based on the screen size double calculateWidth( BuildContext context, BoxConstraints constraints, { + /// width ratio of the cover image to the available width double widthRatio = 0.4, /// height ratio of the cover image to the available height double maxHeightToUse = 0.25, }) { - final availHeight = - min(constraints.maxHeight, MediaQuery.of(context).size.height); - final availWidth = - min(constraints.maxWidth, MediaQuery.of(context).size.width); + final availHeight = min( + constraints.maxHeight, + MediaQuery.of(context).size.height, + ); + final availWidth = min( + constraints.maxWidth, + MediaQuery.of(context).size.width, + ); // make the width widthRatio of the available width var width = availWidth * widthRatio; diff --git a/lib/features/item_viewer/view/library_item_sliver_app_bar.dart b/lib/features/item_viewer/view/library_item_sliver_app_bar.dart index 99068e2..5090160 100644 --- a/lib/features/item_viewer/view/library_item_sliver_app_bar.dart +++ b/lib/features/item_viewer/view/library_item_sliver_app_bar.dart @@ -1,23 +1,78 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider; -class LibraryItemSliverAppBar extends StatelessWidget { +class LibraryItemSliverAppBar extends HookConsumerWidget { const LibraryItemSliverAppBar({ super.key, + required this.id, + required this.scrollController, }); + final String id; + final ScrollController scrollController; + + static const double _showTitleThreshold = kToolbarHeight * 0.5; + @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final item = ref.watch(libraryItemProvider(id)).value; + + final showTitle = useState(false); + + useEffect(() { + void listener() { + final shouldShow = + scrollController.hasClients && + scrollController.offset > _showTitleThreshold; + if (showTitle.value != shouldShow) { + showTitle.value = shouldShow; + } + } + + scrollController.addListener(listener); + // Trigger listener once initially in case the view starts scrolled + // (though unlikely for this specific use case, it's good practice) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients) { + listener(); + } + }); + return () => scrollController.removeListener(listener); + }, [scrollController]); + return SliverAppBar( - backgroundColor: Colors.transparent, elevation: 0, - floating: true, + floating: false, + pinned: true, primary: true, - snap: true, actions: [ - // cast button - IconButton(onPressed: () {}, icon: const Icon(Icons.cast)), - IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)), + // IconButton( + // icon: const Icon(Icons.cast), + // onPressed: () { + // // Handle search action + // }, + // ), ], + title: AnimatedSwitcher( + duration: const Duration(milliseconds: 150), + child: showTitle.value + ? Text( + // Use a Key to help AnimatedSwitcher differentiate widgets + key: const ValueKey('title-text'), + item?.media.metadata.title ?? '', + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyMedium, + ) + : const SizedBox( + // Also give it a key for differentiation + key: ValueKey('empty-title'), + width: 0, // Ensure it takes no space if possible + height: 0, + ), + ), + centerTitle: false, ); } } diff --git a/lib/features/library_browser/view/library_browser_page.dart b/lib/features/library_browser/view/library_browser_page.dart index d12de57..aa03fa8 100644 --- a/lib/features/library_browser/view/library_browser_page.dart +++ b/lib/features/library_browser/view/library_browser_page.dart @@ -1,47 +1,81 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/router/router.dart'; +import 'package:vaani/api/library_provider.dart' show currentLibraryProvider; +import 'package:vaani/features/you/view/widgets/library_switch_chip.dart' + show showLibrarySwitcher; +import 'package:vaani/router/router.dart' show Routes; +import 'package:vaani/shared/icons/abs_icons.dart' show AbsIcons; +import 'package:vaani/shared/widgets/not_implemented.dart' + show showNotImplementedToast; class LibraryBrowserPage extends HookConsumerWidget { const LibraryBrowserPage({super.key}); - @override Widget build(BuildContext context, WidgetRef ref) { + final currentLibrary = ref.watch(currentLibraryProvider).value; + + // Determine the icon to use, with a fallback + final IconData libraryIconData = + AbsIcons.getIconByName(currentLibrary?.icon) ?? Icons.library_books; + + // Determine the title text + final String appBarTitle = '${currentLibrary?.name ?? 'Your'} Library'; + return Scaffold( - appBar: AppBar( - title: const Text('Library'), - backgroundColor: Colors.transparent, - ), - // a list redirecting to authors, genres, and series pages - body: ListView( - children: [ - ListTile( - title: const Text('Authors'), - leading: const Icon(Icons.person), - trailing: const Icon(Icons.chevron_right), - onTap: () {}, + // Use CustomScrollView to enable slivers + body: CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + // floating: true, // Optional: uncomment if you want floating behavior + // snap: + // true, // Optional: uncomment if you want snapping behavior (usually with floating: true) + leading: IconButton( + icon: Icon(libraryIconData), + tooltip: 'Switch Library', // Helpful tooltip for users + onPressed: () { + showLibrarySwitcher(context, ref); + }, + ), + title: Text(appBarTitle), ), - ListTile( - title: const Text('Genres'), - leading: const Icon(Icons.category), - trailing: const Icon(Icons.chevron_right), - onTap: () {}, - ), - ListTile( - title: const Text('Series'), - leading: const Icon(Icons.list), - trailing: const Icon(Icons.chevron_right), - onTap: () {}, - ), - // Downloads - ListTile( - title: const Text('Downloads'), - leading: const Icon(Icons.download), - trailing: const Icon(Icons.chevron_right), - onTap: () { - GoRouter.of(context).pushNamed(Routes.downloads.name); - }, + SliverList( + delegate: SliverChildListDelegate([ + ListTile( + title: const Text('Authors'), + leading: const Icon(Icons.person), + trailing: const Icon(Icons.chevron_right), + onTap: () { + showNotImplementedToast(context); + }, + ), + ListTile( + title: const Text('Genres'), + leading: const Icon(Icons.category), + trailing: const Icon(Icons.chevron_right), + onTap: () { + showNotImplementedToast(context); + }, + ), + ListTile( + title: const Text('Series'), + leading: const Icon(Icons.list), + trailing: const Icon(Icons.chevron_right), + onTap: () { + showNotImplementedToast(context); + }, + ), + // Downloads + ListTile( + title: const Text('Downloads'), + leading: const Icon(Icons.download), + trailing: const Icon(Icons.chevron_right), + onTap: () { + GoRouter.of(context).pushNamed(Routes.downloads.name); + }, + ), + ]), ), ], ), diff --git a/lib/features/logging/core/logger.dart b/lib/features/logging/core/logger.dart new file mode 100644 index 0000000..52eb099 --- /dev/null +++ b/lib/features/logging/core/logger.dart @@ -0,0 +1,36 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; +import 'package:logging_appenders/logging_appenders.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:vaani/shared/extensions/duration_format.dart'; + +Future getLoggingFilePath() async { + final Directory directory = await getApplicationDocumentsDirectory(); + return '${directory.path}/vaani.log'; +} + +Future initLogging() async { + final formatter = const DefaultLogRecordFormatter(); + if (kReleaseMode) { + Logger.root.level = Level.INFO; // is also the default + // Write to a file + RotatingFileAppender( + baseFilePath: await getLoggingFilePath(), + formatter: formatter, + ).attachToLogger(Logger.root); + } else { + Logger.root.level = Level.FINE; // Capture all logs + RotatingFileAppender( + baseFilePath: await getLoggingFilePath(), + formatter: formatter, + ).attachToLogger(Logger.root); + Logger.root.onRecord.listen((record) { + // Print log records to the console + debugPrint( + '${record.loggerName}: ${record.level.name}: ${record.time.time}: ${record.message}', + ); + }); + } +} diff --git a/lib/features/logging/providers/logs_provider.dart b/lib/features/logging/providers/logs_provider.dart new file mode 100644 index 0000000..763fa90 --- /dev/null +++ b/lib/features/logging/providers/logs_provider.dart @@ -0,0 +1,90 @@ +import 'dart:io'; + +import 'package:archive/archive_io.dart'; +import 'package:flutter/foundation.dart'; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:vaani/features/logging/core/logger.dart'; +part 'logs_provider.g.dart'; + +@riverpod +class Logs extends _$Logs { + @override + Future> build() async { + final path = await getLoggingFilePath(); + final file = File(path); + if (!file.existsSync()) { + return []; + } + final lines = await file.readAsLines(); + return lines.map(parseLogLine).toList(); + } + + Future clear() async { + final path = await getLoggingFilePath(); + final file = File(path); + await file.writeAsString(''); + state = AsyncData([]); + } + + Future getZipFilePath() async { + final String targetZipPath = await generateZipFilePath(); + var encoder = ZipFileEncoder(); + encoder.create(targetZipPath); + final logFilePath = await getLoggingFilePath(); + final logFile = File(logFilePath); + if (await logFile.exists()) { + // Check if log file exists before adding + await encoder.addFile(logFile); + } else { + // Handle case where log file doesn't exist? Maybe log a warning? + // Or create an empty file inside the zip? For now, just don't add. + debugPrint( + 'Warning: Log file not found at $logFilePath, creating potentially empty zip.', + ); + } + await encoder.close(); + return targetZipPath; + } +} + +Future generateZipFilePath() async { + Directory appDocDirectory = await getTemporaryDirectory(); + return '${appDocDirectory.path}/${generateZipFileName()}'; +} + +String generateZipFileName() { + return 'vaani-${DateTime.now().microsecondsSinceEpoch}.zip'; +} + +Level parseLevel(String level) { + return Level.LEVELS.firstWhere( + (l) => l.name == level, + orElse: () => Level.ALL, + ); +} + +LogRecord parseLogLine(String line) { + // 2024-10-03 00:48:58.012400 INFO GoRouter - getting location for name: "logs" + + final RegExp logLineRegExp = RegExp( + r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) (\w+) (\w+) - (.+)', + ); + + final match = logLineRegExp.firstMatch(line); + if (match == null) { + // return as is + return LogRecord(Level.ALL, line, 'Unknown'); + } + + final timeString = match.group(1)!; + final levelString = match.group(2)!; + final loggerName = match.group(3)!; + final message = match.group(4)!; + + final time = DateTime.parse(timeString); + final level = parseLevel(levelString); + + return LogRecord(level, message, loggerName, time); +} diff --git a/lib/features/logging/providers/logs_provider.g.dart b/lib/features/logging/providers/logs_provider.g.dart new file mode 100644 index 0000000..c7bb6f8 --- /dev/null +++ b/lib/features/logging/providers/logs_provider.g.dart @@ -0,0 +1,53 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'logs_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(Logs) +final logsProvider = LogsProvider._(); + +final class LogsProvider extends $AsyncNotifierProvider> { + LogsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'logsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$logsHash(); + + @$internal + @override + Logs create() => Logs(); +} + +String _$logsHash() => r'aa9d3d56586cba6ddf69615320ea605d071ea5e2'; + +abstract class _$Logs extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/features/logging/view/logs_page.dart b/lib/features/logging/view/logs_page.dart new file mode 100644 index 0000000..978fd19 --- /dev/null +++ b/lib/features/logging/view/logs_page.dart @@ -0,0 +1,306 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:vaani/features/logging/providers/logs_provider.dart'; +import 'package:vaani/main.dart'; + +class LogsPage extends HookConsumerWidget { + const LogsPage({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + final logs = ref.watch(logsProvider); + final theme = Theme.of(context); + final scrollController = useScrollController(); + return Scaffold( + appBar: AppBar( + title: const Text('Logs'), + actions: [ + IconButton( + tooltip: 'Clear logs', + icon: const Icon(Icons.delete_forever), + onPressed: () async { + // ask for confirmation + final shouldClear = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Clear logs?'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: const Text('Clear'), + ), + ], + ); + }, + ); + if (shouldClear == true) { + ref.read(logsProvider.notifier).clear(); + } + }, + ), + IconButton( + tooltip: 'Share logs', + icon: const Icon(Icons.share), + onPressed: () async { + appLogger.info('Preparing logs for sharing'); + final zipLogFilePath = await ref + .read(logsProvider.notifier) + .getZipFilePath(); + + // submit logs + final result = await Share.shareXFiles([XFile(zipLogFilePath)]); + + switch (result.status) { + case ShareResultStatus.success: + appLogger.info('Share success'); + break; + case ShareResultStatus.dismissed: + appLogger.info('Share dismissed'); + break; + case ShareResultStatus.unavailable: + appLogger.severe('Share unavailable'); + break; + } + }, + ), + + // downloads disabled since manage external storage permission was removed + // see https://gitlab.com/IzzyOnDroid/repo/-/issues/623#note_2240386369 + // IconButton( + // tooltip: 'Download logs', + // icon: const Icon(Icons.download), + // onPressed: () async { + // appLogger.info('Preparing logs for download'); + + // if (Platform.isAndroid) { + // final androidVersion = + // await ref.watch(deviceSdkVersionProvider.future); + + // if ((int.parse(androidVersion)) > 29) { + // final status = await Permission.storage.status; + // if (!status.isGranted) { + // appLogger + // .info('Requesting storage permission'); + // final newStatus = + // await Permission.storage.request(); + // if (!newStatus.isGranted) { + // appLogger + // .warning('storage permission denied'); + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text('Storage permission denied'), + // ), + // ); + // return; + // } + // } + // } else { + // final status = await Permission.storage.status; + // if (!status.isGranted) { + // appLogger.info('Requesting storage permission'); + // final newStatus = await Permission.storage.request(); + // if (!newStatus.isGranted) { + // appLogger.warning('Storage permission denied'); + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text('Storage permission denied'), + // ), + // ); + // return; + // } + // } + // } + // } + // final zipLogFilePath = + // await ref.read(logsProvider.notifier).getZipFilePath(); + + // // save to folder + // String? outputFile = await FilePicker.platform.saveFile( + // dialogTitle: 'Please select an output file:', + // fileName: zipLogFilePath.split('/').last, + // bytes: await File(zipLogFilePath).readAsBytes(), + // ); + // if (outputFile != null) { + // try { + // final file = File(outputFile); + // final zipFile = File(zipLogFilePath); + // await zipFile.copy(file.path); + // appLogger.info('File saved to: $outputFile'); + // } catch (e) { + // appLogger.severe('Error saving file: $e'); + // } + // } else { + // appLogger.info('Download cancelled'); + // } + // }, + // ), + IconButton( + tooltip: 'Refresh logs', + icon: const Icon(Icons.refresh), + onPressed: () { + ref.invalidate(logsProvider); + }, + ), + IconButton( + tooltip: 'Scroll to top', + icon: const Icon(Icons.arrow_upward), + onPressed: () { + scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + }, + ), + ], + ), + // a column with ListView.builder and a scrollable list of logs + body: Column( + children: [ + // a filter for log levels, loggers, and search + // TODO: implement filters and search + Expanded( + child: logs.when( + data: (logRecords) { + return Scrollbar( + controller: scrollController, + child: ListView.builder( + controller: scrollController, + itemCount: logRecords.length, + itemBuilder: (context, index) { + final logRecord = logRecords[index]; + return LogRecordTile(logRecord: logRecord, theme: theme); + }, + ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stackTrace) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Error loading logs'), + ElevatedButton( + onPressed: () { + ref.invalidate(logsProvider); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class LogRecordTile extends StatelessWidget { + final LogRecord logRecord; + final ThemeData theme; + const LogRecordTile({ + required this.logRecord, + required this.theme, + super.key, + }); + @override + Widget build(BuildContext context) { + return ListTile( + leading: CircleAvatar( + backgroundColor: getLogLevelColor(logRecord.level, theme), + child: Icon( + getLogLevelIcon(logRecord.level), + color: getLogLevelTextColor(logRecord.level, theme), + ), + ), + title: Text(logRecord.loggerName), + subtitle: Text(logRecord.message), + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + icon: Icon(getLogLevelIcon(logRecord.level)), + title: Text(logRecord.loggerName), + content: Text.rich( + TextSpan( + children: [ + TextSpan( + text: logRecord.time.toIso8601String(), + style: const TextStyle(fontStyle: FontStyle.italic), + ), + const TextSpan(text: '\n\n'), + TextSpan(text: logRecord.message), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Close'), + ), + ], + ); + }, + ); + }, + ); + } +} + +IconData getLogLevelIcon(Level level) { + switch (level) { + case Level.INFO: + return (Icons.info); + case Level.WARNING: + return (Icons.warning); + case Level.SEVERE: + case Level.SHOUT: + return (Icons.error); + default: + return (Icons.bug_report); + } +} + +Color? getLogLevelColor(Level level, ThemeData theme) { + switch (level) { + case Level.INFO: + return theme.colorScheme.surfaceContainerLow; + case Level.WARNING: + return theme.colorScheme.surfaceBright; + case Level.SEVERE: + case Level.SHOUT: + return theme.colorScheme.errorContainer; + default: + return theme.colorScheme.primaryContainer; + } +} + +Color? getLogLevelTextColor(Level level, ThemeData theme) { + switch (level) { + case Level.INFO: + return theme.colorScheme.onSurface; + case Level.WARNING: + return theme.colorScheme.onSurface; + case Level.SEVERE: + case Level.SHOUT: + return theme.colorScheme.onErrorContainer; + default: + return theme.colorScheme.onPrimaryContainer; + } +} diff --git a/lib/features/onboarding/models/flow.dart b/lib/features/onboarding/models/flow.dart index 80e8514..46539f6 100644 --- a/lib/features/onboarding/models/flow.dart +++ b/lib/features/onboarding/models/flow.dart @@ -5,7 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'flow.freezed.dart'; @freezed -class Flow with _$Flow { +sealed class Flow with _$Flow { const factory Flow({ required Uri serverUri, required String state, diff --git a/lib/features/onboarding/models/flow.freezed.dart b/lib/features/onboarding/models/flow.freezed.dart index 1437f2e..f79c8bb 100644 --- a/lib/features/onboarding/models/flow.freezed.dart +++ b/lib/features/onboarding/models/flow.freezed.dart @@ -1,5 +1,5 @@ -// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file // 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 @@ -9,241 +9,272 @@ part of 'flow.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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 _$Flow { - Uri get serverUri => throw _privateConstructorUsedError; - String get state => throw _privateConstructorUsedError; - String get verifier => throw _privateConstructorUsedError; - Cookie get cookie => throw _privateConstructorUsedError; - bool get isFlowComplete => throw _privateConstructorUsedError; - String? get authToken => throw _privateConstructorUsedError; - /// Create a copy of Flow - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $FlowCopyWith get copyWith => throw _privateConstructorUsedError; + Uri get serverUri; String get state; String get verifier; Cookie get cookie; bool get isFlowComplete; String? get authToken; +/// Create a copy of Flow +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$FlowCopyWith get copyWith => _$FlowCopyWithImpl(this as Flow, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is Flow&&(identical(other.serverUri, serverUri) || other.serverUri == serverUri)&&(identical(other.state, state) || other.state == state)&&(identical(other.verifier, verifier) || other.verifier == verifier)&&(identical(other.cookie, cookie) || other.cookie == cookie)&&(identical(other.isFlowComplete, isFlowComplete) || other.isFlowComplete == isFlowComplete)&&(identical(other.authToken, authToken) || other.authToken == authToken)); +} + + +@override +int get hashCode => Object.hash(runtimeType,serverUri,state,verifier,cookie,isFlowComplete,authToken); + +@override +String toString() { + return 'Flow(serverUri: $serverUri, state: $state, verifier: $verifier, cookie: $cookie, isFlowComplete: $isFlowComplete, authToken: $authToken)'; +} + + } /// @nodoc -abstract class $FlowCopyWith<$Res> { - factory $FlowCopyWith(Flow value, $Res Function(Flow) then) = - _$FlowCopyWithImpl<$Res, Flow>; - @useResult - $Res call( - {Uri serverUri, - String state, - String verifier, - Cookie cookie, - bool isFlowComplete, - String? authToken}); -} +abstract mixin class $FlowCopyWith<$Res> { + factory $FlowCopyWith(Flow value, $Res Function(Flow) _then) = _$FlowCopyWithImpl; +@useResult +$Res call({ + Uri serverUri, String state, String verifier, Cookie cookie, bool isFlowComplete, String? authToken +}); + + + +} /// @nodoc -class _$FlowCopyWithImpl<$Res, $Val extends Flow> +class _$FlowCopyWithImpl<$Res> implements $FlowCopyWith<$Res> { - _$FlowCopyWithImpl(this._value, this._then); + _$FlowCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final Flow _self; + final $Res Function(Flow) _then; - /// Create a copy of Flow - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? serverUri = null, - Object? state = null, - Object? verifier = null, - Object? cookie = null, - Object? isFlowComplete = null, - Object? authToken = freezed, - }) { - return _then(_value.copyWith( - serverUri: null == serverUri - ? _value.serverUri - : serverUri // ignore: cast_nullable_to_non_nullable - as Uri, - state: null == state - ? _value.state - : state // ignore: cast_nullable_to_non_nullable - as String, - verifier: null == verifier - ? _value.verifier - : verifier // ignore: cast_nullable_to_non_nullable - as String, - cookie: null == cookie - ? _value.cookie - : cookie // ignore: cast_nullable_to_non_nullable - as Cookie, - isFlowComplete: null == isFlowComplete - ? _value.isFlowComplete - : isFlowComplete // ignore: cast_nullable_to_non_nullable - as bool, - authToken: freezed == authToken - ? _value.authToken - : authToken // ignore: cast_nullable_to_non_nullable - as String?, - ) as $Val); - } +/// Create a copy of Flow +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? serverUri = null,Object? state = null,Object? verifier = null,Object? cookie = null,Object? isFlowComplete = null,Object? authToken = freezed,}) { + return _then(_self.copyWith( +serverUri: null == serverUri ? _self.serverUri : serverUri // ignore: cast_nullable_to_non_nullable +as Uri,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable +as String,verifier: null == verifier ? _self.verifier : verifier // ignore: cast_nullable_to_non_nullable +as String,cookie: null == cookie ? _self.cookie : cookie // ignore: cast_nullable_to_non_nullable +as Cookie,isFlowComplete: null == isFlowComplete ? _self.isFlowComplete : isFlowComplete // ignore: cast_nullable_to_non_nullable +as bool,authToken: freezed == authToken ? _self.authToken : authToken // ignore: cast_nullable_to_non_nullable +as String?, + )); } -/// @nodoc -abstract class _$$FlowImplCopyWith<$Res> implements $FlowCopyWith<$Res> { - factory _$$FlowImplCopyWith( - _$FlowImpl value, $Res Function(_$FlowImpl) then) = - __$$FlowImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {Uri serverUri, - String state, - String verifier, - Cookie cookie, - bool isFlowComplete, - String? authToken}); } -/// @nodoc -class __$$FlowImplCopyWithImpl<$Res> - extends _$FlowCopyWithImpl<$Res, _$FlowImpl> - implements _$$FlowImplCopyWith<$Res> { - __$$FlowImplCopyWithImpl(_$FlowImpl _value, $Res Function(_$FlowImpl) _then) - : super(_value, _then); - /// Create a copy of Flow - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? serverUri = null, - Object? state = null, - Object? verifier = null, - Object? cookie = null, - Object? isFlowComplete = null, - Object? authToken = freezed, - }) { - return _then(_$FlowImpl( - serverUri: null == serverUri - ? _value.serverUri - : serverUri // ignore: cast_nullable_to_non_nullable - as Uri, - state: null == state - ? _value.state - : state // ignore: cast_nullable_to_non_nullable - as String, - verifier: null == verifier - ? _value.verifier - : verifier // ignore: cast_nullable_to_non_nullable - as String, - cookie: null == cookie - ? _value.cookie - : cookie // ignore: cast_nullable_to_non_nullable - as Cookie, - isFlowComplete: null == isFlowComplete - ? _value.isFlowComplete - : isFlowComplete // ignore: cast_nullable_to_non_nullable - as bool, - authToken: freezed == authToken - ? _value.authToken - : authToken // ignore: cast_nullable_to_non_nullable - as String?, - )); - } +/// Adds pattern-matching-related methods to [Flow]. +extension FlowPatterns on Flow { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _Flow value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _Flow() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _Flow value) $default,){ +final _that = this; +switch (_that) { +case _Flow(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Flow value)? $default,){ +final _that = this; +switch (_that) { +case _Flow() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( Uri serverUri, String state, String verifier, Cookie cookie, bool isFlowComplete, String? authToken)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _Flow() when $default != null: +return $default(_that.serverUri,_that.state,_that.verifier,_that.cookie,_that.isFlowComplete,_that.authToken);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( Uri serverUri, String state, String verifier, Cookie cookie, bool isFlowComplete, String? authToken) $default,) {final _that = this; +switch (_that) { +case _Flow(): +return $default(_that.serverUri,_that.state,_that.verifier,_that.cookie,_that.isFlowComplete,_that.authToken);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( Uri serverUri, String state, String verifier, Cookie cookie, bool isFlowComplete, String? authToken)? $default,) {final _that = this; +switch (_that) { +case _Flow() when $default != null: +return $default(_that.serverUri,_that.state,_that.verifier,_that.cookie,_that.isFlowComplete,_that.authToken);case _: + return null; + +} +} + } /// @nodoc -class _$FlowImpl implements _Flow { - const _$FlowImpl( - {required this.serverUri, - required this.state, - required this.verifier, - required this.cookie, - this.isFlowComplete = false, - this.authToken}); - @override - final Uri serverUri; - @override - final String state; - @override - final String verifier; - @override - final Cookie cookie; - @override - @JsonKey() - final bool isFlowComplete; - @override - final String? authToken; +class _Flow implements Flow { + const _Flow({required this.serverUri, required this.state, required this.verifier, required this.cookie, this.isFlowComplete = false, this.authToken}); + - @override - String toString() { - return 'Flow(serverUri: $serverUri, state: $state, verifier: $verifier, cookie: $cookie, isFlowComplete: $isFlowComplete, authToken: $authToken)'; - } +@override final Uri serverUri; +@override final String state; +@override final String verifier; +@override final Cookie cookie; +@override@JsonKey() final bool isFlowComplete; +@override final String? authToken; - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$FlowImpl && - (identical(other.serverUri, serverUri) || - other.serverUri == serverUri) && - (identical(other.state, state) || other.state == state) && - (identical(other.verifier, verifier) || - other.verifier == verifier) && - (identical(other.cookie, cookie) || other.cookie == cookie) && - (identical(other.isFlowComplete, isFlowComplete) || - other.isFlowComplete == isFlowComplete) && - (identical(other.authToken, authToken) || - other.authToken == authToken)); - } +/// Create a copy of Flow +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$FlowCopyWith<_Flow> get copyWith => __$FlowCopyWithImpl<_Flow>(this, _$identity); - @override - int get hashCode => Object.hash(runtimeType, serverUri, state, verifier, - cookie, isFlowComplete, authToken); - /// Create a copy of Flow - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$FlowImplCopyWith<_$FlowImpl> get copyWith => - __$$FlowImplCopyWithImpl<_$FlowImpl>(this, _$identity); + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Flow&&(identical(other.serverUri, serverUri) || other.serverUri == serverUri)&&(identical(other.state, state) || other.state == state)&&(identical(other.verifier, verifier) || other.verifier == verifier)&&(identical(other.cookie, cookie) || other.cookie == cookie)&&(identical(other.isFlowComplete, isFlowComplete) || other.isFlowComplete == isFlowComplete)&&(identical(other.authToken, authToken) || other.authToken == authToken)); } -abstract class _Flow implements Flow { - const factory _Flow( - {required final Uri serverUri, - required final String state, - required final String verifier, - required final Cookie cookie, - final bool isFlowComplete, - final String? authToken}) = _$FlowImpl; - @override - Uri get serverUri; - @override - String get state; - @override - String get verifier; - @override - Cookie get cookie; - @override - bool get isFlowComplete; - @override - String? get authToken; +@override +int get hashCode => Object.hash(runtimeType,serverUri,state,verifier,cookie,isFlowComplete,authToken); - /// Create a copy of Flow - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$FlowImplCopyWith<_$FlowImpl> get copyWith => - throw _privateConstructorUsedError; +@override +String toString() { + return 'Flow(serverUri: $serverUri, state: $state, verifier: $verifier, cookie: $cookie, isFlowComplete: $isFlowComplete, authToken: $authToken)'; } + + +} + +/// @nodoc +abstract mixin class _$FlowCopyWith<$Res> implements $FlowCopyWith<$Res> { + factory _$FlowCopyWith(_Flow value, $Res Function(_Flow) _then) = __$FlowCopyWithImpl; +@override @useResult +$Res call({ + Uri serverUri, String state, String verifier, Cookie cookie, bool isFlowComplete, String? authToken +}); + + + + +} +/// @nodoc +class __$FlowCopyWithImpl<$Res> + implements _$FlowCopyWith<$Res> { + __$FlowCopyWithImpl(this._self, this._then); + + final _Flow _self; + final $Res Function(_Flow) _then; + +/// Create a copy of Flow +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? serverUri = null,Object? state = null,Object? verifier = null,Object? cookie = null,Object? isFlowComplete = null,Object? authToken = freezed,}) { + return _then(_Flow( +serverUri: null == serverUri ? _self.serverUri : serverUri // ignore: cast_nullable_to_non_nullable +as Uri,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable +as String,verifier: null == verifier ? _self.verifier : verifier // ignore: cast_nullable_to_non_nullable +as String,cookie: null == cookie ? _self.cookie : cookie // ignore: cast_nullable_to_non_nullable +as Cookie,isFlowComplete: null == isFlowComplete ? _self.isFlowComplete : isFlowComplete // ignore: cast_nullable_to_non_nullable +as bool,authToken: freezed == authToken ? _self.authToken : authToken // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + + +} + +// dart format on diff --git a/lib/features/onboarding/providers/oauth_provider.dart b/lib/features/onboarding/providers/oauth_provider.dart index 79445d9..38446fa 100644 --- a/lib/features/onboarding/providers/oauth_provider.dart +++ b/lib/features/onboarding/providers/oauth_provider.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/api/api_provider.dart'; import 'package:vaani/models/error_response.dart'; @@ -52,8 +53,10 @@ class OauthFlows extends _$OauthFlows { } state = { ...state, - oauthState: state[oauthState]! - .copyWith(isFlowComplete: true, authToken: authToken), + oauthState: state[oauthState]!.copyWith( + isFlowComplete: true, + authToken: authToken, + ), }; } } @@ -61,7 +64,7 @@ class OauthFlows extends _$OauthFlows { /// the code returned by the server in exchange for the verifier @riverpod Future loginInExchangeForCode( - LoginInExchangeForCodeRef ref, { + Ref ref, { required State oauthState, required Code code, ErrorResponseHandler? responseHandler, diff --git a/lib/features/onboarding/providers/oauth_provider.g.dart b/lib/features/onboarding/providers/oauth_provider.g.dart index 9b7c4f5..d9c7575 100644 --- a/lib/features/onboarding/providers/oauth_provider.g.dart +++ b/lib/features/onboarding/providers/oauth_provider.g.dart @@ -6,219 +6,167 @@ part of 'oauth_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$loginInExchangeForCodeHash() => - r'e931254959d9eb8196439c6b0c884c26cbe17c2f'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); +@ProviderFor(OauthFlows) +final oauthFlowsProvider = OauthFlowsProvider._(); - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -/// the code returned by the server in exchange for the verifier -/// -/// Copied from [loginInExchangeForCode]. -@ProviderFor(loginInExchangeForCode) -const loginInExchangeForCodeProvider = LoginInExchangeForCodeFamily(); - -/// the code returned by the server in exchange for the verifier -/// -/// Copied from [loginInExchangeForCode]. -class LoginInExchangeForCodeFamily extends Family> { - /// the code returned by the server in exchange for the verifier - /// - /// Copied from [loginInExchangeForCode]. - const LoginInExchangeForCodeFamily(); - - /// the code returned by the server in exchange for the verifier - /// - /// Copied from [loginInExchangeForCode]. - LoginInExchangeForCodeProvider call({ - required String oauthState, - required String code, - ErrorResponseHandler? responseHandler, - }) { - return LoginInExchangeForCodeProvider( - oauthState: oauthState, - code: code, - responseHandler: responseHandler, - ); - } - - @override - LoginInExchangeForCodeProvider getProviderOverride( - covariant LoginInExchangeForCodeProvider provider, - ) { - return call( - oauthState: provider.oauthState, - code: provider.code, - responseHandler: provider.responseHandler, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'loginInExchangeForCodeProvider'; -} - -/// the code returned by the server in exchange for the verifier -/// -/// Copied from [loginInExchangeForCode]. -class LoginInExchangeForCodeProvider - extends AutoDisposeFutureProvider { - /// the code returned by the server in exchange for the verifier - /// - /// Copied from [loginInExchangeForCode]. - LoginInExchangeForCodeProvider({ - required String oauthState, - required String code, - ErrorResponseHandler? responseHandler, - }) : this._internal( - (ref) => loginInExchangeForCode( - ref as LoginInExchangeForCodeRef, - oauthState: oauthState, - code: code, - responseHandler: responseHandler, - ), - from: loginInExchangeForCodeProvider, - name: r'loginInExchangeForCodeProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$loginInExchangeForCodeHash, - dependencies: LoginInExchangeForCodeFamily._dependencies, - allTransitiveDependencies: - LoginInExchangeForCodeFamily._allTransitiveDependencies, - oauthState: oauthState, - code: code, - responseHandler: responseHandler, - ); - - LoginInExchangeForCodeProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.oauthState, - required this.code, - required this.responseHandler, - }) : super.internal(); - - final String oauthState; - final String code; - final ErrorResponseHandler? responseHandler; - - @override - Override overrideWith( - FutureOr Function(LoginInExchangeForCodeRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: LoginInExchangeForCodeProvider._internal( - (ref) => create(ref as LoginInExchangeForCodeRef), - from: from, - name: null, +final class OauthFlowsProvider + extends $NotifierProvider> { + OauthFlowsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'oauthFlowsProvider', + isAutoDispose: false, dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - oauthState: oauthState, - code: code, - responseHandler: responseHandler, - ), - ); - } + $allTransitiveDependencies: null, + ); @override - AutoDisposeFutureProviderElement createElement() { - return _LoginInExchangeForCodeProviderElement(this); + String debugGetCreateSourceHash() => _$oauthFlowsHash(); + + @$internal + @override + OauthFlows create() => OauthFlows(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Map value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>(value), + ); + } +} + +String _$oauthFlowsHash() => r'4e278baa0bf26f2a10694ca2caadb68dd5b6156f'; + +abstract class _$OauthFlows extends $Notifier> { + Map build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref, Map>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, Map>, + Map, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} + +/// the code returned by the server in exchange for the verifier + +@ProviderFor(loginInExchangeForCode) +final loginInExchangeForCodeProvider = LoginInExchangeForCodeFamily._(); + +/// the code returned by the server in exchange for the verifier + +final class LoginInExchangeForCodeProvider + extends $FunctionalProvider, String?, FutureOr> + with $FutureModifier, $FutureProvider { + /// the code returned by the server in exchange for the verifier + LoginInExchangeForCodeProvider._({ + required LoginInExchangeForCodeFamily super.from, + required ({ + State oauthState, + Code code, + ErrorResponseHandler? responseHandler, + }) + super.argument, + }) : super( + retry: null, + name: r'loginInExchangeForCodeProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$loginInExchangeForCodeHash(); + + @override + String toString() { + return r'loginInExchangeForCodeProvider' + '' + '$argument'; + } + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = + this.argument + as ({ + State oauthState, + Code code, + ErrorResponseHandler? responseHandler, + }); + return loginInExchangeForCode( + ref, + oauthState: argument.oauthState, + code: argument.code, + responseHandler: argument.responseHandler, + ); } @override bool operator ==(Object other) { return other is LoginInExchangeForCodeProvider && - other.oauthState == oauthState && - other.code == code && - other.responseHandler == responseHandler; + other.argument == argument; } @override int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, oauthState.hashCode); - hash = _SystemHash.combine(hash, code.hashCode); - hash = _SystemHash.combine(hash, responseHandler.hashCode); - - return _SystemHash.finish(hash); + return argument.hashCode; } } -mixin LoginInExchangeForCodeRef on AutoDisposeFutureProviderRef { - /// The parameter `oauthState` of this provider. - String get oauthState; +String _$loginInExchangeForCodeHash() => + r'bfc3945529048a0f536052fd5579b76457560fcd'; - /// The parameter `code` of this provider. - String get code; +/// the code returned by the server in exchange for the verifier - /// The parameter `responseHandler` of this provider. - ErrorResponseHandler? get responseHandler; +final class LoginInExchangeForCodeFamily extends $Family + with + $FunctionalFamilyOverride< + FutureOr, + ({State oauthState, Code code, ErrorResponseHandler? responseHandler}) + > { + LoginInExchangeForCodeFamily._() + : super( + retry: null, + name: r'loginInExchangeForCodeProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// the code returned by the server in exchange for the verifier + + LoginInExchangeForCodeProvider call({ + required State oauthState, + required Code code, + ErrorResponseHandler? responseHandler, + }) => LoginInExchangeForCodeProvider._( + argument: ( + oauthState: oauthState, + code: code, + responseHandler: responseHandler, + ), + from: this, + ); + + @override + String toString() => r'loginInExchangeForCodeProvider'; } - -class _LoginInExchangeForCodeProviderElement - extends AutoDisposeFutureProviderElement - with LoginInExchangeForCodeRef { - _LoginInExchangeForCodeProviderElement(super.provider); - - @override - String get oauthState => - (origin as LoginInExchangeForCodeProvider).oauthState; - @override - String get code => (origin as LoginInExchangeForCodeProvider).code; - @override - ErrorResponseHandler? get responseHandler => - (origin as LoginInExchangeForCodeProvider).responseHandler; -} - -String _$oauthFlowsHash() => r'4e278baa0bf26f2a10694ca2caadb68dd5b6156f'; - -/// See also [OauthFlows]. -@ProviderFor(OauthFlows) -final oauthFlowsProvider = - NotifierProvider>.internal( - OauthFlows.new, - name: r'oauthFlowsProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$oauthFlowsHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$OauthFlows = 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 diff --git a/lib/features/onboarding/view/callback_page.dart b/lib/features/onboarding/view/callback_page.dart index dd3de86..bb4e139 100644 --- a/lib/features/onboarding/view/callback_page.dart +++ b/lib/features/onboarding/view/callback_page.dart @@ -27,9 +27,7 @@ class CallbackPage extends HookConsumerWidget { // check if the state is in the flows if (!flows.containsKey(state)) { - return const _SomethingWentWrong( - message: 'State not found', - ); + return const _SomethingWentWrong(message: 'State not found'); } // get the token @@ -45,26 +43,21 @@ class CallbackPage extends HookConsumerWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text('Contacting server...\nPlease wait\n\nGot:' - '\nState: $state\nCode: $code'), + Text( + 'Contacting server...\nPlease wait\n\nGot:' + '\nState: $state\nCode: $code', + ), loginAuthToken.when( data: (authenticationToken) { if (authenticationToken == null) { - handleServerError( - context, - serverErrorResponse, - ); + handleServerError(context, serverErrorResponse); return const BackToLoginButton(); } return Text('Token: $authenticationToken'); }, loading: () => const CircularProgressIndicator(), error: (error, _) { - handleServerError( - context, - serverErrorResponse, - e: error, - ); + handleServerError(context, serverErrorResponse, e: error); return Column( children: [ Text('Error with OAuth flow: $error'), @@ -81,9 +74,7 @@ class CallbackPage extends HookConsumerWidget { } class BackToLoginButton extends StatelessWidget { - const BackToLoginButton({ - super.key, - }); + const BackToLoginButton({super.key}); @override Widget build(BuildContext context) { @@ -97,10 +88,7 @@ class BackToLoginButton extends StatelessWidget { } class _SomethingWentWrong extends StatelessWidget { - const _SomethingWentWrong({ - super.key, - this.message = 'Error with OAuth flow', - }); + const _SomethingWentWrong({this.message = 'Error with OAuth flow'}); final String message; @@ -110,10 +98,7 @@ class _SomethingWentWrong extends StatelessWidget { body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(message), - const BackToLoginButton(), - ], + children: [Text(message), const BackToLoginButton()], ), ), ); diff --git a/lib/features/onboarding/view/onboarding_single_page.dart b/lib/features/onboarding/view/onboarding_single_page.dart index 5ceff08..c0e701a 100644 --- a/lib/features/onboarding/view/onboarding_single_page.dart +++ b/lib/features/onboarding/view/onboarding_single_page.dart @@ -9,101 +9,120 @@ import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/widgets/add_new_server.dart'; class OnboardingSinglePage extends HookConsumerWidget { - const OnboardingSinglePage({ - super.key, - }); + const OnboardingSinglePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final apiSettings = ref.watch(apiSettingsProvider); - final serverUriController = useTextEditingController( - text: apiSettings.activeServer?.serverUrl.toString() ?? '', - ); - var audiobookshelfUri = makeBaseUrl(serverUriController.text); - - final canUserLogin = useState(apiSettings.activeServer != null); - - fadeSlideTransitionBuilder( - Widget child, - Animation animation, - ) { - return FadeTransition( - opacity: animation, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 0.3), - end: const Offset(0, 0), - ).animate(animation), - child: child, - ), - ); - } - return Scaffold( - body: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'Welcome to Vaani', - style: Theme.of(context).textTheme.headlineSmall, + body: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Center( + child: SingleChildScrollView( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 600, + minWidth: constraints.maxWidth < 600 + ? constraints.maxWidth + : 0, + ), + child: const Padding( + padding: EdgeInsets.symmetric(vertical: 20.0), + child: SafeArea(child: OnboardingBody()), + ), + ), ), - ), - const SizedBox.square( - dimension: 16.0, - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: AnimatedSwitcher( - duration: 500.ms, - transitionBuilder: fadeSlideTransitionBuilder, - child: canUserLogin.value - ? Text( - 'Server connected, please login', - key: const ValueKey('connected'), - style: Theme.of(context).textTheme.bodyMedium, - ) - : Text( - 'Please enter the URL of your AudiobookShelf Server', - key: const ValueKey('not_connected'), - style: Theme.of(context).textTheme.bodyMedium, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: AddNewServer( - controller: serverUriController, - allowEmpty: true, - onPressed: () { - canUserLogin.value = serverUriController.text.isNotEmpty; - }, - ), - ), - AnimatedSwitcher( - duration: 500.ms, - transitionBuilder: fadeSlideTransitionBuilder, - child: canUserLogin.value - ? UserLoginWidget( - server: audiobookshelfUri, - ) - // ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0) - : const RedirectToABS().animate().fadeIn().slideY( - curve: Curves.easeInOut, - duration: 500.ms, - ), - ), - ], + ); + }, ), ); } } +Widget fadeSlideTransitionBuilder(Widget child, Animation animation) { + return FadeTransition( + opacity: animation, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 0.3), + end: const Offset(0, 0), + ).animate(animation), + child: child, + ), + ); +} + +class OnboardingBody extends HookConsumerWidget { + const OnboardingBody({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final apiSettings = ref.watch(apiSettingsProvider); + final serverUriController = useTextEditingController( + text: apiSettings.activeServer?.serverUrl.toString() ?? 'https://', + ); + var audiobookshelfUri = makeBaseUrl(serverUriController.text); + + final canUserLogin = useState(apiSettings.activeServer != null); + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'Welcome to Vaani', + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + const SizedBox.square(dimension: 16.0), + Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedSwitcher( + duration: 500.ms, + transitionBuilder: fadeSlideTransitionBuilder, + child: canUserLogin.value + ? Text( + 'Server connected, please login', + key: const ValueKey('connected'), + style: Theme.of(context).textTheme.bodyMedium, + ) + : Text( + 'Please enter the URL of your AudiobookShelf Server', + key: const ValueKey('not_connected'), + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: AddNewServer( + controller: serverUriController, + allowEmpty: true, + onPressed: () { + canUserLogin.value = serverUriController.text.isNotEmpty; + }, + ), + ), + const SizedBox.square(dimension: 16.0), + AnimatedSwitcher( + duration: 500.ms, + transitionBuilder: fadeSlideTransitionBuilder, + child: canUserLogin.value + ? UserLoginWidget(server: audiobookshelfUri) + // ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0) + : const RedirectToABS().animate().fadeIn().slideY( + curve: Curves.easeInOut, + duration: 500.ms, + ), + ), + ], + ); + } +} + class RedirectToABS extends StatelessWidget { - const RedirectToABS({ - super.key, - }); + const RedirectToABS({super.key}); @override Widget build(BuildContext context) { @@ -119,18 +138,14 @@ class RedirectToABS extends StatelessWidget { isSemanticButton: false, style: ButtonStyle( elevation: WidgetStateProperty.all(0), - padding: WidgetStateProperty.all( - const EdgeInsets.all(0), - ), + padding: WidgetStateProperty.all(const EdgeInsets.all(0)), ), onPressed: () async { // open the github page // ignore: avoid_print print('Opening the github page'); await handleLaunchUrl( - Uri.parse( - 'https://www.audiobookshelf.org', - ), + Uri.parse('https://www.audiobookshelf.org'), ); }, child: const Text('Click here'), diff --git a/lib/features/onboarding/view/user_login.dart b/lib/features/onboarding/view/user_login.dart index 2b3d575..7428953 100644 --- a/lib/features/onboarding/view/user_login.dart +++ b/lib/features/onboarding/view/user_login.dart @@ -1,32 +1,38 @@ import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart'; -import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/api/server_provider.dart'; -import 'package:vaani/features/onboarding/view/user_login_with_open_id.dart'; -import 'package:vaani/features/onboarding/view/user_login_with_password.dart'; -import 'package:vaani/features/onboarding/view/user_login_with_token.dart'; -import 'package:vaani/hacks/fix_autofill_losing_focus.dart'; -import 'package:vaani/models/error_response.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; +import 'package:shelfsdk/audiobookshelf_api.dart' show AuthMethod; +import 'package:vaani/api/api_provider.dart' show serverStatusProvider; +import 'package:vaani/api/server_provider.dart' + show ServerAlreadyExistsException, audiobookShelfServerProvider; +import 'package:vaani/features/onboarding/view/onboarding_single_page.dart' + show fadeSlideTransitionBuilder; +import 'package:vaani/features/onboarding/view/user_login_with_open_id.dart' + show UserLoginWithOpenID; +import 'package:vaani/features/onboarding/view/user_login_with_password.dart' + show UserLoginWithPassword; +import 'package:vaani/features/onboarding/view/user_login_with_token.dart' + show UserLoginWithToken; +import 'package:vaani/hacks/fix_autofill_losing_focus.dart' + show InactiveFocusScopeObserver; +import 'package:vaani/models/error_response.dart' show ErrorResponseHandler; +import 'package:vaani/settings/api_settings_provider.dart' + show apiSettingsProvider; import 'package:vaani/settings/models/models.dart' as model; class UserLoginWidget extends HookConsumerWidget { - UserLoginWidget({ - super.key, - required this.server, - }); + const UserLoginWidget({super.key, required this.server, this.onSuccess}); final Uri server; - final serverStatusError = ErrorResponseHandler(); + final Function(model.AuthenticatedUser)? onSuccess; @override Widget build(BuildContext context, WidgetRef ref) { - final serverStatus = - ref.watch(serverStatusProvider(server, serverStatusError.storeError)); - - final api = ref.watch(audiobookshelfApiProvider(server)); + final serverStatusError = useMemoized(() => ErrorResponseHandler(), []); + final serverStatus = ref.watch( + serverStatusProvider(server, serverStatusError.storeError), + ); return serverStatus.when( data: (value) { @@ -42,12 +48,11 @@ class UserLoginWidget extends HookConsumerWidget { openIDAvailable: value.authMethods?.contains(AuthMethod.openid) ?? false, openIDButtonText: value.authFormData?.authOpenIDButtonText, + onSuccess: onSuccess, ); }, loading: () { - return const Center( - child: CircularProgressIndicator(), - ); + return const Center(child: CircularProgressIndicator()); }, error: (error, _) { return Center( @@ -58,10 +63,7 @@ class UserLoginWidget extends HookConsumerWidget { ElevatedButton( onPressed: () { ref.invalidate( - serverStatusProvider( - server, - serverStatusError.storeError, - ), + serverStatusProvider(server, serverStatusError.storeError), ); }, child: const Text('Try again'), @@ -74,11 +76,7 @@ class UserLoginWidget extends HookConsumerWidget { } } -enum AuthMethodChoice { - local, - openid, - authToken, -} +enum AuthMethodChoice { local, openid, authToken } class UserLoginMultipleAuth extends HookConsumerWidget { const UserLoginMultipleAuth({ @@ -88,6 +86,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget { this.openIDAvailable = false, this.onPressed, this.openIDButtonText, + this.onSuccess, }); final Uri server; @@ -95,6 +94,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget { final bool openIDAvailable; final void Function()? onPressed; final String? openIDButtonText; + final Function(model.AuthenticatedUser)? onSuccess; @override Widget build(BuildContext context, WidgetRef ref) { @@ -104,24 +104,18 @@ class UserLoginMultipleAuth extends HookConsumerWidget { localAvailable ? AuthMethodChoice.local : AuthMethodChoice.authToken, ); - final apiSettings = ref.watch(apiSettingsProvider); - model.AudiobookShelfServer addServer() { - var newServer = model.AudiobookShelfServer( - serverUrl: server, - ); + var newServer = model.AudiobookShelfServer(serverUrl: server); try { // add the server to the list of servers - ref.read(audiobookShelfServerProvider.notifier).addServer( - newServer, - ); + ref.read(audiobookShelfServerProvider.notifier).addServer(newServer); } on ServerAlreadyExistsException catch (e) { newServer = e.server; } finally { - ref.read(apiSettingsProvider.notifier).updateState( - apiSettings.copyWith( - activeServer: newServer, - ), + ref + .read(apiSettingsProvider.notifier) + .updateState( + ref.read(apiSettingsProvider).copyWith(activeServer: newServer), ); } return newServer; @@ -130,70 +124,87 @@ class UserLoginMultipleAuth extends HookConsumerWidget { return Center( child: InactiveFocusScopeObserver( child: AutofillGroup( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - Wrap( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Wrap( // mainAxisAlignment: MainAxisAlignment.center, spacing: 10, runAlignment: WrapAlignment.center, runSpacing: 10, alignment: WrapAlignment.center, - children: [ - // a small label to show the user what to do - if (localAvailable) - ChoiceChip( - label: const Text('Local'), - selected: methodChoice.value == AuthMethodChoice.local, - onSelected: (selected) { - if (selected) { - methodChoice.value = AuthMethodChoice.local; - } - }, - ), - if (openIDAvailable) - ChoiceChip( - label: const Text('OpenID'), - selected: methodChoice.value == AuthMethodChoice.openid, - onSelected: (selected) { - if (selected) { - methodChoice.value = AuthMethodChoice.openid; - } - }, - ), - ChoiceChip( - label: const Text('Token'), - selected: - methodChoice.value == AuthMethodChoice.authToken, - onSelected: (selected) { - if (selected) { - methodChoice.value = AuthMethodChoice.authToken; - } - }, - ), - ], + children: + [ + // a small label to show the user what to do + if (localAvailable) + ChoiceChip( + label: const Text('Local'), + selected: + methodChoice.value == + AuthMethodChoice.local, + onSelected: (selected) { + if (selected) { + methodChoice.value = AuthMethodChoice.local; + } + }, + ), + if (openIDAvailable) + ChoiceChip( + label: const Text('OpenID'), + selected: + methodChoice.value == + AuthMethodChoice.openid, + onSelected: (selected) { + if (selected) { + methodChoice.value = + AuthMethodChoice.openid; + } + }, + ), + ChoiceChip( + label: const Text('Token'), + selected: + methodChoice.value == + AuthMethodChoice.authToken, + onSelected: (selected) { + if (selected) { + methodChoice.value = + AuthMethodChoice.authToken; + } + }, + ), + ] + .animate(interval: 100.ms) + .fadeIn(duration: 150.ms, curve: Curves.easeIn), ), - const SizedBox.square( - dimension: 8, - ), - switch (methodChoice.value) { - AuthMethodChoice.authToken => UserLoginWithToken( + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedSwitcher( + duration: 200.ms, + transitionBuilder: fadeSlideTransitionBuilder, + child: switch (methodChoice.value) { + AuthMethodChoice.authToken => UserLoginWithToken( server: server, addServer: addServer, + onSuccess: onSuccess, ), - AuthMethodChoice.local => UserLoginWithPassword( + AuthMethodChoice.local => UserLoginWithPassword( server: server, addServer: addServer, + onSuccess: onSuccess, ), - AuthMethodChoice.openid => UserLoginWithOpenID( + AuthMethodChoice.openid => UserLoginWithOpenID( server: server, addServer: addServer, openIDButtonText: openIDButtonText, + onSuccess: onSuccess, ), - }, - ], - ), + }, + ), + ), + ], ), ), ), diff --git a/lib/features/onboarding/view/user_login_with_open_id.dart b/lib/features/onboarding/view/user_login_with_open_id.dart index e403c81..339b6c5 100644 --- a/lib/features/onboarding/view/user_login_with_open_id.dart +++ b/lib/features/onboarding/view/user_login_with_open_id.dart @@ -11,6 +11,7 @@ import 'package:vaani/models/error_response.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/constants.dart'; import 'package:vaani/settings/models/models.dart' as model; +import 'package:vaani/shared/extensions/obfuscation.dart'; import 'package:vaani/shared/utils.dart'; class UserLoginWithOpenID extends HookConsumerWidget { @@ -19,12 +20,14 @@ class UserLoginWithOpenID extends HookConsumerWidget { required this.server, required this.addServer, this.openIDButtonText, + this.onSuccess, }); final Uri server; final model.AudiobookShelfServer Function() addServer; final String? openIDButtonText; final responseErrorHandler = ErrorResponseHandler(name: 'OpenID'); + final Function(model.AuthenticatedUser)? onSuccess; @override Widget build(BuildContext context, WidgetRef ref) { @@ -51,9 +54,9 @@ class UserLoginWithOpenID extends HookConsumerWidget { if (openIDLoginEndpoint == null) { if (responseErrorHandler.response.statusCode == 400 && - responseErrorHandler.response.body - .toLowerCase() - .contains(RegExp(r'invalid.*redirect.*uri'))) { + responseErrorHandler.response.body.toLowerCase().contains( + RegExp(r'invalid.*redirect.*uri'), + )) { // show error handleServerError( context, @@ -89,19 +92,21 @@ class UserLoginWithOpenID extends HookConsumerWidget { return; } - appLogger.fine('Got OpenID login endpoint: $openIDLoginEndpoint'); + appLogger.fine( + 'Got OpenID login endpoint: ${openIDLoginEndpoint.obfuscate()}', + ); // add the flow to the provider - ref.read(oauthFlowsProvider.notifier).addFlow( + ref + .read(oauthFlowsProvider.notifier) + .addFlow( oauthState, verifier: verifier, serverUri: server, cookie: Cookie.fromSetCookieValue(authCookie!), ); - await handleLaunchUrl( - openIDLoginEndpoint, - ); + await handleLaunchUrl(openIDLoginEndpoint); } return Column( diff --git a/lib/features/onboarding/view/user_login_with_password.dart b/lib/features/onboarding/view/user_login_with_password.dart index 91eeea0..20d2613 100644 --- a/lib/features/onboarding/view/user_login_with_password.dart +++ b/lib/features/onboarding/view/user_login_with_password.dart @@ -5,10 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lottie/lottie.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/api/authenticated_user_provider.dart'; +import 'package:vaani/api/authenticated_users_provider.dart'; import 'package:vaani/hacks/fix_autofill_losing_focus.dart'; import 'package:vaani/models/error_response.dart'; import 'package:vaani/router/router.dart'; +import 'package:vaani/settings/constants.dart'; import 'package:vaani/settings/models/models.dart' as model; import 'package:vaani/shared/utils.dart'; @@ -17,17 +18,20 @@ class UserLoginWithPassword extends HookConsumerWidget { super.key, required this.server, required this.addServer, + this.onSuccess, }); final Uri server; final model.AudiobookShelfServer Function() addServer; final serverErrorResponse = ErrorResponseHandler(); + final Function(model.AuthenticatedUser)? onSuccess; @override Widget build(BuildContext context, WidgetRef ref) { final usernameController = useTextEditingController(); final passwordController = useTextEditingController(); final isPasswordVisibleAnimationController = useAnimationController( + initialValue: 1, duration: const Duration(milliseconds: 500), ); @@ -35,17 +39,14 @@ class UserLoginWithPassword extends HookConsumerWidget { final api = ref.watch(audiobookshelfApiProvider(server)); // forward animation when the password visibility changes - useEffect( - () { - if (isPasswordVisible.value) { - isPasswordVisibleAnimationController.forward(); - } else { - isPasswordVisibleAnimationController.reverse(); - } - return null; - }, - [isPasswordVisible.value], - ); + useEffect(() { + if (isPasswordVisible.value) { + isPasswordVisibleAnimationController.forward(); + } else { + isPasswordVisibleAnimationController.reverse(); + } + return null; + }, [isPasswordVisible.value]); /// Login to the server and save the user Future loginAndSave() async { @@ -76,92 +77,89 @@ class UserLoginWithPassword extends HookConsumerWidget { final authenticatedUser = model.AuthenticatedUser( server: addServer(), id: success.user.id, - password: password, username: username, authToken: api.token!, ); - // add the user to the list of users - ref - .read(authenticatedUserProvider.notifier) - .addUser(authenticatedUser, setActive: true); - // redirect to the library page - GoRouter.of(context).goNamed(Routes.home.name); + if (onSuccess != null) { + onSuccess!(authenticatedUser); + } else { + // add the user to the list of users + ref + .read(authenticatedUsersProvider.notifier) + .addUser(authenticatedUser, setActive: true); + context.goNamed(Routes.home.name); + } } return Center( child: InactiveFocusScopeObserver( child: AutofillGroup( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextFormField( - controller: usernameController, - autofocus: true, - autofillHints: const [AutofillHints.username], - textInputAction: TextInputAction.next, - decoration: InputDecoration( - labelText: 'Username', - labelStyle: TextStyle( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.8), - ), - border: const OutlineInputBorder(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextFormField( + controller: usernameController, + autofocus: true, + autofillHints: const [AutofillHints.username], + textInputAction: TextInputAction.next, + decoration: InputDecoration( + labelText: 'Username', + labelStyle: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.8), ), + border: const OutlineInputBorder(), ), - const SizedBox(height: 10), - TextFormField( - controller: passwordController, - autofillHints: const [AutofillHints.password], - textInputAction: TextInputAction.done, - obscureText: !isPasswordVisible.value, - onFieldSubmitted: (_) { - loginAndSave(); - }, - decoration: InputDecoration( - labelText: 'Password', - labelStyle: TextStyle( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.8), + ), + const SizedBox(height: 10), + TextFormField( + controller: passwordController, + autofillHints: const [AutofillHints.password], + textInputAction: TextInputAction.done, + obscureText: !isPasswordVisible.value, + onFieldSubmitted: (_) { + loginAndSave(); + }, + decoration: InputDecoration( + labelText: 'Password', + labelStyle: TextStyle( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.8), + ), + border: const OutlineInputBorder(), + suffixIcon: ColorFiltered( + colorFilter: ColorFilter.mode( + Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.8), + BlendMode.srcIn, ), - border: const OutlineInputBorder(), - suffixIcon: ColorFiltered( - colorFilter: ColorFilter.mode( - Theme.of(context).colorScheme.primary.withOpacity(0.8), - BlendMode.srcIn, - ), - child: InkWell( - borderRadius: BorderRadius.circular(50), - onTap: () { - isPasswordVisible.value = !isPasswordVisible.value; - }, - child: Container( - margin: const EdgeInsets.only(left: 8, right: 8), - child: Lottie.asset( - 'assets/animations/Animation - 1714930099660.json', - controller: isPasswordVisibleAnimationController, - ), + child: InkWell( + borderRadius: BorderRadius.circular(50), + onTap: () { + isPasswordVisible.value = !isPasswordVisible.value; + }, + child: Container( + margin: const EdgeInsets.only(left: 8, right: 8), + child: Lottie.asset( + 'assets/animations/Animation - 1714930099660.json', + controller: isPasswordVisibleAnimationController, ), ), ), - suffixIconConstraints: const BoxConstraints( - maxHeight: 45, - ), ), + suffixIconConstraints: const BoxConstraints(maxHeight: 45), ), - const SizedBox(height: 30), - ElevatedButton( - onPressed: loginAndSave, - child: const Text('Login'), - ), - ], - ), + ), + const SizedBox(height: 30), + ElevatedButton( + onPressed: loginAndSave, + child: const Text('Login'), + ), + ], ), ), ), @@ -191,10 +189,12 @@ Future handleServerError( context: context, builder: (context) => AlertDialog( title: const Text('Error'), - content: SelectableText('$title\n' - 'Got response: ${responseErrorHandler.response.body} (${responseErrorHandler.response.statusCode})\n' - 'Stacktrace: $e\n\n' - '$body\n\n'), + content: SelectableText( + '$title\n' + 'Got response: ${responseErrorHandler.response.body} (${responseErrorHandler.response.statusCode})\n' + 'Stacktrace: $e\n\n' + '$body\n\n', + ), actions: [ if (outLink != null) TextButton( @@ -207,8 +207,10 @@ Future handleServerError( onPressed: () { // open an issue on the github page handleLaunchUrl( - Uri.parse( - 'https://github.com/Dr-Blank/Vaani/issues', + AppMetadata.githubRepo + // append the issue url + .replace( + path: '${AppMetadata.githubRepo.path}/issues/new', ), ); }, diff --git a/lib/features/onboarding/view/user_login_with_token.dart b/lib/features/onboarding/view/user_login_with_token.dart index 35cdf55..fb5a1fa 100644 --- a/lib/features/onboarding/view/user_login_with_token.dart +++ b/lib/features/onboarding/view/user_login_with_token.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/api/authenticated_user_provider.dart'; +import 'package:vaani/api/authenticated_users_provider.dart'; import 'package:vaani/models/error_response.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/models/models.dart' as model; @@ -14,11 +14,13 @@ class UserLoginWithToken extends HookConsumerWidget { super.key, required this.server, required this.addServer, + this.onSuccess, }); final Uri server; final model.AudiobookShelfServer Function() addServer; final serverErrorResponse = ErrorResponseHandler(); + final Function(model.AuthenticatedUser)? onSuccess; @override Widget build(BuildContext context, WidgetRef ref) { @@ -65,11 +67,14 @@ class UserLoginWithToken extends HookConsumerWidget { authToken: api.token!, ); - ref - .read(authenticatedUserProvider.notifier) - .addUser(authenticatedUser, setActive: true); - - context.goNamed(Routes.home.name); + if (onSuccess != null) { + onSuccess!(authenticatedUser); + } else { + ref + .read(authenticatedUsersProvider.notifier) + .addUser(authenticatedUser, setActive: true); + context.goNamed(Routes.home.name); + } } return Form( @@ -84,7 +89,9 @@ class UserLoginWithToken extends HookConsumerWidget { decoration: InputDecoration( labelText: 'API Token', labelStyle: TextStyle( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.8), ), border: const OutlineInputBorder(), ), @@ -99,10 +106,7 @@ class UserLoginWithToken extends HookConsumerWidget { }, ), const SizedBox(height: 10), - ElevatedButton( - onPressed: loginAndSave, - child: const Text('Login'), - ), + ElevatedButton(onPressed: loginAndSave, child: const Text('Login')), ], ), ); diff --git a/lib/features/per_book_settings/models/book_settings.dart b/lib/features/per_book_settings/models/book_settings.dart index b4f0cda..853fe53 100644 --- a/lib/features/per_book_settings/models/book_settings.dart +++ b/lib/features/per_book_settings/models/book_settings.dart @@ -6,7 +6,7 @@ part 'book_settings.g.dart'; /// per book settings @freezed -class BookSettings with _$BookSettings { +sealed class BookSettings with _$BookSettings { const factory BookSettings({ required String bookId, @Default(NullablePlayerSettings()) NullablePlayerSettings playerSettings, diff --git a/lib/features/per_book_settings/models/book_settings.freezed.dart b/lib/features/per_book_settings/models/book_settings.freezed.dart index 63b55da..8ba7009 100644 --- a/lib/features/per_book_settings/models/book_settings.freezed.dart +++ b/lib/features/per_book_settings/models/book_settings.freezed.dart @@ -1,5 +1,5 @@ -// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file // 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 @@ -9,195 +9,284 @@ part of 'book_settings.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -BookSettings _$BookSettingsFromJson(Map json) { - return _BookSettings.fromJson(json); -} - /// @nodoc mixin _$BookSettings { - String get bookId => throw _privateConstructorUsedError; - NullablePlayerSettings get playerSettings => - throw _privateConstructorUsedError; + + String get bookId; NullablePlayerSettings get playerSettings; +/// Create a copy of BookSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$BookSettingsCopyWith get copyWith => _$BookSettingsCopyWithImpl(this as BookSettings, _$identity); /// Serializes this BookSettings to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is BookSettings&&(identical(other.bookId, bookId) || other.bookId == bookId)&&(identical(other.playerSettings, playerSettings) || other.playerSettings == playerSettings)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,bookId,playerSettings); + +@override +String toString() { + return 'BookSettings(bookId: $bookId, playerSettings: $playerSettings)'; +} + - /// Create a copy of BookSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $BookSettingsCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $BookSettingsCopyWith<$Res> { - factory $BookSettingsCopyWith( - BookSettings value, $Res Function(BookSettings) then) = - _$BookSettingsCopyWithImpl<$Res, BookSettings>; - @useResult - $Res call({String bookId, NullablePlayerSettings playerSettings}); +abstract mixin class $BookSettingsCopyWith<$Res> { + factory $BookSettingsCopyWith(BookSettings value, $Res Function(BookSettings) _then) = _$BookSettingsCopyWithImpl; +@useResult +$Res call({ + String bookId, NullablePlayerSettings playerSettings +}); + + +$NullablePlayerSettingsCopyWith<$Res> get playerSettings; - $NullablePlayerSettingsCopyWith<$Res> get playerSettings; } - /// @nodoc -class _$BookSettingsCopyWithImpl<$Res, $Val extends BookSettings> +class _$BookSettingsCopyWithImpl<$Res> implements $BookSettingsCopyWith<$Res> { - _$BookSettingsCopyWithImpl(this._value, this._then); + _$BookSettingsCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final BookSettings _self; + final $Res Function(BookSettings) _then; - /// Create a copy of BookSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? bookId = null, - Object? playerSettings = null, - }) { - return _then(_value.copyWith( - bookId: null == bookId - ? _value.bookId - : bookId // ignore: cast_nullable_to_non_nullable - as String, - playerSettings: null == playerSettings - ? _value.playerSettings - : playerSettings // ignore: cast_nullable_to_non_nullable - as NullablePlayerSettings, - ) as $Val); - } - - /// Create a copy of BookSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $NullablePlayerSettingsCopyWith<$Res> get playerSettings { - return $NullablePlayerSettingsCopyWith<$Res>(_value.playerSettings, - (value) { - return _then(_value.copyWith(playerSettings: value) as $Val); - }); - } +/// Create a copy of BookSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? bookId = null,Object? playerSettings = null,}) { + return _then(_self.copyWith( +bookId: null == bookId ? _self.bookId : bookId // ignore: cast_nullable_to_non_nullable +as String,playerSettings: null == playerSettings ? _self.playerSettings : playerSettings // ignore: cast_nullable_to_non_nullable +as NullablePlayerSettings, + )); +} +/// Create a copy of BookSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$NullablePlayerSettingsCopyWith<$Res> get playerSettings { + + return $NullablePlayerSettingsCopyWith<$Res>(_self.playerSettings, (value) { + return _then(_self.copyWith(playerSettings: value)); + }); +} } -/// @nodoc -abstract class _$$BookSettingsImplCopyWith<$Res> - implements $BookSettingsCopyWith<$Res> { - factory _$$BookSettingsImplCopyWith( - _$BookSettingsImpl value, $Res Function(_$BookSettingsImpl) then) = - __$$BookSettingsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({String bookId, NullablePlayerSettings playerSettings}); - @override - $NullablePlayerSettingsCopyWith<$Res> get playerSettings; +/// Adds pattern-matching-related methods to [BookSettings]. +extension BookSettingsPatterns on BookSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _BookSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _BookSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _BookSettings value) $default,){ +final _that = this; +switch (_that) { +case _BookSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _BookSettings value)? $default,){ +final _that = this; +switch (_that) { +case _BookSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String bookId, NullablePlayerSettings playerSettings)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _BookSettings() when $default != null: +return $default(_that.bookId,_that.playerSettings);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String bookId, NullablePlayerSettings playerSettings) $default,) {final _that = this; +switch (_that) { +case _BookSettings(): +return $default(_that.bookId,_that.playerSettings);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String bookId, NullablePlayerSettings playerSettings)? $default,) {final _that = this; +switch (_that) { +case _BookSettings() when $default != null: +return $default(_that.bookId,_that.playerSettings);case _: + return null; + +} } -/// @nodoc -class __$$BookSettingsImplCopyWithImpl<$Res> - extends _$BookSettingsCopyWithImpl<$Res, _$BookSettingsImpl> - implements _$$BookSettingsImplCopyWith<$Res> { - __$$BookSettingsImplCopyWithImpl( - _$BookSettingsImpl _value, $Res Function(_$BookSettingsImpl) _then) - : super(_value, _then); - - /// Create a copy of BookSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? bookId = null, - Object? playerSettings = null, - }) { - return _then(_$BookSettingsImpl( - bookId: null == bookId - ? _value.bookId - : bookId // ignore: cast_nullable_to_non_nullable - as String, - playerSettings: null == playerSettings - ? _value.playerSettings - : playerSettings // ignore: cast_nullable_to_non_nullable - as NullablePlayerSettings, - )); - } } /// @nodoc @JsonSerializable() -class _$BookSettingsImpl implements _BookSettings { - const _$BookSettingsImpl( - {required this.bookId, - this.playerSettings = const NullablePlayerSettings()}); - factory _$BookSettingsImpl.fromJson(Map json) => - _$$BookSettingsImplFromJson(json); +class _BookSettings implements BookSettings { + const _BookSettings({required this.bookId, this.playerSettings = const NullablePlayerSettings()}); + factory _BookSettings.fromJson(Map json) => _$BookSettingsFromJson(json); - @override - final String bookId; - @override - @JsonKey() - final NullablePlayerSettings playerSettings; +@override final String bookId; +@override@JsonKey() final NullablePlayerSettings playerSettings; - @override - String toString() { - return 'BookSettings(bookId: $bookId, playerSettings: $playerSettings)'; - } +/// Create a copy of BookSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$BookSettingsCopyWith<_BookSettings> get copyWith => __$BookSettingsCopyWithImpl<_BookSettings>(this, _$identity); - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$BookSettingsImpl && - (identical(other.bookId, bookId) || other.bookId == bookId) && - (identical(other.playerSettings, playerSettings) || - other.playerSettings == playerSettings)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, bookId, playerSettings); - - /// Create a copy of BookSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$BookSettingsImplCopyWith<_$BookSettingsImpl> get copyWith => - __$$BookSettingsImplCopyWithImpl<_$BookSettingsImpl>(this, _$identity); - - @override - Map toJson() { - return _$$BookSettingsImplToJson( - this, - ); - } +@override +Map toJson() { + return _$BookSettingsToJson(this, ); } -abstract class _BookSettings implements BookSettings { - const factory _BookSettings( - {required final String bookId, - final NullablePlayerSettings playerSettings}) = _$BookSettingsImpl; - - factory _BookSettings.fromJson(Map json) = - _$BookSettingsImpl.fromJson; - - @override - String get bookId; - @override - NullablePlayerSettings get playerSettings; - - /// Create a copy of BookSettings - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$BookSettingsImplCopyWith<_$BookSettingsImpl> get copyWith => - throw _privateConstructorUsedError; +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _BookSettings&&(identical(other.bookId, bookId) || other.bookId == bookId)&&(identical(other.playerSettings, playerSettings) || other.playerSettings == playerSettings)); } + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,bookId,playerSettings); + +@override +String toString() { + return 'BookSettings(bookId: $bookId, playerSettings: $playerSettings)'; +} + + +} + +/// @nodoc +abstract mixin class _$BookSettingsCopyWith<$Res> implements $BookSettingsCopyWith<$Res> { + factory _$BookSettingsCopyWith(_BookSettings value, $Res Function(_BookSettings) _then) = __$BookSettingsCopyWithImpl; +@override @useResult +$Res call({ + String bookId, NullablePlayerSettings playerSettings +}); + + +@override $NullablePlayerSettingsCopyWith<$Res> get playerSettings; + +} +/// @nodoc +class __$BookSettingsCopyWithImpl<$Res> + implements _$BookSettingsCopyWith<$Res> { + __$BookSettingsCopyWithImpl(this._self, this._then); + + final _BookSettings _self; + final $Res Function(_BookSettings) _then; + +/// Create a copy of BookSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? bookId = null,Object? playerSettings = null,}) { + return _then(_BookSettings( +bookId: null == bookId ? _self.bookId : bookId // ignore: cast_nullable_to_non_nullable +as String,playerSettings: null == playerSettings ? _self.playerSettings : playerSettings // ignore: cast_nullable_to_non_nullable +as NullablePlayerSettings, + )); +} + +/// Create a copy of BookSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$NullablePlayerSettingsCopyWith<$Res> get playerSettings { + + return $NullablePlayerSettingsCopyWith<$Res>(_self.playerSettings, (value) { + return _then(_self.copyWith(playerSettings: value)); + }); +} +} + +// dart format on diff --git a/lib/features/per_book_settings/models/book_settings.g.dart b/lib/features/per_book_settings/models/book_settings.g.dart index a66fba1..b5888f7 100644 --- a/lib/features/per_book_settings/models/book_settings.g.dart +++ b/lib/features/per_book_settings/models/book_settings.g.dart @@ -6,16 +6,17 @@ part of 'book_settings.dart'; // JsonSerializableGenerator // ************************************************************************** -_$BookSettingsImpl _$$BookSettingsImplFromJson(Map json) => - _$BookSettingsImpl( +_BookSettings _$BookSettingsFromJson(Map json) => + _BookSettings( bookId: json['bookId'] as String, playerSettings: json['playerSettings'] == null ? const NullablePlayerSettings() : NullablePlayerSettings.fromJson( - json['playerSettings'] as Map), + json['playerSettings'] as Map, + ), ); -Map _$$BookSettingsImplToJson(_$BookSettingsImpl instance) => +Map _$BookSettingsToJson(_BookSettings instance) => { 'bookId': instance.bookId, 'playerSettings': instance.playerSettings, diff --git a/lib/features/per_book_settings/models/nullable_player_settings.dart b/lib/features/per_book_settings/models/nullable_player_settings.dart index 39135d7..d54e2eb 100644 --- a/lib/features/per_book_settings/models/nullable_player_settings.dart +++ b/lib/features/per_book_settings/models/nullable_player_settings.dart @@ -5,7 +5,7 @@ part 'nullable_player_settings.freezed.dart'; part 'nullable_player_settings.g.dart'; @freezed -class NullablePlayerSettings with _$NullablePlayerSettings { +sealed class NullablePlayerSettings with _$NullablePlayerSettings { const factory NullablePlayerSettings({ MinimizedPlayerSettings? miniPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings, diff --git a/lib/features/per_book_settings/models/nullable_player_settings.freezed.dart b/lib/features/per_book_settings/models/nullable_player_settings.freezed.dart index 9450ed4..8039d60 100644 --- a/lib/features/per_book_settings/models/nullable_player_settings.freezed.dart +++ b/lib/features/per_book_settings/models/nullable_player_settings.freezed.dart @@ -1,5 +1,5 @@ -// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file // 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 @@ -9,369 +9,361 @@ part of 'nullable_player_settings.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -NullablePlayerSettings _$NullablePlayerSettingsFromJson( - Map json) { - return _NullablePlayerSettings.fromJson(json); -} - /// @nodoc mixin _$NullablePlayerSettings { - MinimizedPlayerSettings? get miniPlayerSettings => - throw _privateConstructorUsedError; - ExpandedPlayerSettings? get expandedPlayerSettings => - throw _privateConstructorUsedError; - double? get preferredDefaultVolume => throw _privateConstructorUsedError; - double? get preferredDefaultSpeed => throw _privateConstructorUsedError; - List? get speedOptions => throw _privateConstructorUsedError; - SleepTimerSettings? get sleepTimerSettings => - throw _privateConstructorUsedError; - Duration? get playbackReportInterval => throw _privateConstructorUsedError; + + MinimizedPlayerSettings? get miniPlayerSettings; ExpandedPlayerSettings? get expandedPlayerSettings; double? get preferredDefaultVolume; double? get preferredDefaultSpeed; List? get speedOptions; SleepTimerSettings? get sleepTimerSettings; Duration? get playbackReportInterval; +/// Create a copy of NullablePlayerSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NullablePlayerSettingsCopyWith get copyWith => _$NullablePlayerSettingsCopyWithImpl(this as NullablePlayerSettings, _$identity); /// Serializes this NullablePlayerSettings to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NullablePlayerSettings&&(identical(other.miniPlayerSettings, miniPlayerSettings) || other.miniPlayerSettings == miniPlayerSettings)&&(identical(other.expandedPlayerSettings, expandedPlayerSettings) || other.expandedPlayerSettings == expandedPlayerSettings)&&(identical(other.preferredDefaultVolume, preferredDefaultVolume) || other.preferredDefaultVolume == preferredDefaultVolume)&&(identical(other.preferredDefaultSpeed, preferredDefaultSpeed) || other.preferredDefaultSpeed == preferredDefaultSpeed)&&const DeepCollectionEquality().equals(other.speedOptions, speedOptions)&&(identical(other.sleepTimerSettings, sleepTimerSettings) || other.sleepTimerSettings == sleepTimerSettings)&&(identical(other.playbackReportInterval, playbackReportInterval) || other.playbackReportInterval == playbackReportInterval)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,miniPlayerSettings,expandedPlayerSettings,preferredDefaultVolume,preferredDefaultSpeed,const DeepCollectionEquality().hash(speedOptions),sleepTimerSettings,playbackReportInterval); + +@override +String toString() { + return 'NullablePlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, playbackReportInterval: $playbackReportInterval)'; +} + - /// Create a copy of NullablePlayerSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $NullablePlayerSettingsCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $NullablePlayerSettingsCopyWith<$Res> { - factory $NullablePlayerSettingsCopyWith(NullablePlayerSettings value, - $Res Function(NullablePlayerSettings) then) = - _$NullablePlayerSettingsCopyWithImpl<$Res, NullablePlayerSettings>; - @useResult - $Res call( - {MinimizedPlayerSettings? miniPlayerSettings, - ExpandedPlayerSettings? expandedPlayerSettings, - double? preferredDefaultVolume, - double? preferredDefaultSpeed, - List? speedOptions, - SleepTimerSettings? sleepTimerSettings, - Duration? playbackReportInterval}); +abstract mixin class $NullablePlayerSettingsCopyWith<$Res> { + factory $NullablePlayerSettingsCopyWith(NullablePlayerSettings value, $Res Function(NullablePlayerSettings) _then) = _$NullablePlayerSettingsCopyWithImpl; +@useResult +$Res call({ + MinimizedPlayerSettings? miniPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings, double? preferredDefaultVolume, double? preferredDefaultSpeed, List? speedOptions, SleepTimerSettings? sleepTimerSettings, Duration? playbackReportInterval +}); + + +$MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings;$ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings;$SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings; - $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings; - $ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings; - $SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings; } - /// @nodoc -class _$NullablePlayerSettingsCopyWithImpl<$Res, - $Val extends NullablePlayerSettings> +class _$NullablePlayerSettingsCopyWithImpl<$Res> implements $NullablePlayerSettingsCopyWith<$Res> { - _$NullablePlayerSettingsCopyWithImpl(this._value, this._then); + _$NullablePlayerSettingsCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final NullablePlayerSettings _self; + final $Res Function(NullablePlayerSettings) _then; - /// Create a copy of NullablePlayerSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? miniPlayerSettings = freezed, - Object? expandedPlayerSettings = freezed, - Object? preferredDefaultVolume = freezed, - Object? preferredDefaultSpeed = freezed, - Object? speedOptions = freezed, - Object? sleepTimerSettings = freezed, - Object? playbackReportInterval = freezed, - }) { - return _then(_value.copyWith( - miniPlayerSettings: freezed == miniPlayerSettings - ? _value.miniPlayerSettings - : miniPlayerSettings // ignore: cast_nullable_to_non_nullable - as MinimizedPlayerSettings?, - expandedPlayerSettings: freezed == expandedPlayerSettings - ? _value.expandedPlayerSettings - : expandedPlayerSettings // ignore: cast_nullable_to_non_nullable - as ExpandedPlayerSettings?, - preferredDefaultVolume: freezed == preferredDefaultVolume - ? _value.preferredDefaultVolume - : preferredDefaultVolume // ignore: cast_nullable_to_non_nullable - as double?, - preferredDefaultSpeed: freezed == preferredDefaultSpeed - ? _value.preferredDefaultSpeed - : preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable - as double?, - speedOptions: freezed == speedOptions - ? _value.speedOptions - : speedOptions // ignore: cast_nullable_to_non_nullable - as List?, - sleepTimerSettings: freezed == sleepTimerSettings - ? _value.sleepTimerSettings - : sleepTimerSettings // ignore: cast_nullable_to_non_nullable - as SleepTimerSettings?, - playbackReportInterval: freezed == playbackReportInterval - ? _value.playbackReportInterval - : playbackReportInterval // ignore: cast_nullable_to_non_nullable - as Duration?, - ) as $Val); +/// Create a copy of NullablePlayerSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? miniPlayerSettings = freezed,Object? expandedPlayerSettings = freezed,Object? preferredDefaultVolume = freezed,Object? preferredDefaultSpeed = freezed,Object? speedOptions = freezed,Object? sleepTimerSettings = freezed,Object? playbackReportInterval = freezed,}) { + return _then(_self.copyWith( +miniPlayerSettings: freezed == miniPlayerSettings ? _self.miniPlayerSettings : miniPlayerSettings // ignore: cast_nullable_to_non_nullable +as MinimizedPlayerSettings?,expandedPlayerSettings: freezed == expandedPlayerSettings ? _self.expandedPlayerSettings : expandedPlayerSettings // ignore: cast_nullable_to_non_nullable +as ExpandedPlayerSettings?,preferredDefaultVolume: freezed == preferredDefaultVolume ? _self.preferredDefaultVolume : preferredDefaultVolume // ignore: cast_nullable_to_non_nullable +as double?,preferredDefaultSpeed: freezed == preferredDefaultSpeed ? _self.preferredDefaultSpeed : preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable +as double?,speedOptions: freezed == speedOptions ? _self.speedOptions : speedOptions // ignore: cast_nullable_to_non_nullable +as List?,sleepTimerSettings: freezed == sleepTimerSettings ? _self.sleepTimerSettings : sleepTimerSettings // ignore: cast_nullable_to_non_nullable +as SleepTimerSettings?,playbackReportInterval: freezed == playbackReportInterval ? _self.playbackReportInterval : playbackReportInterval // ignore: cast_nullable_to_non_nullable +as Duration?, + )); +} +/// Create a copy of NullablePlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings { + if (_self.miniPlayerSettings == null) { + return null; } - /// Create a copy of NullablePlayerSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings { - if (_value.miniPlayerSettings == null) { - return null; - } - - return $MinimizedPlayerSettingsCopyWith<$Res>(_value.miniPlayerSettings!, - (value) { - return _then(_value.copyWith(miniPlayerSettings: value) as $Val); - }); + return $MinimizedPlayerSettingsCopyWith<$Res>(_self.miniPlayerSettings!, (value) { + return _then(_self.copyWith(miniPlayerSettings: value)); + }); +}/// Create a copy of NullablePlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings { + if (_self.expandedPlayerSettings == null) { + return null; } - /// Create a copy of NullablePlayerSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings { - if (_value.expandedPlayerSettings == null) { - return null; - } - - return $ExpandedPlayerSettingsCopyWith<$Res>(_value.expandedPlayerSettings!, - (value) { - return _then(_value.copyWith(expandedPlayerSettings: value) as $Val); - }); + return $ExpandedPlayerSettingsCopyWith<$Res>(_self.expandedPlayerSettings!, (value) { + return _then(_self.copyWith(expandedPlayerSettings: value)); + }); +}/// Create a copy of NullablePlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings { + if (_self.sleepTimerSettings == null) { + return null; } - /// Create a copy of NullablePlayerSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings { - if (_value.sleepTimerSettings == null) { - return null; - } - - return $SleepTimerSettingsCopyWith<$Res>(_value.sleepTimerSettings!, - (value) { - return _then(_value.copyWith(sleepTimerSettings: value) as $Val); - }); - } + return $SleepTimerSettingsCopyWith<$Res>(_self.sleepTimerSettings!, (value) { + return _then(_self.copyWith(sleepTimerSettings: value)); + }); +} } -/// @nodoc -abstract class _$$NullablePlayerSettingsImplCopyWith<$Res> - implements $NullablePlayerSettingsCopyWith<$Res> { - factory _$$NullablePlayerSettingsImplCopyWith( - _$NullablePlayerSettingsImpl value, - $Res Function(_$NullablePlayerSettingsImpl) then) = - __$$NullablePlayerSettingsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {MinimizedPlayerSettings? miniPlayerSettings, - ExpandedPlayerSettings? expandedPlayerSettings, - double? preferredDefaultVolume, - double? preferredDefaultSpeed, - List? speedOptions, - SleepTimerSettings? sleepTimerSettings, - Duration? playbackReportInterval}); - @override - $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings; - @override - $ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings; - @override - $SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings; +/// Adds pattern-matching-related methods to [NullablePlayerSettings]. +extension NullablePlayerSettingsPatterns on NullablePlayerSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _NullablePlayerSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _NullablePlayerSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _NullablePlayerSettings value) $default,){ +final _that = this; +switch (_that) { +case _NullablePlayerSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _NullablePlayerSettings value)? $default,){ +final _that = this; +switch (_that) { +case _NullablePlayerSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( MinimizedPlayerSettings? miniPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings, double? preferredDefaultVolume, double? preferredDefaultSpeed, List? speedOptions, SleepTimerSettings? sleepTimerSettings, Duration? playbackReportInterval)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _NullablePlayerSettings() when $default != null: +return $default(_that.miniPlayerSettings,_that.expandedPlayerSettings,_that.preferredDefaultVolume,_that.preferredDefaultSpeed,_that.speedOptions,_that.sleepTimerSettings,_that.playbackReportInterval);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( MinimizedPlayerSettings? miniPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings, double? preferredDefaultVolume, double? preferredDefaultSpeed, List? speedOptions, SleepTimerSettings? sleepTimerSettings, Duration? playbackReportInterval) $default,) {final _that = this; +switch (_that) { +case _NullablePlayerSettings(): +return $default(_that.miniPlayerSettings,_that.expandedPlayerSettings,_that.preferredDefaultVolume,_that.preferredDefaultSpeed,_that.speedOptions,_that.sleepTimerSettings,_that.playbackReportInterval);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( MinimizedPlayerSettings? miniPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings, double? preferredDefaultVolume, double? preferredDefaultSpeed, List? speedOptions, SleepTimerSettings? sleepTimerSettings, Duration? playbackReportInterval)? $default,) {final _that = this; +switch (_that) { +case _NullablePlayerSettings() when $default != null: +return $default(_that.miniPlayerSettings,_that.expandedPlayerSettings,_that.preferredDefaultVolume,_that.preferredDefaultSpeed,_that.speedOptions,_that.sleepTimerSettings,_that.playbackReportInterval);case _: + return null; + +} } -/// @nodoc -class __$$NullablePlayerSettingsImplCopyWithImpl<$Res> - extends _$NullablePlayerSettingsCopyWithImpl<$Res, - _$NullablePlayerSettingsImpl> - implements _$$NullablePlayerSettingsImplCopyWith<$Res> { - __$$NullablePlayerSettingsImplCopyWithImpl( - _$NullablePlayerSettingsImpl _value, - $Res Function(_$NullablePlayerSettingsImpl) _then) - : super(_value, _then); - - /// Create a copy of NullablePlayerSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? miniPlayerSettings = freezed, - Object? expandedPlayerSettings = freezed, - Object? preferredDefaultVolume = freezed, - Object? preferredDefaultSpeed = freezed, - Object? speedOptions = freezed, - Object? sleepTimerSettings = freezed, - Object? playbackReportInterval = freezed, - }) { - return _then(_$NullablePlayerSettingsImpl( - miniPlayerSettings: freezed == miniPlayerSettings - ? _value.miniPlayerSettings - : miniPlayerSettings // ignore: cast_nullable_to_non_nullable - as MinimizedPlayerSettings?, - expandedPlayerSettings: freezed == expandedPlayerSettings - ? _value.expandedPlayerSettings - : expandedPlayerSettings // ignore: cast_nullable_to_non_nullable - as ExpandedPlayerSettings?, - preferredDefaultVolume: freezed == preferredDefaultVolume - ? _value.preferredDefaultVolume - : preferredDefaultVolume // ignore: cast_nullable_to_non_nullable - as double?, - preferredDefaultSpeed: freezed == preferredDefaultSpeed - ? _value.preferredDefaultSpeed - : preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable - as double?, - speedOptions: freezed == speedOptions - ? _value._speedOptions - : speedOptions // ignore: cast_nullable_to_non_nullable - as List?, - sleepTimerSettings: freezed == sleepTimerSettings - ? _value.sleepTimerSettings - : sleepTimerSettings // ignore: cast_nullable_to_non_nullable - as SleepTimerSettings?, - playbackReportInterval: freezed == playbackReportInterval - ? _value.playbackReportInterval - : playbackReportInterval // ignore: cast_nullable_to_non_nullable - as Duration?, - )); - } } /// @nodoc @JsonSerializable() -class _$NullablePlayerSettingsImpl implements _NullablePlayerSettings { - const _$NullablePlayerSettingsImpl( - {this.miniPlayerSettings, - this.expandedPlayerSettings, - this.preferredDefaultVolume, - this.preferredDefaultSpeed, - final List? speedOptions, - this.sleepTimerSettings, - this.playbackReportInterval}) - : _speedOptions = speedOptions; - factory _$NullablePlayerSettingsImpl.fromJson(Map json) => - _$$NullablePlayerSettingsImplFromJson(json); +class _NullablePlayerSettings implements NullablePlayerSettings { + const _NullablePlayerSettings({this.miniPlayerSettings, this.expandedPlayerSettings, this.preferredDefaultVolume, this.preferredDefaultSpeed, final List? speedOptions, this.sleepTimerSettings, this.playbackReportInterval}): _speedOptions = speedOptions; + factory _NullablePlayerSettings.fromJson(Map json) => _$NullablePlayerSettingsFromJson(json); - @override - final MinimizedPlayerSettings? miniPlayerSettings; - @override - final ExpandedPlayerSettings? expandedPlayerSettings; - @override - final double? preferredDefaultVolume; - @override - final double? preferredDefaultSpeed; - final List? _speedOptions; - @override - List? get speedOptions { - final value = _speedOptions; - if (value == null) return null; - if (_speedOptions is EqualUnmodifiableListView) return _speedOptions; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); - } - - @override - final SleepTimerSettings? sleepTimerSettings; - @override - final Duration? playbackReportInterval; - - @override - String toString() { - return 'NullablePlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, playbackReportInterval: $playbackReportInterval)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$NullablePlayerSettingsImpl && - (identical(other.miniPlayerSettings, miniPlayerSettings) || - other.miniPlayerSettings == miniPlayerSettings) && - (identical(other.expandedPlayerSettings, expandedPlayerSettings) || - other.expandedPlayerSettings == expandedPlayerSettings) && - (identical(other.preferredDefaultVolume, preferredDefaultVolume) || - other.preferredDefaultVolume == preferredDefaultVolume) && - (identical(other.preferredDefaultSpeed, preferredDefaultSpeed) || - other.preferredDefaultSpeed == preferredDefaultSpeed) && - const DeepCollectionEquality() - .equals(other._speedOptions, _speedOptions) && - (identical(other.sleepTimerSettings, sleepTimerSettings) || - other.sleepTimerSettings == sleepTimerSettings) && - (identical(other.playbackReportInterval, playbackReportInterval) || - other.playbackReportInterval == playbackReportInterval)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - miniPlayerSettings, - expandedPlayerSettings, - preferredDefaultVolume, - preferredDefaultSpeed, - const DeepCollectionEquality().hash(_speedOptions), - sleepTimerSettings, - playbackReportInterval); - - /// Create a copy of NullablePlayerSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$NullablePlayerSettingsImplCopyWith<_$NullablePlayerSettingsImpl> - get copyWith => __$$NullablePlayerSettingsImplCopyWithImpl< - _$NullablePlayerSettingsImpl>(this, _$identity); - - @override - Map toJson() { - return _$$NullablePlayerSettingsImplToJson( - this, - ); - } +@override final MinimizedPlayerSettings? miniPlayerSettings; +@override final ExpandedPlayerSettings? expandedPlayerSettings; +@override final double? preferredDefaultVolume; +@override final double? preferredDefaultSpeed; + final List? _speedOptions; +@override List? get speedOptions { + final value = _speedOptions; + if (value == null) return null; + if (_speedOptions is EqualUnmodifiableListView) return _speedOptions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); } -abstract class _NullablePlayerSettings implements NullablePlayerSettings { - const factory _NullablePlayerSettings( - {final MinimizedPlayerSettings? miniPlayerSettings, - final ExpandedPlayerSettings? expandedPlayerSettings, - final double? preferredDefaultVolume, - final double? preferredDefaultSpeed, - final List? speedOptions, - final SleepTimerSettings? sleepTimerSettings, - final Duration? playbackReportInterval}) = _$NullablePlayerSettingsImpl; +@override final SleepTimerSettings? sleepTimerSettings; +@override final Duration? playbackReportInterval; - factory _NullablePlayerSettings.fromJson(Map json) = - _$NullablePlayerSettingsImpl.fromJson; +/// Create a copy of NullablePlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$NullablePlayerSettingsCopyWith<_NullablePlayerSettings> get copyWith => __$NullablePlayerSettingsCopyWithImpl<_NullablePlayerSettings>(this, _$identity); - @override - MinimizedPlayerSettings? get miniPlayerSettings; - @override - ExpandedPlayerSettings? get expandedPlayerSettings; - @override - double? get preferredDefaultVolume; - @override - double? get preferredDefaultSpeed; - @override - List? get speedOptions; - @override - SleepTimerSettings? get sleepTimerSettings; - @override - Duration? get playbackReportInterval; - - /// Create a copy of NullablePlayerSettings - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$NullablePlayerSettingsImplCopyWith<_$NullablePlayerSettingsImpl> - get copyWith => throw _privateConstructorUsedError; +@override +Map toJson() { + return _$NullablePlayerSettingsToJson(this, ); } + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _NullablePlayerSettings&&(identical(other.miniPlayerSettings, miniPlayerSettings) || other.miniPlayerSettings == miniPlayerSettings)&&(identical(other.expandedPlayerSettings, expandedPlayerSettings) || other.expandedPlayerSettings == expandedPlayerSettings)&&(identical(other.preferredDefaultVolume, preferredDefaultVolume) || other.preferredDefaultVolume == preferredDefaultVolume)&&(identical(other.preferredDefaultSpeed, preferredDefaultSpeed) || other.preferredDefaultSpeed == preferredDefaultSpeed)&&const DeepCollectionEquality().equals(other._speedOptions, _speedOptions)&&(identical(other.sleepTimerSettings, sleepTimerSettings) || other.sleepTimerSettings == sleepTimerSettings)&&(identical(other.playbackReportInterval, playbackReportInterval) || other.playbackReportInterval == playbackReportInterval)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,miniPlayerSettings,expandedPlayerSettings,preferredDefaultVolume,preferredDefaultSpeed,const DeepCollectionEquality().hash(_speedOptions),sleepTimerSettings,playbackReportInterval); + +@override +String toString() { + return 'NullablePlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, playbackReportInterval: $playbackReportInterval)'; +} + + +} + +/// @nodoc +abstract mixin class _$NullablePlayerSettingsCopyWith<$Res> implements $NullablePlayerSettingsCopyWith<$Res> { + factory _$NullablePlayerSettingsCopyWith(_NullablePlayerSettings value, $Res Function(_NullablePlayerSettings) _then) = __$NullablePlayerSettingsCopyWithImpl; +@override @useResult +$Res call({ + MinimizedPlayerSettings? miniPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings, double? preferredDefaultVolume, double? preferredDefaultSpeed, List? speedOptions, SleepTimerSettings? sleepTimerSettings, Duration? playbackReportInterval +}); + + +@override $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings;@override $ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings;@override $SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings; + +} +/// @nodoc +class __$NullablePlayerSettingsCopyWithImpl<$Res> + implements _$NullablePlayerSettingsCopyWith<$Res> { + __$NullablePlayerSettingsCopyWithImpl(this._self, this._then); + + final _NullablePlayerSettings _self; + final $Res Function(_NullablePlayerSettings) _then; + +/// Create a copy of NullablePlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? miniPlayerSettings = freezed,Object? expandedPlayerSettings = freezed,Object? preferredDefaultVolume = freezed,Object? preferredDefaultSpeed = freezed,Object? speedOptions = freezed,Object? sleepTimerSettings = freezed,Object? playbackReportInterval = freezed,}) { + return _then(_NullablePlayerSettings( +miniPlayerSettings: freezed == miniPlayerSettings ? _self.miniPlayerSettings : miniPlayerSettings // ignore: cast_nullable_to_non_nullable +as MinimizedPlayerSettings?,expandedPlayerSettings: freezed == expandedPlayerSettings ? _self.expandedPlayerSettings : expandedPlayerSettings // ignore: cast_nullable_to_non_nullable +as ExpandedPlayerSettings?,preferredDefaultVolume: freezed == preferredDefaultVolume ? _self.preferredDefaultVolume : preferredDefaultVolume // ignore: cast_nullable_to_non_nullable +as double?,preferredDefaultSpeed: freezed == preferredDefaultSpeed ? _self.preferredDefaultSpeed : preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable +as double?,speedOptions: freezed == speedOptions ? _self._speedOptions : speedOptions // ignore: cast_nullable_to_non_nullable +as List?,sleepTimerSettings: freezed == sleepTimerSettings ? _self.sleepTimerSettings : sleepTimerSettings // ignore: cast_nullable_to_non_nullable +as SleepTimerSettings?,playbackReportInterval: freezed == playbackReportInterval ? _self.playbackReportInterval : playbackReportInterval // ignore: cast_nullable_to_non_nullable +as Duration?, + )); +} + +/// Create a copy of NullablePlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings { + if (_self.miniPlayerSettings == null) { + return null; + } + + return $MinimizedPlayerSettingsCopyWith<$Res>(_self.miniPlayerSettings!, (value) { + return _then(_self.copyWith(miniPlayerSettings: value)); + }); +}/// Create a copy of NullablePlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings { + if (_self.expandedPlayerSettings == null) { + return null; + } + + return $ExpandedPlayerSettingsCopyWith<$Res>(_self.expandedPlayerSettings!, (value) { + return _then(_self.copyWith(expandedPlayerSettings: value)); + }); +}/// Create a copy of NullablePlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings { + if (_self.sleepTimerSettings == null) { + return null; + } + + return $SleepTimerSettingsCopyWith<$Res>(_self.sleepTimerSettings!, (value) { + return _then(_self.copyWith(sleepTimerSettings: value)); + }); +} +} + +// dart format on diff --git a/lib/features/per_book_settings/models/nullable_player_settings.g.dart b/lib/features/per_book_settings/models/nullable_player_settings.g.dart index 28eb3bc..3c45864 100644 --- a/lib/features/per_book_settings/models/nullable_player_settings.g.dart +++ b/lib/features/per_book_settings/models/nullable_player_settings.g.dart @@ -6,42 +6,42 @@ part of 'nullable_player_settings.dart'; // JsonSerializableGenerator // ************************************************************************** -_$NullablePlayerSettingsImpl _$$NullablePlayerSettingsImplFromJson( - Map json) => - _$NullablePlayerSettingsImpl( - miniPlayerSettings: json['miniPlayerSettings'] == null - ? null - : MinimizedPlayerSettings.fromJson( - json['miniPlayerSettings'] as Map), - expandedPlayerSettings: json['expandedPlayerSettings'] == null - ? null - : ExpandedPlayerSettings.fromJson( - json['expandedPlayerSettings'] as Map), - preferredDefaultVolume: - (json['preferredDefaultVolume'] as num?)?.toDouble(), - preferredDefaultSpeed: - (json['preferredDefaultSpeed'] as num?)?.toDouble(), - speedOptions: (json['speedOptions'] as List?) - ?.map((e) => (e as num).toDouble()) - .toList(), - sleepTimerSettings: json['sleepTimerSettings'] == null - ? null - : SleepTimerSettings.fromJson( - json['sleepTimerSettings'] as Map), - playbackReportInterval: json['playbackReportInterval'] == null - ? null - : Duration( - microseconds: (json['playbackReportInterval'] as num).toInt()), - ); +_NullablePlayerSettings _$NullablePlayerSettingsFromJson( + Map json, +) => _NullablePlayerSettings( + miniPlayerSettings: json['miniPlayerSettings'] == null + ? null + : MinimizedPlayerSettings.fromJson( + json['miniPlayerSettings'] as Map, + ), + expandedPlayerSettings: json['expandedPlayerSettings'] == null + ? null + : ExpandedPlayerSettings.fromJson( + json['expandedPlayerSettings'] as Map, + ), + preferredDefaultVolume: (json['preferredDefaultVolume'] as num?)?.toDouble(), + preferredDefaultSpeed: (json['preferredDefaultSpeed'] as num?)?.toDouble(), + speedOptions: (json['speedOptions'] as List?) + ?.map((e) => (e as num).toDouble()) + .toList(), + sleepTimerSettings: json['sleepTimerSettings'] == null + ? null + : SleepTimerSettings.fromJson( + json['sleepTimerSettings'] as Map, + ), + playbackReportInterval: json['playbackReportInterval'] == null + ? null + : Duration(microseconds: (json['playbackReportInterval'] as num).toInt()), +); -Map _$$NullablePlayerSettingsImplToJson( - _$NullablePlayerSettingsImpl instance) => - { - 'miniPlayerSettings': instance.miniPlayerSettings, - 'expandedPlayerSettings': instance.expandedPlayerSettings, - 'preferredDefaultVolume': instance.preferredDefaultVolume, - 'preferredDefaultSpeed': instance.preferredDefaultSpeed, - 'speedOptions': instance.speedOptions, - 'sleepTimerSettings': instance.sleepTimerSettings, - 'playbackReportInterval': instance.playbackReportInterval?.inMicroseconds, - }; +Map _$NullablePlayerSettingsToJson( + _NullablePlayerSettings instance, +) => { + 'miniPlayerSettings': instance.miniPlayerSettings, + 'expandedPlayerSettings': instance.expandedPlayerSettings, + 'preferredDefaultVolume': instance.preferredDefaultVolume, + 'preferredDefaultSpeed': instance.preferredDefaultSpeed, + 'speedOptions': instance.speedOptions, + 'sleepTimerSettings': instance.sleepTimerSettings, + 'playbackReportInterval': instance.playbackReportInterval?.inMicroseconds, +}; diff --git a/lib/features/per_book_settings/providers/book_settings_provider.g.dart b/lib/features/per_book_settings/providers/book_settings_provider.g.dart index 943bd55..b8ed222 100644 --- a/lib/features/per_book_settings/providers/book_settings_provider.g.dart +++ b/lib/features/per_book_settings/providers/book_settings_provider.g.dart @@ -6,169 +6,102 @@ part of 'book_settings_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$bookSettingsHash() => r'b976df954edf98ec6ccb3eb41e9d07dd4a9193eb'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -abstract class _$BookSettings - extends BuildlessAutoDisposeNotifier { - late final String bookId; - - model.BookSettings build( - String bookId, - ); -} - -/// See also [BookSettings]. @ProviderFor(BookSettings) -const bookSettingsProvider = BookSettingsFamily(); +final bookSettingsProvider = BookSettingsFamily._(); -/// See also [BookSettings]. -class BookSettingsFamily extends Family { - /// See also [BookSettings]. - const BookSettingsFamily(); +final class BookSettingsProvider + extends $NotifierProvider { + BookSettingsProvider._({ + required BookSettingsFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'bookSettingsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); - /// See also [BookSettings]. - BookSettingsProvider call( - String bookId, - ) { - return BookSettingsProvider( - bookId, - ); + @override + String debugGetCreateSourceHash() => _$bookSettingsHash(); + + @override + String toString() { + return r'bookSettingsProvider' + '' + '($argument)'; } + @$internal @override - BookSettingsProvider getProviderOverride( - covariant BookSettingsProvider provider, - ) { - return call( - provider.bookId, - ); - } + BookSettings create() => BookSettings(); - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'bookSettingsProvider'; -} - -/// See also [BookSettings]. -class BookSettingsProvider - extends AutoDisposeNotifierProviderImpl { - /// See also [BookSettings]. - BookSettingsProvider( - String bookId, - ) : this._internal( - () => BookSettings()..bookId = bookId, - from: bookSettingsProvider, - name: r'bookSettingsProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$bookSettingsHash, - dependencies: BookSettingsFamily._dependencies, - allTransitiveDependencies: - BookSettingsFamily._allTransitiveDependencies, - bookId: bookId, - ); - - BookSettingsProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.bookId, - }) : super.internal(); - - final String bookId; - - @override - model.BookSettings runNotifierBuild( - covariant BookSettings notifier, - ) { - return notifier.build( - bookId, - ); - } - - @override - Override overrideWith(BookSettings Function() create) { - return ProviderOverride( + /// {@macro riverpod.override_with_value} + Override overrideWithValue(model.BookSettings value) { + return $ProviderOverride( origin: this, - override: BookSettingsProvider._internal( - () => create()..bookId = bookId, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - bookId: bookId, - ), + providerOverride: $SyncValueProvider(value), ); } - @override - AutoDisposeNotifierProviderElement - createElement() { - return _BookSettingsProviderElement(this); - } - @override bool operator ==(Object other) { - return other is BookSettingsProvider && other.bookId == bookId; + return other is BookSettingsProvider && other.argument == argument; } @override int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, bookId.hashCode); - - return _SystemHash.finish(hash); + return argument.hashCode; } } -mixin BookSettingsRef on AutoDisposeNotifierProviderRef { - /// The parameter `bookId` of this provider. - String get bookId; -} +String _$bookSettingsHash() => r'b976df954edf98ec6ccb3eb41e9d07dd4a9193eb'; -class _BookSettingsProviderElement - extends AutoDisposeNotifierProviderElement - with BookSettingsRef { - _BookSettingsProviderElement(super.provider); +final class BookSettingsFamily extends $Family + with + $ClassFamilyOverride< + BookSettings, + model.BookSettings, + model.BookSettings, + model.BookSettings, + String + > { + BookSettingsFamily._() + : super( + retry: null, + name: r'bookSettingsProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + BookSettingsProvider call(String bookId) => + BookSettingsProvider._(argument: bookId, from: this); @override - String get bookId => (origin as BookSettingsProvider).bookId; + String toString() => r'bookSettingsProvider'; +} + +abstract class _$BookSettings extends $Notifier { + late final _$args = ref.$arg as String; + String get bookId => _$args; + + model.BookSettings build(String bookId); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + model.BookSettings, + Object?, + Object? + >; + element.handleCreate(ref, () => build(_$args)); + } } -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/features/playback_reporting/core/playback_reporter.dart b/lib/features/playback_reporting/core/playback_reporter.dart index 7416927..e140bf6 100644 --- a/lib/features/playback_reporting/core/playback_reporter.dart +++ b/lib/features/playback_reporting/core/playback_reporter.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/features/player/core/audiobook_player.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart'; final _logger = Logger('PlaybackReporter'); @@ -39,7 +41,15 @@ class PlaybackReporter { /// the minimum duration to report final Duration reportingDurationThreshold; + /// the duration to wait before starting the reporting + /// this is to ignore the initial duration in case user is browsing + final Duration? minimumPositionForReporting; + + /// the duration to mark the book as complete when the time left is less than this + final Duration markCompleteWhenTimeLeft; + /// timer to report every 10 seconds + /// tracking the time since the last report Timer? _reportTimer; /// metadata to report @@ -61,6 +71,8 @@ class PlaybackReporter { this.deviceManufacturer, this.reportingDurationThreshold = const Duration(seconds: 1), Duration reportingInterval = const Duration(seconds: 10), + this.minimumPositionForReporting, + this.markCompleteWhenTimeLeft = const Duration(seconds: 5), }) : _reportingInterval = reportingInterval { // initial conditions if (player.playing) { @@ -97,23 +109,33 @@ class PlaybackReporter { _logger.fine( 'player state observed, stopping stopwatch at ${_stopwatch.elapsed}', ); - await syncCurrentPosition(); + await tryReportPlayback(null); } }), ); _logger.fine( - 'initialized with interval: $reportingInterval, threshold: $reportingDurationThreshold', + 'initialized with reportingInterval: $reportingInterval, reportingDurationThreshold: $reportingDurationThreshold', + ); + _logger.fine( + 'initialized with minimumPositionForReporting: $minimumPositionForReporting, markCompleteWhenTimeLeft: $markCompleteWhenTimeLeft', ); _logger.fine( 'initialized with deviceModel: $deviceModel, deviceSdkVersion: $deviceSdkVersion, deviceClientName: $deviceClientName, deviceClientVersion: $deviceClientVersion, deviceManufacturer: $deviceManufacturer', ); } - void tryReportPlayback(_) async { - _logger.fine( - 'callback called when elapsed ${_stopwatch.elapsed}', - ); + Future tryReportPlayback(_) async { + _logger.fine('callback called when elapsed ${_stopwatch.elapsed}'); + if (player.book != null && + player.positionInBook >= + player.book!.duration - markCompleteWhenTimeLeft) { + _logger.info( + 'marking complete as time left is less than $markCompleteWhenTimeLeft', + ); + await markComplete(); + return; + } if (_stopwatch.elapsed > reportingDurationThreshold) { _logger.fine( 'reporting now with elapsed ${_stopwatch.elapsed} > threshold $reportingDurationThreshold', @@ -144,7 +166,8 @@ class PlaybackReporter { return _session!; } if (player.book == null) { - throw NoAudiobookPlayingError(); + _logger.warning('No audiobook playing to start session'); + return null; } _session = await authenticatedApi.items.play( libraryItemId: player.book!.libraryItemId, @@ -159,6 +182,22 @@ class PlaybackReporter { return _session; } + Future markComplete() async { + if (player.book == null) { + throw NoAudiobookPlayingError(); + } + await authenticatedApi.me.createUpdateMediaProgress( + libraryItemId: player.book!.libraryItemId, + parameters: CreateUpdateProgressReqParams( + isFinished: true, + currentTime: player.positionInBook, + duration: player.book!.duration, + ), + responseErrorHandler: _responseErrorHandler, + ); + _logger.info('Marked complete for book: ${player.book!.libraryItemId}'); + } + Future syncCurrentPosition() async { final data = _getSyncData(); if (data == null) { @@ -166,8 +205,11 @@ class PlaybackReporter { } try { _session ??= await startSession(); - } on NoAudiobookPlayingError { - _logger.warning('No audiobook playing to sync position'); + } on Error catch (e) { + _logger.warning('Error starting session: $e'); + } + if (_session == null) { + _logger.warning('No session to sync position'); return; } final currentPosition = player.positionInBook; @@ -213,9 +255,9 @@ class PlaybackReporter { _logger.fine('cancelled timer'); } - void _responseErrorHandler(response, [error]) { + void _responseErrorHandler(http.Response response, [error]) { if (response.statusCode != 200) { - _logger.shout('Error with api: $response, $error'); + _logger.severe('Error with api: ${response.obfuscate()}, $error'); throw PlaybackSyncError( 'Error syncing position: ${response.body}, $error', ); @@ -229,6 +271,23 @@ class PlaybackReporter { ); return null; } + + // if in the ignore duration, don't sync + if (minimumPositionForReporting != null && + player.positionInBook < minimumPositionForReporting!) { + // but if elapsed time is more than the minimumPositionForReporting, sync + if (_stopwatch.elapsed > minimumPositionForReporting!) { + _logger.info( + 'Syncing position despite being less than minimumPositionForReporting as elapsed time is more: ${_stopwatch.elapsed}', + ); + } else { + _logger.info( + 'Ignoring sync for position: ${player.positionInBook} < $minimumPositionForReporting', + ); + return null; + } + } + return SyncSessionReqParams( currentTime: player.positionInBook, timeListened: _stopwatch.elapsed, diff --git a/lib/features/playback_reporting/providers/playback_reporter_provider.dart b/lib/features/playback_reporting/providers/playback_reporter_provider.dart index e76f7ff..1bee521 100644 --- a/lib/features/playback_reporting/providers/playback_reporter_provider.dart +++ b/lib/features/playback_reporting/providers/playback_reporter_provider.dart @@ -13,20 +13,23 @@ part 'playback_reporter_provider.g.dart'; class PlaybackReporter extends _$PlaybackReporter { @override Future build() async { - final appSettings = ref.watch(appSettingsProvider); + final playerSettings = ref.watch(appSettingsProvider).playerSettings; final player = ref.watch(simpleAudiobookPlayerProvider); final packageInfo = await PackageInfo.fromPlatform(); final api = ref.watch(authenticatedApiProvider); final deviceName = await ref.watch(deviceNameProvider.future); final deviceModel = await ref.watch(deviceModelProvider.future); final deviceSdkVersion = await ref.watch(deviceSdkVersionProvider.future); - final deviceManufacturer = - await ref.watch(deviceManufacturerProvider.future); + final deviceManufacturer = await ref.watch( + deviceManufacturerProvider.future, + ); final reporter = core.PlaybackReporter( player, api, - reportingInterval: appSettings.playerSettings.playbackReportInterval, + reportingInterval: playerSettings.playbackReportInterval, + markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft, + minimumPositionForReporting: playerSettings.minimumPositionForReporting, deviceName: deviceName, deviceModel: deviceModel, deviceSdkVersion: deviceSdkVersion, diff --git a/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart index 2a76328..e017dbb 100644 --- a/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart +++ b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart @@ -6,21 +6,55 @@ part of 'playback_reporter_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$playbackReporterHash() => r'c210b7286d9c151fd59a9ead9eb4a28d1cffdc7c'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning -/// See also [PlaybackReporter]. @ProviderFor(PlaybackReporter) -final playbackReporterProvider = - AsyncNotifierProvider.internal( - PlaybackReporter.new, - name: r'playbackReporterProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$playbackReporterHash, - dependencies: null, - allTransitiveDependencies: null, -); +final playbackReporterProvider = PlaybackReporterProvider._(); -typedef _$PlaybackReporter = AsyncNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +final class PlaybackReporterProvider + extends $AsyncNotifierProvider { + PlaybackReporterProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'playbackReporterProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$playbackReporterHash(); + + @$internal + @override + PlaybackReporter create() => PlaybackReporter(); +} + +String _$playbackReporterHash() => r'f5436d652e51c37bcc684acdaec94e17a97e68e5'; + +abstract class _$PlaybackReporter + extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref + as $Ref, core.PlaybackReporter>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier< + AsyncValue, + core.PlaybackReporter + >, + AsyncValue, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/features/player/core/audiobook_player.dart b/lib/features/player/core/audiobook_player.dart index 106a547..c3d7b46 100644 --- a/lib/features/player/core/audiobook_player.dart +++ b/lib/features/player/core/audiobook_player.dart @@ -8,38 +8,40 @@ import 'package:just_audio/just_audio.dart'; import 'package:just_audio_background/just_audio_background.dart'; import 'package:logging/logging.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/models/app_settings.dart'; +import 'package:vaani/shared/extensions/model_conversions.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart'; final _logger = Logger('AudiobookPlayer'); /// returns the sum of the duration of all the previous tracks before the [index] Duration sumOfTracks(BookExpanded book, int? index) { + _logger.fine('Calculating sum of tracks for index: $index'); // return 0 if index is less than 0 if (index == null || index < 0) { + _logger.warning('Index is null or less than 0, returning 0'); return Duration.zero; } - return book.tracks.sublist(0, index).fold(Duration.zero, - (previousValue, element) { - return previousValue + element.duration; - }); + final total = book.tracks + .sublist(0, index) + .fold( + Duration.zero, + (previousValue, element) => previousValue + element.duration, + ); + _logger.fine('Sum of tracks for index: $index is $total'); + return total; } /// returns the [AudioTrack] to play based on the [position] in the [book] AudioTrack getTrackToPlay(BookExpanded book, Duration position) { - // var totalDuration = Duration.zero; - // for (var track in book.tracks) { - // totalDuration += track.duration; - // if (totalDuration >= position) { - // return track; - // } - // } - // return book.tracks.last; - return book.tracks.firstWhere( - (element) { - return element.startOffset <= position && - (element.startOffset + element.duration) >= position; - }, - orElse: () => book.tracks.last, - ); + _logger.fine('Getting track to play for position: $position'); + final track = book.tracks.firstWhere((element) { + return element.startOffset <= position && + (element.startOffset + element.duration) >= position; + }, orElse: () => book.tracks.last); + _logger.fine('Track to play for position: $position is $track'); + return track; } /// will manage the audio player instance @@ -47,6 +49,7 @@ class AudiobookPlayer extends AudioPlayer { // constructor which takes in the BookExpanded object AudiobookPlayer(this.token, this.baseUrl) : super() { // set the source of the player to the first track in the book + _logger.config('Setting up audiobook player'); } /// the [BookExpanded] being played @@ -81,19 +84,23 @@ class AudiobookPlayer extends AudioPlayer { List? downloadedUris, Uri? artworkUri, }) async { - // if the book is null, stop the player + _logger.finer( + 'Initial position: $initialPosition, Downloaded URIs: $downloadedUris', + ); + final appSettings = loadOrCreateAppSettings(); if (book == null) { _book = null; _logger.info('Book is null, stopping player'); return stop(); } - // see if the book is the same as the current book if (_book == book) { _logger.info('Book is the same, doing nothing'); return; } - // first stop the player and clear the source + _logger.info('Setting source for book: $book'); + + _logger.fine('Stopping player'); await stop(); _book = book; @@ -110,6 +117,7 @@ class AudiobookPlayer extends AudioPlayer { ? initialPosition - trackToPlay.startOffset : null; + _logger.finer('Setting audioSource'); await setAudioSource( preload: preload, initialIndex: initialIndex, @@ -117,10 +125,14 @@ class AudiobookPlayer extends AudioPlayer { ConcatenatingAudioSource( useLazyPreparation: true, children: book.tracks.map((track) { - final retrievedUri = - _getUri(track, downloadedUris, baseUrl: baseUrl, token: token); + final retrievedUri = _getUri( + track, + downloadedUris, + baseUrl: baseUrl, + token: token, + ); _logger.fine( - 'Setting source for track: ${track.title}, URI: $retrievedUri', + 'Setting source for track: ${track.title}, URI: ${retrievedUri.obfuscate()}', ); return AudioSource.uri( retrievedUri, @@ -128,9 +140,12 @@ class AudiobookPlayer extends AudioPlayer { // Specify a unique ID for each media item: id: book.libraryItemId + track.index.toString(), // Metadata to display in the notification: - album: book.metadata.title, - title: book.metadata.title ?? track.title, - artUri: artworkUri ?? + title: appSettings.notificationSettings.primaryTitle + .formatNotificationTitle(book), + album: appSettings.notificationSettings.secondaryTitle + .formatNotificationTitle(book), + artUri: + artworkUri ?? Uri.parse( '$baseUrl/api/items/${book.libraryItemId}/cover?token=$token&width=800', ), @@ -139,7 +154,7 @@ class AudiobookPlayer extends AudioPlayer { }).toList(), ), ).catchError((error) { - _logger.shout('Error: $error'); + _logger.shout('Error in setting audio source: $error'); }); } @@ -147,7 +162,7 @@ class AudiobookPlayer extends AudioPlayer { Future togglePlayPause() { // check if book is set if (_book == null) { - throw StateError('No book is set'); + _logger.warning('No book is set, not toggling play/pause'); } // TODO refactor this to cover all the states @@ -163,9 +178,11 @@ class AudiobookPlayer extends AudioPlayer { @override Future seek(Duration? positionInBook, {int? index}) async { if (_book == null) { + _logger.warning('No book is set, not seeking'); return; } if (positionInBook == null) { + _logger.warning('Position given is null, not seeking'); return; } final tracks = _book!.tracks; @@ -198,7 +215,7 @@ class AudiobookPlayer extends AudioPlayer { @override Stream get positionStream { - // return the positioninbook stream + // return the positionInBook stream return super.positionStream.map((position) { if (_book == null) { return Duration.zero; @@ -242,12 +259,9 @@ class AudiobookPlayer extends AudioPlayer { if (_book!.chapters.isEmpty) { return null; } - return _book!.chapters.firstWhere( - (element) { - return element.start <= positionInBook && element.end >= positionInBook; - }, - orElse: () => _book!.chapters.first, - ); + return _book!.chapters.firstWhere((element) { + return element.start <= positionInBook && element.end >= positionInBook; + }, orElse: () => _book!.chapters.first); } } @@ -258,12 +272,46 @@ Uri _getUri( required String token, }) { // check if the track is in the downloadedUris - final uri = downloadedUris?.firstWhereOrNull( - (element) { - return element.pathSegments.last == track.metadata?.filename; - }, - ); + final uri = downloadedUris?.firstWhereOrNull((element) { + return element.pathSegments.last == track.metadata?.filename; + }); return uri ?? Uri.parse('${baseUrl.toString()}${track.contentUrl}?token=$token'); } + +extension FormatNotificationTitle on String { + String formatNotificationTitle(BookExpanded book) { + return replaceAllMapped(RegExp(r'\$(\w+)'), (match) { + final type = match.group(1); + return NotificationTitleType.values + .firstWhere((element) => element.name == type) + .extractFrom(book) ?? + match.group(0) ?? + ''; + }); + } +} + +extension NotificationTitleUtils on NotificationTitleType { + String? extractFrom(BookExpanded book) { + var bookMetadataExpanded = book.metadata.asBookMetadataExpanded; + switch (this) { + case NotificationTitleType.bookTitle: + return bookMetadataExpanded.title; + case NotificationTitleType.chapterTitle: + // TODO: implement chapter title; depends on https://github.com/Dr-Blank/Vaani/issues/2 + return bookMetadataExpanded.title; + case NotificationTitleType.author: + return bookMetadataExpanded.authorName; + case NotificationTitleType.narrator: + return bookMetadataExpanded.narratorName; + case NotificationTitleType.series: + return bookMetadataExpanded.seriesName; + case NotificationTitleType.subtitle: + return bookMetadataExpanded.subtitle; + case NotificationTitleType.year: + return bookMetadataExpanded.publishedYear; + } + } +} diff --git a/lib/features/player/core/init.dart b/lib/features/player/core/init.dart new file mode 100644 index 0000000..a95f3c7 --- /dev/null +++ b/lib/features/player/core/init.dart @@ -0,0 +1,67 @@ +import 'package:audio_service/audio_service.dart'; +import 'package:audio_session/audio_session.dart'; +import 'package:just_audio_background/just_audio_background.dart' + show JustAudioBackground, NotificationConfig; +import 'package:just_audio_media_kit/just_audio_media_kit.dart' + show JustAudioMediaKit; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/models/app_settings.dart'; + +Future configurePlayer() async { + // for playing audio on windows, linux + JustAudioMediaKit.ensureInitialized(); + + // for configuring how this app will interact with other audio apps + final session = await AudioSession.instance; + await session.configure(const AudioSessionConfiguration.speech()); + + final appSettings = loadOrCreateAppSettings(); + + // for playing audio in the background + await JustAudioBackground.init( + androidNotificationChannelId: 'com.vaani.bg_demo.channel.audio', + androidNotificationChannelName: 'Audio playback', + androidNotificationOngoing: false, + androidStopForegroundOnPause: false, + androidNotificationChannelDescription: 'Audio playback in the background', + androidNotificationIcon: 'drawable/ic_stat_logo', + rewindInterval: appSettings.notificationSettings.rewindInterval, + fastForwardInterval: appSettings.notificationSettings.fastForwardInterval, + androidShowNotificationBadge: false, + notificationConfigBuilder: (state) { + final controls = [ + if (appSettings.notificationSettings.mediaControls.contains( + NotificationMediaControl.skipToPreviousChapter, + ) && + state.hasPrevious) + MediaControl.skipToPrevious, + if (appSettings.notificationSettings.mediaControls.contains( + NotificationMediaControl.rewind, + )) + MediaControl.rewind, + if (state.playing) MediaControl.pause else MediaControl.play, + if (appSettings.notificationSettings.mediaControls.contains( + NotificationMediaControl.fastForward, + )) + MediaControl.fastForward, + if (appSettings.notificationSettings.mediaControls.contains( + NotificationMediaControl.skipToNextChapter, + ) && + state.hasNext) + MediaControl.skipToNext, + if (appSettings.notificationSettings.mediaControls.contains( + NotificationMediaControl.stop, + )) + MediaControl.stop, + ]; + return NotificationConfig( + controls: controls, + systemActions: const { + MediaAction.seek, + MediaAction.seekForward, + MediaAction.seekBackward, + }, + ); + }, + ); +} diff --git a/lib/features/player/playlist.dart b/lib/features/player/playlist.dart index 61c2326..c8baedd 100644 --- a/lib/features/player/playlist.dart +++ b/lib/features/player/playlist.dart @@ -62,8 +62,8 @@ class AudiobookPlaylist { this.books = const [], currentIndex = 0, subCurrentIndex = 0, - }) : _currentIndex = currentIndex, - _subCurrentIndex = subCurrentIndex; + }) : _currentIndex = currentIndex, + _subCurrentIndex = subCurrentIndex; // most important method, gets the audio file to play // this is needed as a library item is a list of audio files diff --git a/lib/features/player/playlist_provider.g.dart b/lib/features/player/playlist_provider.g.dart index 061289c..2baca58 100644 --- a/lib/features/player/playlist_provider.g.dart +++ b/lib/features/player/playlist_provider.g.dart @@ -6,20 +6,57 @@ part of 'playlist_provider.dart'; // RiverpodGenerator // ************************************************************************** +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(Playlist) +final playlistProvider = PlaylistProvider._(); + +final class PlaylistProvider + extends $NotifierProvider { + PlaylistProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'playlistProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$playlistHash(); + + @$internal + @override + Playlist create() => Playlist(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(AudiobookPlaylist value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + String _$playlistHash() => r'bed4642e4c2de829e4d0630cb5bf92bffeeb1f60'; -/// See also [Playlist]. -@ProviderFor(Playlist) -final playlistProvider = - AutoDisposeNotifierProvider.internal( - Playlist.new, - name: r'playlistProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$playlistHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$Playlist = AutoDisposeNotifier; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +abstract class _$Playlist extends $Notifier { + AudiobookPlaylist build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + AudiobookPlaylist, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/features/player/providers/audiobook_player.dart b/lib/features/player/providers/audiobook_player.dart index 947ca57..29fede9 100644 --- a/lib/features/player/providers/audiobook_player.dart +++ b/lib/features/player/providers/audiobook_player.dart @@ -1,21 +1,11 @@ +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/features/player/core/audiobook_player.dart' - as core; +import 'package:vaani/features/player/core/audiobook_player.dart' as core; part 'audiobook_player.g.dart'; -// @Riverpod(keepAlive: true) -// core.AudiobookPlayer audiobookPlayer( -// AudiobookPlayerRef ref, -// ) { -// final api = ref.watch(authenticatedApiProvider); -// final player = core.AudiobookPlayer(api.token!, api.baseUrl); - -// ref.onDispose(player.dispose); - -// return player; -// } +final _logger = Logger('AudiobookPlayerProvider'); const playerId = 'audiobook_player'; @@ -26,12 +16,10 @@ class SimpleAudiobookPlayer extends _$SimpleAudiobookPlayer { @override core.AudiobookPlayer build() { final api = ref.watch(authenticatedApiProvider); - final player = core.AudiobookPlayer( - api.token!, - api.baseUrl, - ); + final player = core.AudiobookPlayer(api.token!, api.baseUrl); ref.onDispose(player.dispose); + _logger.finer('created simple player'); return player; } @@ -47,18 +35,16 @@ class AudiobookPlayer extends _$AudiobookPlayer { // bind notify listeners to the player player.playerStateStream.listen((_) { - notifyListeners(); + ref.notifyListeners(); }); + _logger.finer('created player'); + return player; } - void notifyListeners() { - ref.notifyListeners(); - } - Future setSpeed(double speed) async { await state.setSpeed(speed); - notifyListeners(); + ref.notifyListeners(); } } diff --git a/lib/features/player/providers/audiobook_player.g.dart b/lib/features/player/providers/audiobook_player.g.dart index 849cde6..900fd73 100644 --- a/lib/features/player/providers/audiobook_player.g.dart +++ b/lib/features/player/providers/audiobook_player.g.dart @@ -6,41 +6,119 @@ part of 'audiobook_player.dart'; // RiverpodGenerator // ************************************************************************** -String _$simpleAudiobookPlayerHash() => - r'9e11ed2791d35e308f8cbe61a79a45cf51466ebb'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Simple because it doesn't rebuild when the player state changes +/// it only rebuilds when the token changes + +@ProviderFor(SimpleAudiobookPlayer) +final simpleAudiobookPlayerProvider = SimpleAudiobookPlayerProvider._(); /// Simple because it doesn't rebuild when the player state changes /// it only rebuilds when the token changes -/// -/// Copied from [SimpleAudiobookPlayer]. -@ProviderFor(SimpleAudiobookPlayer) -final simpleAudiobookPlayerProvider = - NotifierProvider.internal( - SimpleAudiobookPlayer.new, - name: r'simpleAudiobookPlayerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$simpleAudiobookPlayerHash, - dependencies: null, - allTransitiveDependencies: null, -); +final class SimpleAudiobookPlayerProvider + extends $NotifierProvider { + /// Simple because it doesn't rebuild when the player state changes + /// it only rebuilds when the token changes + SimpleAudiobookPlayerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'simpleAudiobookPlayerProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); -typedef _$SimpleAudiobookPlayer = Notifier; -String _$audiobookPlayerHash() => r'44394b1dbbf85eb19ef1f693717e8cbc15b768e5'; + @override + String debugGetCreateSourceHash() => _$simpleAudiobookPlayerHash(); + + @$internal + @override + SimpleAudiobookPlayer create() => SimpleAudiobookPlayer(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(core.AudiobookPlayer value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$simpleAudiobookPlayerHash() => + r'5e94bbff4314adceb5affa704fc4d079d4016afa'; + +/// Simple because it doesn't rebuild when the player state changes +/// it only rebuilds when the token changes + +abstract class _$SimpleAudiobookPlayer extends $Notifier { + core.AudiobookPlayer build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + core.AudiobookPlayer, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} -/// See also [AudiobookPlayer]. @ProviderFor(AudiobookPlayer) -final audiobookPlayerProvider = - NotifierProvider.internal( - AudiobookPlayer.new, - name: r'audiobookPlayerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$audiobookPlayerHash, - dependencies: null, - allTransitiveDependencies: null, -); +final audiobookPlayerProvider = AudiobookPlayerProvider._(); -typedef _$AudiobookPlayer = 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 +final class AudiobookPlayerProvider + extends $NotifierProvider { + AudiobookPlayerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'audiobookPlayerProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$audiobookPlayerHash(); + + @$internal + @override + AudiobookPlayer create() => AudiobookPlayer(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(core.AudiobookPlayer value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$audiobookPlayerHash() => r'0f180308067486896fec6a65a6afb0e6686ac4a0'; + +abstract class _$AudiobookPlayer extends $Notifier { + core.AudiobookPlayer build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + core.AudiobookPlayer, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/features/player/providers/currently_playing_provider.dart b/lib/features/player/providers/currently_playing_provider.dart index 67e0a35..e8b8af9 100644 --- a/lib/features/player/providers/currently_playing_provider.dart +++ b/lib/features/player/providers/currently_playing_provider.dart @@ -1,3 +1,5 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; @@ -5,19 +7,22 @@ import 'package:vaani/shared/extensions/model_conversions.dart'; part 'currently_playing_provider.g.dart'; +final _logger = Logger('CurrentlyPlayingProvider'); + @riverpod -BookExpanded? currentlyPlayingBook(CurrentlyPlayingBookRef ref) { +BookExpanded? currentlyPlayingBook(Ref ref) { try { final player = ref.watch(audiobookPlayerProvider); return player.book; } catch (e) { + _logger.warning('Error getting currently playing book: $e'); return null; } } /// provided the current chapter of the book being played @riverpod -BookChapter? currentPlayingChapter(CurrentPlayingChapterRef ref) { +BookChapter? currentPlayingChapter(Ref ref) { final player = ref.watch(audiobookPlayerProvider); player.slowPositionStream.listen((_) { ref.invalidateSelf(); @@ -28,7 +33,7 @@ BookChapter? currentPlayingChapter(CurrentPlayingChapterRef ref) { /// provides the book metadata of the currently playing book @riverpod -BookMetadataExpanded? currentBookMetadata(CurrentBookMetadataRef ref) { +BookMetadataExpanded? currentBookMetadata(Ref ref) { final player = ref.watch(audiobookPlayerProvider); if (player.book == null) return null; return player.book!.metadata.asBookMetadataExpanded; diff --git a/lib/features/player/providers/currently_playing_provider.g.dart b/lib/features/player/providers/currently_playing_provider.g.dart index fb22028..f886f8e 100644 --- a/lib/features/player/providers/currently_playing_provider.g.dart +++ b/lib/features/player/providers/currently_playing_provider.g.dart @@ -6,60 +6,147 @@ part of 'currently_playing_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$currentlyPlayingBookHash() => - r'52334c7b4d68fd498a2a00208d8d7f1ba0085237'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning -/// See also [currentlyPlayingBook]. @ProviderFor(currentlyPlayingBook) -final currentlyPlayingBookProvider = - AutoDisposeProvider.internal( - currentlyPlayingBook, - name: r'currentlyPlayingBookProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentlyPlayingBookHash, - dependencies: null, - allTransitiveDependencies: null, -); +final currentlyPlayingBookProvider = CurrentlyPlayingBookProvider._(); -typedef CurrentlyPlayingBookRef = AutoDisposeProviderRef; -String _$currentPlayingChapterHash() => - r'a084da724e3d8bb1b1475e867ab3200d7d61d827'; +final class CurrentlyPlayingBookProvider + extends $FunctionalProvider + with $Provider { + CurrentlyPlayingBookProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'currentlyPlayingBookProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$currentlyPlayingBookHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + BookExpanded? create(Ref ref) { + return currentlyPlayingBook(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(BookExpanded? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$currentlyPlayingBookHash() => + r'e4258694c8f0d1e89651b330fae0f672ca13a484'; /// provided the current chapter of the book being played -/// -/// Copied from [currentPlayingChapter]. -@ProviderFor(currentPlayingChapter) -final currentPlayingChapterProvider = - AutoDisposeProvider.internal( - currentPlayingChapter, - name: r'currentPlayingChapterProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentPlayingChapterHash, - dependencies: null, - allTransitiveDependencies: null, -); -typedef CurrentPlayingChapterRef = AutoDisposeProviderRef; -String _$currentBookMetadataHash() => - r'9088debba151894b61f2dcba1bba12a89244b9b1'; +@ProviderFor(currentPlayingChapter) +final currentPlayingChapterProvider = CurrentPlayingChapterProvider._(); + +/// provided the current chapter of the book being played + +final class CurrentPlayingChapterProvider + extends $FunctionalProvider + with $Provider { + /// provided the current chapter of the book being played + CurrentPlayingChapterProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'currentPlayingChapterProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$currentPlayingChapterHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + BookChapter? create(Ref ref) { + return currentPlayingChapter(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(BookChapter? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$currentPlayingChapterHash() => + r'73db8b8a9058573bb0c68ec5d5f8aba9306f3d24'; /// provides the book metadata of the currently playing book -/// -/// Copied from [currentBookMetadata]. -@ProviderFor(currentBookMetadata) -final currentBookMetadataProvider = - AutoDisposeProvider.internal( - currentBookMetadata, - name: r'currentBookMetadataProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$currentBookMetadataHash, - dependencies: null, - allTransitiveDependencies: null, -); -typedef CurrentBookMetadataRef = AutoDisposeProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +@ProviderFor(currentBookMetadata) +final currentBookMetadataProvider = CurrentBookMetadataProvider._(); + +/// provides the book metadata of the currently playing book + +final class CurrentBookMetadataProvider + extends + $FunctionalProvider< + BookMetadataExpanded?, + BookMetadataExpanded?, + BookMetadataExpanded? + > + with $Provider { + /// provides the book metadata of the currently playing book + CurrentBookMetadataProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'currentBookMetadataProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$currentBookMetadataHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + BookMetadataExpanded? create(Ref ref) { + return currentBookMetadata(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(BookMetadataExpanded? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$currentBookMetadataHash() => + r'f537ef4ef19280bc952de658ecf6520c535ae344'; diff --git a/lib/features/player/providers/player_form.dart b/lib/features/player/providers/player_form.dart index 975f47f..9652486 100644 --- a/lib/features/player/providers/player_form.dart +++ b/lib/features/player/providers/player_form.dart @@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:miniplayer/miniplayer.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart'; part 'player_form.g.dart'; @@ -25,11 +26,10 @@ extension on Ref { } @Riverpod(keepAlive: true) -Raw> playerExpandProgressNotifier( - PlayerExpandProgressNotifierRef ref, -) { - final ValueNotifier playerExpandProgress = - ValueNotifier(playerMinHeight); +Raw> playerExpandProgressNotifier(Ref ref) { + final ValueNotifier playerExpandProgress = ValueNotifier( + playerMinHeight, + ); return ref.disposeAndListenChangeNotifier(playerExpandProgress); } @@ -45,10 +45,8 @@ Raw> playerExpandProgressNotifier( // a provider that will listen to the playerExpandProgressNotifier and return the percentage of the player expanded @Riverpod(keepAlive: true) -double playerHeight( - PlayerHeightRef ref, -) { - final playerExpandProgress = ref.watch(playerExpandProgressNotifierProvider); +double playerHeight(Ref ref) { + final playerExpandProgress = ref.watch(playerExpandProgressProvider); // on change of the playerExpandProgress invalidate playerExpandProgress.addListener(() { @@ -60,3 +58,18 @@ double playerHeight( } final audioBookMiniplayerController = MiniplayerController(); + +@Riverpod(keepAlive: true) +bool isPlayerActive(Ref ref) { + try { + final player = ref.watch(audiobookPlayerProvider); + if (player.book != null) { + return true; + } else { + final playerHeight = ref.watch(playerHeightProvider); + return playerHeight < playerMinHeight; + } + } catch (e) { + return false; + } +} diff --git a/lib/features/player/providers/player_form.g.dart b/lib/features/player/providers/player_form.g.dart index b3b73d9..b2d17ce 100644 --- a/lib/features/player/providers/player_form.g.dart +++ b/lib/features/player/providers/player_form.g.dart @@ -6,37 +6,134 @@ part of 'player_form.dart'; // RiverpodGenerator // ************************************************************************** -String _$playerExpandProgressNotifierHash() => - r'e4817361b9a311b61ca23e51082ed11b0a1120ab'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning -/// See also [playerExpandProgressNotifier]. @ProviderFor(playerExpandProgressNotifier) -final playerExpandProgressNotifierProvider = - Provider>>.internal( - playerExpandProgressNotifier, - name: r'playerExpandProgressNotifierProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$playerExpandProgressNotifierHash, - dependencies: null, - allTransitiveDependencies: null, -); +final playerExpandProgressProvider = PlayerExpandProgressNotifierProvider._(); -typedef PlayerExpandProgressNotifierRef - = ProviderRef>>; -String _$playerHeightHash() => r'26dbcb180d494575488d700bd5bdb58c02c224a9'; +final class PlayerExpandProgressNotifierProvider + extends + $FunctionalProvider< + Raw>, + Raw>, + Raw> + > + with $Provider>> { + PlayerExpandProgressNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'playerExpandProgressProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$playerExpandProgressNotifierHash(); + + @$internal + @override + $ProviderElement>> $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + Raw> create(Ref ref) { + return playerExpandProgressNotifier(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(Raw> value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider>>(value), + ); + } +} + +String _$playerExpandProgressNotifierHash() => + r'1ac7172d90a070f96222286edd1a176be197f378'; -/// See also [playerHeight]. @ProviderFor(playerHeight) -final playerHeightProvider = Provider.internal( - playerHeight, - name: r'playerHeightProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$playerHeightHash, - dependencies: null, - allTransitiveDependencies: null, -); +final playerHeightProvider = PlayerHeightProvider._(); -typedef PlayerHeightRef = ProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +final class PlayerHeightProvider + extends $FunctionalProvider + with $Provider { + PlayerHeightProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'playerHeightProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$playerHeightHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + double create(Ref ref) { + return playerHeight(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(double value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$playerHeightHash() => r'41144a733b5ffd1c872a237ed7c9ea5f450dd0d4'; + +@ProviderFor(isPlayerActive) +final isPlayerActiveProvider = IsPlayerActiveProvider._(); + +final class IsPlayerActiveProvider extends $FunctionalProvider + with $Provider { + IsPlayerActiveProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'isPlayerActiveProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$isPlayerActiveHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + bool create(Ref ref) { + return isPlayerActive(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(bool value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$isPlayerActiveHash() => r'2c7ca125423126fb5f0ef218d37bc8fe0ca9ec98'; diff --git a/lib/features/player/view/audiobook_player.dart b/lib/features/player/view/audiobook_player.dart index f227342..e80202a 100644 --- a/lib/features/player/view/audiobook_player.dart +++ b/lib/features/player/view/audiobook_player.dart @@ -14,7 +14,7 @@ import 'package:vaani/features/player/providers/player_form.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/inverse_lerp.dart'; import 'package:vaani/shared/widgets/shelves/book_shelf.dart'; -import 'package:vaani/theme/theme_from_cover_provider.dart'; +import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; import 'player_when_expanded.dart'; import 'player_when_minimized.dart'; @@ -31,19 +31,15 @@ class AudiobookPlayer extends HookConsumerWidget { if (currentBook == null) { return const SizedBox.shrink(); } - final itemBeingPlayed = - ref.watch(libraryItemProvider(currentBook.libraryItemId)); + final itemBeingPlayed = ref.watch( + libraryItemProvider(currentBook.libraryItemId), + ); final player = ref.watch(audiobookPlayerProvider); - final imageOfItemBeingPlayed = itemBeingPlayed.valueOrNull != null - ? ref.watch( - coverImageProvider(itemBeingPlayed.valueOrNull!), - ) + final imageOfItemBeingPlayed = itemBeingPlayed.value != null + ? ref.watch(coverImageProvider(itemBeingPlayed.value!.id)) : null; - final imgWidget = imageOfItemBeingPlayed?.valueOrNull != null - ? Image.memory( - imageOfItemBeingPlayed!.valueOrNull!, - fit: BoxFit.cover, - ) + final imgWidget = imageOfItemBeingPlayed?.value != null + ? Image.memory(imageOfItemBeingPlayed!.value!, fit: BoxFit.cover) : const BookCoverSkeleton(); final playPauseController = useAnimationController( @@ -63,8 +59,11 @@ class AudiobookPlayer extends HookConsumerWidget { // theme from image final imageTheme = ref.watch( themeOfLibraryItemProvider( - itemBeingPlayed.valueOrNull, + itemBeingPlayed.value?.id, brightness: Theme.of(context).brightness, + highContrast: + appSettings.themeSettings.highContrast || + MediaQuery.of(context).highContrast, ), ); @@ -79,15 +78,16 @@ class AudiobookPlayer extends HookConsumerWidget { final preferredVolume = appSettings.playerSettings.preferredDefaultVolume; return Theme( data: ThemeData( - colorScheme: imageTheme.valueOrNull ?? Theme.of(context).colorScheme, + colorScheme: imageTheme.value ?? Theme.of(context).colorScheme, ), child: Miniplayer( - valueNotifier: ref.watch(playerExpandProgressNotifierProvider), + valueNotifier: ref.watch(playerExpandProgressProvider), onDragDown: (percentage) async { // preferred volume // set volume to 0 when dragging down - await player - .setVolume(preferredVolume * (1 - percentage.clamp(0, .75))); + await player.setVolume( + preferredVolume * (1 - percentage.clamp(0, .75)), + ); }, minHeight: playerMinHeight, // subtract the height of notches and other system UI @@ -107,17 +107,14 @@ class AudiobookPlayer extends HookConsumerWidget { // also at this point the image should be at its max size and in the center of the player final miniplayerPercentageDeclaration = (maxImgSize - playerMinHeight) / - (playerMaxHeight - playerMinHeight); + (playerMaxHeight - playerMinHeight); final bool isFormMiniplayer = percentage < miniplayerPercentageDeclaration; if (!isFormMiniplayer) { // this calculation needs a refactor var percentageExpandedPlayer = percentage - .inverseLerp( - miniplayerPercentageDeclaration, - 1, - ) + .inverseLerp(miniplayerPercentageDeclaration, 1) .clamp(0.0, 1.0); return PlayerWhenExpanded( @@ -162,37 +159,33 @@ class AudiobookPlayerPlayPauseButton extends HookConsumerWidget { return switch (player.processingState) { ProcessingState.loading || ProcessingState.buffering => const Padding( - padding: EdgeInsets.all(8.0), - child: CircularProgressIndicator(), - ), + padding: EdgeInsets.all(8.0), + child: CircularProgressIndicator(), + ), ProcessingState.completed => IconButton( - onPressed: () async { - await player.seek(const Duration(seconds: 0)); - await player.play(); - }, - icon: const Icon( - Icons.replay, - ), - ), + onPressed: () async { + await player.seek(const Duration(seconds: 0)); + await player.play(); + }, + icon: const Icon(Icons.replay), + ), ProcessingState.ready => IconButton( - onPressed: () async { - await player.togglePlayPause(); - }, - iconSize: iconSize, - icon: AnimatedIcon( - icon: AnimatedIcons.play_pause, - progress: playPauseController, - ), + onPressed: () async { + await player.togglePlayPause(); + }, + iconSize: iconSize, + icon: AnimatedIcon( + icon: AnimatedIcons.play_pause, + progress: playPauseController, ), + ), ProcessingState.idle => const SizedBox.shrink(), }; } } class AudiobookChapterProgressBar extends HookConsumerWidget { - const AudiobookChapterProgressBar({ - super.key, - }); + const AudiobookChapterProgressBar({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/features/player/view/mini_player_bottom_padding.dart b/lib/features/player/view/mini_player_bottom_padding.dart new file mode 100644 index 0000000..c403361 --- /dev/null +++ b/lib/features/player/view/mini_player_bottom_padding.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/features/player/providers/player_form.dart'; + +class MiniPlayerBottomPadding extends HookConsumerWidget { + const MiniPlayerBottomPadding({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + return AnimatedSize( + duration: const Duration(milliseconds: 200), + child: ref.watch(isPlayerActiveProvider) + ? const SizedBox(height: playerMinHeight + 8) + : const SizedBox.shrink(), + ); + } +} diff --git a/lib/features/player/view/player_when_expanded.dart b/lib/features/player/view/player_when_expanded.dart index 3c8ee41..28e6993 100644 --- a/lib/features/player/view/player_when_expanded.dart +++ b/lib/features/player/view/player_when_expanded.dart @@ -1,16 +1,11 @@ -import 'package:duration_picker/duration_picker.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:miniplayer/miniplayer.dart'; import 'package:vaani/constants/sizes.dart'; import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/player/providers/player_form.dart'; import 'package:vaani/features/player/view/audiobook_player.dart'; -import 'package:vaani/features/sleep_timer/core/sleep_timer.dart'; -import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' - show sleepTimerProvider; -import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart'; import 'package:vaani/shared/extensions/inverse_lerp.dart'; import 'package:vaani/shared/widgets/not_implemented.dart'; @@ -43,10 +38,7 @@ class PlayerWhenExpanded extends HookConsumerWidget { const lateStart = 0.4; const earlyEnd = 1; final earlyPercentage = percentageExpandedPlayer - .inverseLerp( - lateStart, - earlyEnd, - ) + .inverseLerp(lateStart, earlyEnd) .clamp(0.0, 1.0); final currentChapter = ref.watch(currentPlayingChapterProvider); final currentBookMetadata = ref.watch(currentBookMetadataProvider); @@ -54,15 +46,11 @@ class PlayerWhenExpanded extends HookConsumerWidget { return Column( children: [ // sized box for system status bar; not needed as not full screen - SizedBox( - height: MediaQuery.of(context).padding.top * earlyPercentage, - ), + SizedBox(height: MediaQuery.of(context).padding.top * earlyPercentage), // a row with a down arrow to minimize the player, a pill shaped container to drag the player, and a cast button ConstrainedBox( - constraints: BoxConstraints( - maxHeight: 100 * earlyPercentage, - ), + constraints: BoxConstraints(maxHeight: 100 * earlyPercentage), child: Opacity( opacity: earlyPercentage, child: Padding( @@ -109,8 +97,9 @@ class PlayerWhenExpanded extends HookConsumerWidget { decoration: BoxDecoration( boxShadow: [ BoxShadow( - color: - Theme.of(context).colorScheme.primary.withOpacity(0.1), + color: Theme.of( + context, + ).colorScheme.primary.withValues(alpha: 0.1), blurRadius: 32 * earlyPercentage, spreadRadius: 8 * earlyPercentage, // offset: Offset(0, 16 * earlyPercentage), @@ -173,11 +162,10 @@ class PlayerWhenExpanded extends HookConsumerWidget { currentBookMetadata?.authorName ?? '', ].join(' - '), style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), + ), maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -245,13 +233,13 @@ class PlayerWhenExpanded extends HookConsumerWidget { // chapter list const ChapterSelectionButton(), // settings - IconButton( - icon: const Icon(Icons.more_horiz), - onPressed: () { - // show toast - showNotImplementedToast(context); - }, - ), + // IconButton( + // icon: const Icon(Icons.more_horiz), + // onPressed: () { + // // show toast + // showNotImplementedToast(context); + // }, + // ), ], ), ), @@ -260,142 +248,3 @@ class PlayerWhenExpanded extends HookConsumerWidget { ); } } - -class SleepTimerButton extends HookConsumerWidget { - const SleepTimerButton({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sleepTimer = ref.watch(sleepTimerProvider); - // if sleep timer is not active, show the button with the sleep timer icon - // if the sleep timer is active, show the remaining time in a pill shaped container - return Tooltip( - message: 'Sleep Timer', - child: InkWell( - onTap: () async { - pendingPlayerModals++; - // show the sleep timer dialog - final resultingDuration = await showDurationPicker( - context: context, - initialTime: ref - .watch(appSettingsProvider) - .playerSettings - .sleepTimerSettings - .defaultDuration, - ); - pendingPlayerModals--; - if (resultingDuration != null) { - // if 0 is selected, cancel the timer - if (resultingDuration.inSeconds == 0) { - ref.read(sleepTimerProvider.notifier).cancelTimer(); - } else { - ref.read(sleepTimerProvider.notifier).setTimer(resultingDuration); - } - } - }, - child: sleepTimer == null - ? Icon( - Icons.timer_rounded, - color: Theme.of(context).colorScheme.onSurface, - ) - : RemainingSleepTimeDisplay( - timer: sleepTimer, - ), - ), - ); - } -} - -class SleepTimerDialog extends HookConsumerWidget { - const SleepTimerDialog({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sleepTimer = ref.watch(sleepTimerProvider); - final sleepTimerSettings = - ref.watch(appSettingsProvider).playerSettings.sleepTimerSettings; - final timerDurationController = useTextEditingController( - text: sleepTimerSettings.defaultDuration.inMinutes.toString(), - ); - - return AlertDialog( - title: const Text('Sleep Timer'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Set the duration for the sleep timer'), - TextField( - controller: timerDurationController, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - labelText: 'Duration in minutes', - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - // sleepTimer.setTimer( - // Duration( - // minutes: int.tryParse(timerDurationController.text) ?? 0, - // ), - // ); - Navigator.of(context).pop(); - }, - child: const Text('Set Timer'), - ), - ], - ); - } -} - -class RemainingSleepTimeDisplay extends HookConsumerWidget { - const RemainingSleepTimeDisplay({ - super.key, - required this.timer, - }); - - final SleepTimer timer; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final remainingTime = useStream(timer.remainingTimeStream).data; - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(16), - ), - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - child: Text( - timer.timer == null - ? timer.duration.formatSingleLargestUnit() - : remainingTime?.formatSingleLargestUnit() ?? '', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onPrimary, - ), - ), - ); - } -} - -extension DurationFormat on Duration { - /// will return a number followed by h, m, or s depending on the duration - /// only the largest unit will be shown - String formatSingleLargestUnit() { - if (inHours > 0) { - return '${inHours}h'; - } else if (inMinutes > 0) { - return '${inMinutes}m'; - } else { - return '${inSeconds}s'; - } - } -} diff --git a/lib/features/player/view/player_when_minimized.dart b/lib/features/player/view/player_when_minimized.dart index 435a6de..9729fe2 100644 --- a/lib/features/player/view/player_when_minimized.dart +++ b/lib/features/player/view/player_when_minimized.dart @@ -32,8 +32,10 @@ class PlayerWhenMinimized extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final player = ref.watch(audiobookPlayerProvider); final vanishingPercentage = 1 - percentageMiniplayer; - final progress = - useStream(player.slowPositionStream, initialData: Duration.zero); + final progress = useStream( + player.slowPositionStream, + initialData: Duration.zero, + ); final bookMetaExpanded = ref.watch(currentBookMetadataProvider); @@ -61,9 +63,7 @@ class PlayerWhenMinimized extends HookConsumerWidget { ); }, child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: maxImgSize, - ), + constraints: BoxConstraints(maxWidth: maxImgSize), child: imgWidget, ), ), @@ -80,7 +80,8 @@ class PlayerWhenMinimized extends HookConsumerWidget { // AutoScrollText( Text( bookMetaExpanded?.title ?? '', - maxLines: 1, overflow: TextOverflow.ellipsis, + maxLines: 1, + overflow: TextOverflow.ellipsis, // velocity: // const Velocity(pixelsPerSecond: Offset(16, 0)), style: Theme.of(context).textTheme.bodyLarge, @@ -90,11 +91,10 @@ class PlayerWhenMinimized extends HookConsumerWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context) - .colorScheme - .onSurface - .withOpacity(0.7), - ), + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.7), + ), ), ], ), @@ -135,7 +135,8 @@ class PlayerWhenMinimized extends HookConsumerWidget { SizedBox( height: barHeight, child: LinearProgressIndicator( - value: (progress.data ?? Duration.zero).inSeconds / + value: + (progress.data ?? Duration.zero).inSeconds / player.book!.duration.inSeconds, color: Theme.of(context).colorScheme.onPrimaryContainer, backgroundColor: Theme.of(context).colorScheme.primaryContainer, diff --git a/lib/features/player/view/widgets/audiobook_player_seek_button.dart b/lib/features/player/view/widgets/audiobook_player_seek_button.dart index ac1ec20..d1d1edc 100644 --- a/lib/features/player/view/widgets/audiobook_player_seek_button.dart +++ b/lib/features/player/view/widgets/audiobook_player_seek_button.dart @@ -4,10 +4,7 @@ import 'package:vaani/constants/sizes.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; class AudiobookPlayerSeekButton extends HookConsumerWidget { - const AudiobookPlayerSeekButton({ - super.key, - required this.isForward, - }); + const AudiobookPlayerSeekButton({super.key, required this.isForward}); /// if true, the button seeks forward, else it seeks backwards final bool isForward; diff --git a/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart b/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart index ad47e8b..16127ad 100644 --- a/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart +++ b/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart @@ -5,10 +5,7 @@ import 'package:vaani/constants/sizes.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { - const AudiobookPlayerSeekChapterButton({ - super.key, - required this.isForward, - }); + const AudiobookPlayerSeekChapterButton({super.key, required this.isForward}); /// if true, the button seeks forward, else it seeks backwards final bool isForward; @@ -27,9 +24,7 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { void seekForward() { final index = player.book!.chapters.indexOf(player.currentChapter!); if (index < player.book!.chapters.length - 1) { - player.seek( - player.book!.chapters[index + 1].start + offset, - ); + player.seek(player.book!.chapters[index + 1].start + offset); } else { player.seek(player.currentChapter!.end); } @@ -37,8 +32,9 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { /// seek backward to the previous chapter or the start of the current chapter void seekBackward() { - final currentPlayingChapterIndex = - player.book!.chapters.indexOf(player.currentChapter!); + final currentPlayingChapterIndex = player.book!.chapters.indexOf( + player.currentChapter!, + ); final chapterPosition = player.positionInBook - player.currentChapter!.start; BookChapter chapterToSeekTo; @@ -49,9 +45,7 @@ class AudiobookPlayerSeekChapterButton extends HookConsumerWidget { } else { chapterToSeekTo = player.currentChapter!; } - player.seek( - chapterToSeekTo.start + offset, - ); + player.seek(chapterToSeekTo.start + offset); } return IconButton( diff --git a/lib/features/player/view/widgets/chapter_selection_button.dart b/lib/features/player/view/widgets/chapter_selection_button.dart index d4da604..ea2154e 100644 --- a/lib/features/player/view/widgets/chapter_selection_button.dart +++ b/lib/features/player/view/widgets/chapter_selection_button.dart @@ -1,17 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/features/player/providers/currently_playing_provider.dart'; -import 'package:vaani/features/player/view/player_when_expanded.dart'; -import 'package:vaani/shared/extensions/chapter.dart'; -import 'package:vaani/shared/extensions/duration_format.dart'; -import 'package:vaani/shared/hooks.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart' + show audiobookPlayerProvider; +import 'package:vaani/features/player/providers/currently_playing_provider.dart' + show currentPlayingChapterProvider, currentlyPlayingBookProvider; +import 'package:vaani/features/player/view/player_when_expanded.dart' + show pendingPlayerModals; +import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart'; +import 'package:vaani/main.dart' show appLogger; +import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration; +import 'package:vaani/shared/extensions/duration_format.dart' + show DurationFormat; +import 'package:vaani/shared/hooks.dart' show useTimer; class ChapterSelectionButton extends HookConsumerWidget { - const ChapterSelectionButton({ - super.key, - }); + const ChapterSelectionButton({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -43,9 +47,7 @@ class ChapterSelectionButton extends HookConsumerWidget { } class ChapterSelectionModal extends HookConsumerWidget { - const ChapterSelectionModal({ - super.key, - }); + const ChapterSelectionModal({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -55,7 +57,7 @@ class ChapterSelectionModal extends HookConsumerWidget { final currentChapterIndex = currentChapter?.id; final chapterKey = GlobalKey(); scrollToCurrentChapter() async { - debugPrint('scrolling to chapter'); + appLogger.fine('scrolling to chapter'); await Scrollable.ensureVisible( chapterKey.currentContext!, duration: 200.ms, @@ -66,6 +68,7 @@ class ChapterSelectionModal extends HookConsumerWidget { useTimer(scrollToCurrentChapter, 500.ms); // useInterval(scrollToCurrentChapter, 500.ms); + final theme = Theme.of(context); return Column( children: [ ListTile( @@ -80,24 +83,40 @@ class ChapterSelectionModal extends HookConsumerWidget { child: currentBook?.chapters == null ? const Text('No chapters found') : Column( - children: [ - for (final chapter in currentBook!.chapters) - ListTile( - title: Text(chapter.title), - trailing: Text( - '(${chapter.duration.smartBinaryFormat})', - ), - selected: currentChapterIndex == chapter.id, - key: currentChapterIndex == chapter.id - ? chapterKey + children: currentBook!.chapters.map((chapter) { + final isCurrent = currentChapterIndex == chapter.id; + final isPlayed = + currentChapterIndex != null && + chapter.id < currentChapterIndex; + return ListTile( + autofocus: isCurrent, + iconColor: isPlayed && !isCurrent + ? theme.disabledColor + : null, + title: Text( + chapter.title, + style: isPlayed && !isCurrent + ? TextStyle(color: theme.disabledColor) : null, - onTap: () { - Navigator.of(context).pop(); - notifier.seek(chapter.start + 90.ms); - notifier.play(); - }, ), - ], + subtitle: Text( + '(${chapter.duration.smartBinaryFormat})', + style: isPlayed && !isCurrent + ? TextStyle(color: theme.disabledColor) + : null, + ), + trailing: isCurrent + ? const PlayingIndicatorIcon() + : const Icon(Icons.play_arrow), + selected: isCurrent, + key: isCurrent ? chapterKey : null, + onTap: () { + Navigator.of(context).pop(); + notifier.seek(chapter.start + 90.ms); + notifier.play(); + }, + ); + }).toList(), ), ), ), diff --git a/lib/features/player/view/widgets/player_speed_adjust_button.dart b/lib/features/player/view/widgets/player_speed_adjust_button.dart index e13d278..021f999 100644 --- a/lib/features/player/view/widgets/player_speed_adjust_button.dart +++ b/lib/features/player/view/widgets/player_speed_adjust_button.dart @@ -10,9 +10,7 @@ import 'package:vaani/settings/app_settings_provider.dart'; final _logger = Logger('PlayerSpeedAdjustButton'); class PlayerSpeedAdjustButton extends HookConsumerWidget { - const PlayerSpeedAdjustButton({ - super.key, - }); + const PlayerSpeedAdjustButton({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -29,30 +27,25 @@ class PlayerSpeedAdjustButton extends HookConsumerWidget { await showModalBottomSheet( context: context, barrierLabel: 'Select Speed', - constraints: const BoxConstraints( - maxHeight: 225, - ), builder: (context) { return SpeedSelector( onSpeedSelected: (speed) { notifier.setSpeed(speed); if (appSettings.playerSettings.configurePlayerForEveryBook) { ref - .read( - bookSettingsProvider(bookId).notifier, - ) + .read(bookSettingsProvider(bookId).notifier) .update( - bookSettings.copyWith - .playerSettings(preferredDefaultSpeed: speed), + bookSettings.copyWith.playerSettings( + preferredDefaultSpeed: speed, + ), ); } else { ref - .read( - appSettingsProvider.notifier, - ) + .read(appSettingsProvider.notifier) .update( - appSettings.copyWith - .playerSettings(preferredDefaultSpeed: speed), + appSettings.copyWith.playerSettings( + preferredDefaultSpeed: speed, + ), ); } }, diff --git a/lib/features/player/view/widgets/playing_indicator_icon.dart b/lib/features/player/view/widgets/playing_indicator_icon.dart new file mode 100644 index 0000000..35c47fc --- /dev/null +++ b/lib/features/player/view/widgets/playing_indicator_icon.dart @@ -0,0 +1,194 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; + +/// An icon that animates like audio equalizer bars to indicate playback. +/// +/// Creates multiple vertical bars that independently animate their height +/// in a looping, visually dynamic pattern. +class PlayingIndicatorIcon extends StatefulWidget { + /// The number of vertical bars in the indicator. + final int barCount; + + /// The total width and height of the icon area. + final double size; + + /// The color of the bars. Defaults to the current [IconTheme] color. + final Color? color; + + /// The minimum height factor for a bar (relative to [size]). + /// When [centerSymmetric] is true, this represents the minimum height + /// extending from the center line (so total minimum height is 2 * minHeightFactor * size). + /// When false, it's the minimum height from the bottom. + final double minHeightFactor; + + /// The maximum height factor for a bar (relative to [size]). + /// When [centerSymmetric] is true, this represents the maximum height + /// extending from the center line (so total maximum height is 2 * maxHeightFactor * size). + /// When false, it's the maximum height from the bottom. + final double maxHeightFactor; + + /// Base duration for a full up/down animation cycle for a single bar. + /// Actual duration will vary slightly per bar. + final Duration baseCycleDuration; + + /// If true, the bars animate symmetrically expanding/collapsing from the + /// horizontal center line. If false (default), they expand/collapse from + /// the bottom edge. + final bool centerSymmetric; + + const PlayingIndicatorIcon({ + super.key, + this.barCount = 4, + this.size = 20.0, + this.color, + this.minHeightFactor = 0.2, + this.maxHeightFactor = 1.0, + this.baseCycleDuration = const Duration(milliseconds: 350), + this.centerSymmetric = true, + }); + + @override + State createState() => _PlayingIndicatorIconState(); +} + +class _PlayingIndicatorIconState extends State { + late List<_BarAnimationParams> _animationParams; + final _random = Random(); + + @override + void initState() { + super.initState(); + _animationParams = List.generate( + widget.barCount, + _createRandomParams, + growable: false, + ); + } + + // Helper to generate random parameters for one bar's animation cycle + _BarAnimationParams _createRandomParams(int index) { + final duration1 = + (widget.baseCycleDuration * (0.8 + _random.nextDouble() * 0.4)); + final duration2 = + (widget.baseCycleDuration * (0.8 + _random.nextDouble() * 0.4)); + + // Note: These factors represent the scale relative to the *half-height* + // if centerSymmetric is true, controlled by the alignment in scaleY. + final targetHeightFactor1 = + widget.minHeightFactor + + _random.nextDouble() * + (widget.maxHeightFactor - widget.minHeightFactor); + final targetHeightFactor2 = + widget.minHeightFactor + + _random.nextDouble() * + (widget.maxHeightFactor - widget.minHeightFactor); + + // --- Random initial delay --- + final initialDelay = + (_random.nextDouble() * (widget.baseCycleDuration.inMilliseconds / 4)) + .ms; + + return _BarAnimationParams( + duration1: duration1, + duration2: duration2, + targetHeightFactor1: targetHeightFactor1, + targetHeightFactor2: targetHeightFactor2, + initialDelay: initialDelay, + ); + } + + @override + Widget build(BuildContext context) { + final color = + widget.color ?? + IconTheme.of(context).color ?? + Theme.of(context).colorScheme.primary; + + // --- Bar geometry calculation --- + final double totalSpacing = widget.size * 0.2; + // Ensure at least 1px spacing if size is very small + final double barSpacing = max(1.0, totalSpacing / (widget.barCount + 1)); + final double availableWidthForBars = + widget.size - (barSpacing * (widget.barCount + 1)); + final double barWidth = max(1.0, availableWidthForBars / widget.barCount); + // Max height remains the full size potential for the container + final double maxHeight = widget.size; + + // Determine the alignment for scaling based on the symmetric flag + final Alignment scaleAlignment = widget.centerSymmetric + ? Alignment.center + : Alignment.bottomCenter; + + // Determine the cross axis alignment for the Row + final CrossAxisAlignment rowAlignment = widget.centerSymmetric + ? CrossAxisAlignment.center + : CrossAxisAlignment.end; + + return SizedBox( + width: widget.size, + height: widget.size, + // Clip ensures bars don't draw outside the SizedBox bounds + // especially important for center alignment if maxFactor > 0.5 + child: ClipRect( + child: Row( + // Use calculated alignment + crossAxisAlignment: rowAlignment, + // Use spaceEvenly for better distribution, especially with center alignment + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: List.generate(widget.barCount, (index) { + final params = _animationParams[index]; + // The actual bar widget that will be animated + return Container( + width: barWidth, + // Set initial height to the max potential height + // The scaleY animation will control the visible height + height: maxHeight, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(barWidth / 2), + ), + ) + .animate( + delay: params.initialDelay, + onPlay: (controller) => controller.repeat(reverse: true), + ) + // 1. Scale to targetHeightFactor1 + .scaleY( + begin: widget.minHeightFactor, // Scale factor starts near min + end: params.targetHeightFactor1, + duration: params.duration1, + curve: Curves.easeInOutCirc, + alignment: scaleAlignment, // Apply chosen alignment + ) + // 2. Then scale to targetHeightFactor2 + .then() + .scaleY( + end: params.targetHeightFactor2, + duration: params.duration2, + curve: Curves.easeInOutCirc, + alignment: scaleAlignment, // Apply chosen alignment + ); + }, growable: false), + ), + ), + ); + } +} + +// Helper class: Renamed height fields for clarity +class _BarAnimationParams { + final Duration duration1; + final Duration duration2; + final double targetHeightFactor1; // Factor relative to total size + final double targetHeightFactor2; // Factor relative to total size + final Duration initialDelay; + + _BarAnimationParams({ + required this.duration1, + required this.duration2, + required this.targetHeightFactor1, + required this.targetHeightFactor2, + required this.initialDelay, + }); +} diff --git a/lib/features/player/view/widgets/speed_selector.dart b/lib/features/player/view/widgets/speed_selector.dart index a93550e..f14604a 100644 --- a/lib/features/player/view/widgets/speed_selector.dart +++ b/lib/features/player/view/widgets/speed_selector.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -5,201 +7,255 @@ import 'package:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/settings/app_settings_provider.dart'; +const double itemExtent = 25; + class SpeedSelector extends HookConsumerWidget { - const SpeedSelector({ - super.key, - required this.onSpeedSelected, - }); + const SpeedSelector({super.key, required this.onSpeedSelected}); final void Function(double speed) onSpeedSelected; @override Widget build(BuildContext context, WidgetRef ref) { final appSettings = ref.watch(appSettingsProvider); - final speeds = appSettings.playerSettings.speedOptions; + final playerSettings = appSettings.playerSettings; + final speeds = playerSettings.speedOptions; final currentSpeed = ref.watch(audiobookPlayerProvider).speed; final speedState = useState(currentSpeed); // hook the onSpeedSelected function to the state - useEffect( - () { - onSpeedSelected(speedState.value); - return null; - }, - [speedState.value], - ); + useEffect(() { + onSpeedSelected(speedState.value); + return null; + }, [speedState.value]); // the speed options - const minSpeed = 0.1; - const maxSpeed = 4.0; - const speedIncrement = 0.05; - final availableSpeeds = ((maxSpeed - minSpeed) / speedIncrement).ceil(); - final availableSpeedsList = List.generate( - availableSpeeds, - (index) { - // need to round to 2 decimal place to avoid floating point errors - return double.parse( - (minSpeed + index * speedIncrement).toStringAsFixed(2), - ); - }, - ); + final minSpeed = min(speeds.reduce(min), playerSettings.minSpeed); + final maxSpeed = max(speeds.reduce(max), playerSettings.maxSpeed); + final speedIncrement = playerSettings.speedIncrement; + final availableSpeeds = ((maxSpeed - minSpeed) / speedIncrement).ceil() + 1; + final availableSpeedsList = List.generate(availableSpeeds, (index) { + // need to round to 2 decimal place to avoid floating point errors + return double.parse( + (minSpeed + index * speedIncrement).toStringAsFixed(2), + ); + }); - final scrollController = FixedExtentScrollController( + final scrollController = useFixedExtentScrollController( initialItem: availableSpeedsList.indexOf(currentSpeed), ); - const double itemExtent = 25; - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: Text( - 'Playback Speed: ${speedState.value}x', - style: Theme.of(context).textTheme.titleLarge, - ), - ), + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // the title + Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), + child: Center( + child: Text( + 'Playback Speed: ${speedState.value}x', + style: Theme.of(context).textTheme.titleLarge, ), ), - Flexible( - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - // a minus button to decrease the speed - IconButton.filledTonal( - icon: const Icon(Icons.remove), - onPressed: () { - // animate to index - 1 - final index = availableSpeedsList.indexOf(speedState.value); - if (index > 0) { - scrollController.animateToItem( - index - 1, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - }, - ), - Expanded( - child: ListWheelScrollViewX( - controller: scrollController, - scrollDirection: Axis.horizontal, - itemExtent: itemExtent, - diameterRatio: 1.5, squeeze: 1.2, - // useMagnifier: true, - // magnification: 1.5, - physics: const FixedExtentScrollPhysics(), - children: availableSpeedsList - .map( - (speed) => Column( - children: [ - // a vertical line - Container( - height: itemExtent * 2, - // thick if multiple of 1, thin if multiple of 0.5 and transparent if multiple of 0.05 - width: speed % 0.5 == 0 - ? 3 - : speed % 0.25 == 0 - ? 2 - : 0.5, - color: - Theme.of(context).colorScheme.onSurface, - ), - // the speed text but only at .5 increments of speed - if (speed % 0.25 == 0) - Text( - speed.toString(), - style: TextStyle( - color: Theme.of(context) - .colorScheme - .onSurface, - ), - ), - ], - ), - ) - .toList(), - onSelectedItemChanged: (index) { - speedState.value = availableSpeedsList[index]; - // onSpeedSelected(availableSpeedsList[index]); - // call after 500ms to avoid the scrollview from scrolling to the selected speed - // Future.delayed( - // const Duration(milliseconds: 100), - // () => onSpeedSelected(availableSpeedsList[index]), - // ); - }, - ), - ), - // a plus button to increase the speed - IconButton.filledTonal( - icon: const Icon(Icons.add), - onPressed: () { - // animate to index + 1 - final index = availableSpeedsList.indexOf(speedState.value); - if (index < availableSpeedsList.length - 1) { - scrollController.animateToItem( - index + 1, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - ); - } - }, - ), - ], + ), + + // a inverted triangle to indicate the speed selector + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Icon( + Icons.arrow_drop_down, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + + // the speed selector + Padding( + padding: const EdgeInsets.only(bottom: 8.0, right: 8.0, left: 8.0), + child: SizedBox( + height: 80, + child: SpeedWheel( + availableSpeedsList: availableSpeedsList, + speedState: speedState, + scrollController: scrollController, ), ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + ), + + // the speed buttons + Padding( + padding: const EdgeInsets.all(8.0), + child: Wrap( + spacing: 8.0, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, children: speeds .map( - (speed) => Flexible( - // the text button should be highlighted if the speed is selected - child: TextButton( - style: speed == speedState.value - ? TextButton.styleFrom( - backgroundColor: Theme.of(context) - .colorScheme - .primaryContainer, - foregroundColor: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ) - : null, - onPressed: () async { - // animate the wheel to the selected speed - var index = availableSpeedsList.indexOf(speed); - // if the speed is not in the list - if (index == -1) { - // find the nearest speed - final nearestSpeed = availableSpeedsList.firstWhere( - (element) => element > speed, - orElse: () => availableSpeedsList.last, - ); - index = availableSpeedsList.indexOf(nearestSpeed); - } - await scrollController.animateToItem( - index, - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, + (speed) => TextButton( + style: speed == speedState.value + ? TextButton.styleFrom( + backgroundColor: Theme.of( + context, + ).colorScheme.primaryContainer, + foregroundColor: Theme.of( + context, + ).colorScheme.onPrimaryContainer, + ) + // border if not selected + : TextButton.styleFrom( + side: BorderSide( + color: Theme.of( + context, + ).colorScheme.primaryContainer, + ), + ), + onPressed: () async { + // animate the wheel to the selected speed + var index = availableSpeedsList.indexOf(speed); + // if the speed is not in the list + if (index == -1) { + // find the nearest speed + final nearestSpeed = availableSpeedsList.firstWhere( + (element) => element > speed, + orElse: () => availableSpeedsList.last, ); + index = availableSpeedsList.indexOf(nearestSpeed); + } + await scrollController.animateToItem( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); - // call the onSpeedSelected function - speedState.value = speed; - }, - child: Text('$speed'), - ), + // call the onSpeedSelected function + speedState.value = speed; + }, + child: Text('$speed'), ), ) .toList(), ), - const SizedBox( - height: 8, - ), - ], - ), + ), + ], + ); + } +} + +class SpeedWheel extends StatelessWidget { + const SpeedWheel({ + super.key, + required this.availableSpeedsList, + required this.speedState, + required this.scrollController, + this.showIncrementButtons = true, + }); + + final List availableSpeedsList; + final ValueNotifier speedState; + final FixedExtentScrollController scrollController; + final bool showIncrementButtons; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // a minus button to decrease the speed + if (showIncrementButtons) + IconButton.filledTonal( + icon: const Icon(Icons.remove), + onPressed: () { + // animate to index - 1 + final index = availableSpeedsList.indexOf(speedState.value); + if (index > 0) { + scrollController.animateToItem( + index - 1, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + ), + // the speed selector wheel + Flexible( + child: ListWheelScrollViewX( + controller: scrollController, + scrollDirection: Axis.horizontal, + itemExtent: itemExtent, + diameterRatio: 1.5, + squeeze: 1.2, + // useMagnifier: true, + // magnification: 1.5, + physics: const FixedExtentScrollPhysics(), + children: availableSpeedsList + .map((speed) => SpeedLine(speed: speed)) + .toList(), + onSelectedItemChanged: (index) { + speedState.value = availableSpeedsList[index]; + }, + ), + ), + + if (showIncrementButtons) + // a plus button to increase the speed + IconButton.filledTonal( + icon: const Icon(Icons.add), + onPressed: () { + // animate to index + 1 + final index = availableSpeedsList.indexOf(speedState.value); + if (index < availableSpeedsList.length - 1) { + scrollController.animateToItem( + index + 1, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + ), + ], + ); + } +} + +class SpeedLine extends StatelessWidget { + const SpeedLine({super.key, required this.speed}); + + final double speed; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // a vertical line + Expanded( + child: Container( + // thick if multiple of 1, thin if multiple of 0.5 and transparent if multiple of 0.05 + width: speed % 0.5 == 0 + ? 3 + : speed % 0.25 == 0 + ? 2 + : 0.5, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + // the speed text but only at .5 increments of speed + // if (speed % 0.25 == 0) + Opacity( + opacity: speed % 0.25 == 0 ? 1 : 0, + child: Text.rich( + TextSpan( + text: speed.floor().toString(), + children: [ + TextSpan( + text: '.${speed.toStringAsFixed(2).split('.').last}', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall?.fontSize, + ), + ), + ], + ), + ), + ), + ], ); } } diff --git a/lib/features/shake_detection/core/shake_detector.dart b/lib/features/shake_detection/core/shake_detector.dart new file mode 100644 index 0000000..effa131 --- /dev/null +++ b/lib/features/shake_detection/core/shake_detector.dart @@ -0,0 +1,86 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:logging/logging.dart'; +import 'package:sensors_plus/sensors_plus.dart'; +import 'package:vaani/settings/models/app_settings.dart'; + +final _logger = Logger('ShakeDetector'); + +class ShakeDetector { + final ShakeDetectionSettings _settings; + final Function()? onShakeDetected; + + ShakeDetector( + this._settings, + this.onShakeDetected, { + startImmediately = true, + }) { + _logger.fine('ShakeDetector created with settings: $_settings'); + if (startImmediately) { + start(); + } + } + + StreamSubscription? _accelerometerSubscription; + + int _currentShakeCount = 0; + + DateTime _lastShakeTime = DateTime.now(); + + final StreamController + _detectedShakeStreamController = StreamController.broadcast(); + + void start() { + if (_accelerometerSubscription != null) { + _logger.warning('ShakeDetector is already running'); + return; + } + _accelerometerSubscription = + userAccelerometerEventStream( + samplingPeriod: _settings.samplingPeriod, + ).listen((event) { + _logger.finest('RMS: ${event.rms}'); + if (event.rms > _settings.threshold) { + _currentShakeCount++; + + if (_currentShakeCount >= _settings.shakeTriggerCount && + !isCoolDownNeeded()) { + _logger.fine('Shake detected $_currentShakeCount times'); + + onShakeDetected?.call(); + _detectedShakeStreamController.add(event); + + _lastShakeTime = DateTime.now(); + _currentShakeCount = 0; + } + } else { + _currentShakeCount = 0; + } + }); + + _logger.fine('ShakeDetector started'); + } + + void stop() { + _currentShakeCount = 0; + _accelerometerSubscription?.cancel(); + _accelerometerSubscription = null; + _detectedShakeStreamController.close(); + _logger.fine('ShakeDetector stopped'); + } + + void dispose() { + stop(); + } + + bool isCoolDownNeeded() { + return _lastShakeTime + .add(_settings.shakeTriggerCoolDown) + .isAfter(DateTime.now()); + } +} + +extension UserAccelerometerEventRMS on UserAccelerometerEvent { + double get rms => sqrt(x * x + y * y + z * z); +} diff --git a/lib/features/shake_detection/providers/shake_detector.dart b/lib/features/shake_detection/providers/shake_detector.dart new file mode 100644 index 0000000..fa17920 --- /dev/null +++ b/lib/features/shake_detection/providers/shake_detector.dart @@ -0,0 +1,174 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart' + show audiobookPlayerProvider, simpleAudiobookPlayerProvider; +import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' + show sleepTimerProvider; +import 'package:vaani/settings/app_settings_provider.dart' + show appSettingsProvider; +import 'package:vaani/settings/models/app_settings.dart'; +import 'package:vibration/vibration.dart'; + +import '../core/shake_detector.dart' as core; + +part 'shake_detector.g.dart'; + +Logger _logger = Logger('ShakeDetector'); + +@riverpod +class ShakeDetector extends _$ShakeDetector { + bool wasPlayerLoaded = false; + + @override + core.ShakeDetector? build() { + final appSettings = ref.watch(appSettingsProvider); + final shakeDetectionSettings = appSettings.shakeDetectionSettings; + + if (!shakeDetectionSettings.isEnabled) { + _logger.config('Shake detection is disabled'); + return null; + } + + // if no book is loaded, shake detection should not be enabled + final player = ref.watch(simpleAudiobookPlayerProvider); + player.playerStateStream.listen((event) { + if (event.processingState == ProcessingState.idle && wasPlayerLoaded) { + _logger.config('Player is now not loaded, invalidating'); + wasPlayerLoaded = false; + ref.invalidateSelf(); + } + if (event.processingState != ProcessingState.idle && !wasPlayerLoaded) { + _logger.config('Player is now loaded, invalidating'); + wasPlayerLoaded = true; + ref.invalidateSelf(); + } + }); + + if (player.book == null) { + _logger.config('No book is loaded, disabling shake detection'); + wasPlayerLoaded = false; + return null; + } else { + _logger.finer('Book is loaded, marking player as loaded'); + wasPlayerLoaded = true; + } + + // if sleep timer is not enabled, shake detection should not be enabled + final sleepTimer = ref.watch(sleepTimerProvider); + if (!shakeDetectionSettings.shakeAction.isPlaybackManagementEnabled && + sleepTimer == null) { + _logger.config( + 'No playback management is enabled and sleep timer is off, ' + 'so shake detection is disabled', + ); + return null; + } + + _logger.config('Creating shake detector'); + final detector = core.ShakeDetector(shakeDetectionSettings, () { + final wasActionComplete = doShakeAction( + shakeDetectionSettings.shakeAction, + ref: ref, + ); + if (wasActionComplete) { + shakeDetectionSettings.feedback.forEach(postShakeFeedback); + } + }); + ref.onDispose(detector.dispose); + return detector; + } + + /// Perform the shake action and return whether the action was successful + bool doShakeAction(ShakeAction shakeAction, {required Ref ref}) { + final player = ref.read(simpleAudiobookPlayerProvider); + if (player.book == null && shakeAction.isPlaybackManagementEnabled) { + _logger.warning('No book is loaded'); + return false; + } + switch (shakeAction) { + case ShakeAction.resetSleepTimer: + _logger.fine('Resetting sleep timer'); + var sleepTimer = ref.read(sleepTimerProvider); + if (sleepTimer == null || !sleepTimer.isActive) { + _logger.warning('No sleep timer is running'); + return false; + } + sleepTimer.restartTimer(); + return true; + case ShakeAction.fastForward: + _logger.fine('Fast forwarding'); + if (!player.playing) { + _logger.warning('Player is not playing'); + return false; + } + player.seek(player.position + const Duration(seconds: 30)); + return true; + case ShakeAction.rewind: + _logger.fine('Rewinding'); + if (!player.playing) { + _logger.warning('Player is not playing'); + return false; + } + player.seek(player.position - const Duration(seconds: 30)); + return true; + case ShakeAction.playPause: + _logger.fine('Toggling play/pause'); + player.togglePlayPause(); + return true; + default: + return false; + } + } + + Future postShakeFeedback(ShakeDetectedFeedback feedback) async { + switch (feedback) { + case ShakeDetectedFeedback.vibrate: + _logger.fine('Vibrating'); + + if (await Vibration.hasAmplitudeControl() ?? false) { + Vibration.vibrate(amplitude: 128, duration: 200); + break; + } + + if (await Vibration.hasVibrator() ?? false) { + Vibration.vibrate(); + break; + } + + _logger.warning('No vibration support'); + + break; + case ShakeDetectedFeedback.beep: + _logger.fine('Beeping'); + final player = AudioPlayer(); + await player.setAsset('assets/sounds/beep.mp3'); + await player.setVolume(0.5); + await player.play(); + await player.dispose(); + break; + default: + break; + } + } +} + +extension on ShakeAction { + bool get isActiveWhenPaused { + // If the shake action is play/pause, it should be required when not playing + return this == ShakeAction.playPause; + } + + bool get isPlaybackManagementEnabled { + return { + ShakeAction.playPause, + ShakeAction.fastForward, + ShakeAction.rewind, + }.contains(this); + } + + bool get shouldActOnSleepTimer { + return {ShakeAction.resetSleepTimer}.contains(this); + } +} diff --git a/lib/features/shake_detection/providers/shake_detector.g.dart b/lib/features/shake_detection/providers/shake_detector.g.dart new file mode 100644 index 0000000..669d6d5 --- /dev/null +++ b/lib/features/shake_detection/providers/shake_detector.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'shake_detector.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(ShakeDetector) +final shakeDetectorProvider = ShakeDetectorProvider._(); + +final class ShakeDetectorProvider + extends $NotifierProvider { + ShakeDetectorProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'shakeDetectorProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$shakeDetectorHash(); + + @$internal + @override + ShakeDetector create() => ShakeDetector(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(core.ShakeDetector? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$shakeDetectorHash() => r'2a380bab1d4021d05d2ae40fec964a5f33d3730c'; + +abstract class _$ShakeDetector extends $Notifier { + core.ShakeDetector? build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + core.ShakeDetector?, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/features/sleep_timer/core/sleep_timer.dart b/lib/features/sleep_timer/core/sleep_timer.dart index fb38e0f..b774062 100644 --- a/lib/features/sleep_timer/core/sleep_timer.dart +++ b/lib/features/sleep_timer/core/sleep_timer.dart @@ -19,7 +19,17 @@ class SleepTimer { set duration(Duration value) { _duration = value; - reset(); + _logger.fine('duration set to $value'); + + /// if the timer is active, restart it with the new duration + /// if the timer is not active, do nothing + if (isActive && player.playing) { + _logger.fine('timer is active counting down with new duration'); + startCountDown(value); + } else { + _logger.fine('timer is not active'); + clearCountDownTimer(); + } } /// The player to be paused @@ -28,6 +38,9 @@ class SleepTimer { /// The timer that will pause the player Timer? timer; + /// is the sleep timer actively counting down + bool get isActive => timer != null && timer!.isActive; + /// for internal use only /// when the timer was started DateTime? startedAt; @@ -40,7 +53,7 @@ class SleepTimer { player.playbackEventStream.listen((event) { if (event.processingState == ProcessingState.completed || event.processingState == ProcessingState.idle) { - reset(); + clearCountDownTimer(); } }), ); @@ -49,18 +62,18 @@ class SleepTimer { _subscriptions.add( player.playerStateStream.listen((state) { if (state.playing && timer == null) { - startTimer(); + startCountDown(); } else if (!state.playing) { - reset(); + clearCountDownTimer(); } }), ); - _logger.fine('created with duration: $duration'); + _logger.info('created with duration: $duration'); } - /// resets the timer - void reset() { + /// resets the timer and stops it + void clearCountDownTimer() { if (timer != null) { timer!.cancel(); _logger.fine( @@ -70,19 +83,27 @@ class SleepTimer { } } + /// refills the timer with the default duration and starts it if the player is playing + /// if the player is not playing, the timer is stopped + void restartTimer() { + clearCountDownTimer(); + if (player.playing) { + startCountDown(); + } + _logger.info('restarted timer'); + } + /// starts the timer with the given duration or the default duration - void startTimer([ - Duration? forDuration, - ]) { - reset(); + void startCountDown([Duration? forDuration]) { + clearCountDownTimer(); duration = forDuration ?? duration; timer = Timer(duration, () { player.pause(); - reset(); + clearCountDownTimer(); _logger.fine('paused player after $duration'); }); startedAt = DateTime.now(); - _logger.fine('started for $duration at $startedAt'); + _logger.info('started for $duration at $startedAt'); } Duration get remainingTime { @@ -103,10 +124,10 @@ class SleepTimer { /// dispose the timer void dispose() { - reset(); + clearCountDownTimer(); for (var sub in _subscriptions) { sub.cancel(); } - _logger.fine('disposed'); + _logger.info('disposed'); } } diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.dart index 563c34e..5c9ac2d 100644 --- a/lib/features/sleep_timer/providers/sleep_timer_provider.dart +++ b/lib/features/sleep_timer/providers/sleep_timer_provider.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; -import 'package:vaani/features/sleep_timer/core/sleep_timer.dart' - as core; +import 'package:vaani/features/sleep_timer/core/sleep_timer.dart' as core; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/time_of_day.dart'; @@ -12,20 +11,16 @@ part 'sleep_timer_provider.g.dart'; class SleepTimer extends _$SleepTimer { @override core.SleepTimer? build() { - final appSettings = ref.watch(appSettingsProvider); - final sleepTimerSettings = appSettings.playerSettings.sleepTimerSettings; - bool isEnabled = sleepTimerSettings.autoTurnOnTimer; - if (!isEnabled) { + final sleepTimerSettings = ref.watch(sleepTimerSettingsProvider); + if (!sleepTimerSettings.autoTurnOnTimer) { return null; } if ((!sleepTimerSettings.alwaysAutoTurnOnTimer) && - (sleepTimerSettings.autoTurnOnTime - .toTimeOfDay() - .isAfter(TimeOfDay.now()) && - sleepTimerSettings.autoTurnOffTime - .toTimeOfDay() - .isBefore(TimeOfDay.now()))) { + !shouldBuildRightNow( + sleepTimerSettings.autoTurnOnTime, + sleepTimerSettings.autoTurnOffTime, + )) { return null; } @@ -37,10 +32,16 @@ class SleepTimer extends _$SleepTimer { return sleepTimer; } - void setTimer(Duration resultingDuration) { + void setTimer(Duration? resultingDuration, {bool notifyListeners = true}) { + if (resultingDuration == null || resultingDuration.inSeconds == 0) { + cancelTimer(); + return; + } if (state != null) { state!.duration = resultingDuration; - ref.notifyListeners(); + if (notifyListeners) { + ref.notifyListeners(); + } } else { final timer = core.SleepTimer( duration: resultingDuration, @@ -48,11 +49,26 @@ class SleepTimer extends _$SleepTimer { ); ref.onDispose(timer.dispose); state = timer; + state!.startCountDown(); } } + void restartTimer() { + state?.restartTimer(); + + // ref.notifyListeners(); // see https://github.com/Dr-Blank/Vaani/pull/40 for more information on why this is commented out + } + void cancelTimer() { state?.dispose(); state = null; } } + +bool shouldBuildRightNow(Duration autoTurnOnTime, Duration autoTurnOffTime) { + final now = TimeOfDay.now(); + return now.isBetween( + autoTurnOnTime.toTimeOfDay(), + autoTurnOffTime.toTimeOfDay(), + ); +} diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart index ef930bf..a3b0e3e 100644 --- a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart +++ b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart @@ -6,20 +6,57 @@ part of 'sleep_timer_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$sleepTimerHash() => r'ad77e82c1b513bbc62815c64ce1ed403f92fc055'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning -/// See also [SleepTimer]. @ProviderFor(SleepTimer) -final sleepTimerProvider = - NotifierProvider.internal( - SleepTimer.new, - name: r'sleepTimerProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$sleepTimerHash, - dependencies: null, - allTransitiveDependencies: null, -); +final sleepTimerProvider = SleepTimerProvider._(); -typedef _$SleepTimer = 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 +final class SleepTimerProvider + extends $NotifierProvider { + SleepTimerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'sleepTimerProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$sleepTimerHash(); + + @$internal + @override + SleepTimer create() => SleepTimer(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(core.SleepTimer? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$sleepTimerHash() => r'2679454a217d0630a833d730557ab4e4feac2e56'; + +abstract class _$SleepTimer extends $Notifier { + core.SleepTimer? build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + core.SleepTimer?, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/features/sleep_timer/view/sleep_timer_button.dart b/lib/features/sleep_timer/view/sleep_timer_button.dart new file mode 100644 index 0000000..0f33b72 --- /dev/null +++ b/lib/features/sleep_timer/view/sleep_timer_button.dart @@ -0,0 +1,350 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:vaani/features/player/view/player_when_expanded.dart'; +import 'package:vaani/features/player/view/widgets/speed_selector.dart'; +import 'package:vaani/features/sleep_timer/core/sleep_timer.dart'; +import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' + show sleepTimerProvider; +import 'package:vaani/main.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/shared/extensions/duration_format.dart'; + +class SleepTimerButton extends HookConsumerWidget { + const SleepTimerButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final sleepTimer = ref.watch(sleepTimerProvider); + final durationState = useState(sleepTimer?.duration); + + // if sleep timer is not active, show the button with the sleep timer icon + // if the sleep timer is active, show the remaining time in a pill shaped container + return Tooltip( + message: 'Sleep Timer', + child: InkWell( + onTap: () async { + appLogger.fine('Sleep Timer button pressed'); + pendingPlayerModals++; + // show the sleep timer dialog + await showModalBottomSheet( + context: context, + barrierLabel: 'Sleep Timer', + builder: (context) { + return SleepTimerBottomSheet( + onDurationSelected: (duration) { + durationState.value = duration; + // ref + // .read(sleepTimerProvider.notifier) + // .setTimer(duration, notifyListeners: false); + }, + ); + }, + ); + pendingPlayerModals--; + ref.read(sleepTimerProvider.notifier).setTimer(durationState.value); + appLogger.fine( + 'Sleep Timer dialog closed with ${durationState.value}', + ); + }, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: sleepTimer == null + ? Icon( + Symbols.bedtime, + color: Theme.of(context).colorScheme.onSurface, + ) + : RemainingSleepTimeDisplay(timer: sleepTimer), + ), + ), + ); + } +} + +class SleepTimerBottomSheet extends HookConsumerWidget { + const SleepTimerBottomSheet({super.key, this.onDurationSelected}); + + final void Function(Duration?)? onDurationSelected; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final sleepTimer = ref.watch(sleepTimerProvider); + final sleepTimerSettings = ref.watch(sleepTimerSettingsProvider); + + final durationOptions = sleepTimerSettings.presetDurations; + final minDuration = Duration.zero; + final maxDuration = [ + ...durationOptions, + sleepTimerSettings.maxDuration, + ].reduce((a, b) => a > b ? a : b); + final incrementStep = Duration(minutes: 1); + final allPossibleDurations = [ + for (var i = minDuration; i <= maxDuration; i += incrementStep) i, + ]; + + final scrollController = useFixedExtentScrollController( + initialItem: allPossibleDurations.indexOf( + sleepTimer?.duration ?? minDuration, + ), + ); + + final durationState = useState( + sleepTimer?.duration ?? minDuration, + ); + + // useEffect to rebuild the sleep timer when the duration changes + useEffect(() { + onDurationSelected?.call(durationState.value); + return null; + }, [durationState.value]); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // the title + Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), + child: Center( + child: Text( + 'Sleep Timer', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + + // a inverted triangle to indicate the speed selector + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Icon( + Icons.arrow_drop_down, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + + // the speed selector + Padding( + padding: const EdgeInsets.only(bottom: 8.0, right: 8.0, left: 8.0), + child: SizedBox( + height: 80, + child: SleepTimerWheel( + durationState: durationState, + availableDurations: allPossibleDurations, + scrollController: scrollController, + ), + ), + ), + + // a cancel button to cancel the sleep timer + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: TextButton.icon( + onPressed: () { + ref.read(sleepTimerProvider.notifier).cancelTimer(); + onDurationSelected?.call(null); + Navigator.of(context).pop(); + }, + icon: const Icon(Symbols.bedtime_off), + label: const Text('Cancel Sleep Timer'), + ), + ), + + // the speed buttons + Padding( + padding: const EdgeInsets.all(8.0), + child: Wrap( + spacing: 8.0, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: durationOptions + .map( + (timerDuration) => TextButton( + style: timerDuration == durationState.value + ? TextButton.styleFrom( + backgroundColor: Theme.of( + context, + ).colorScheme.primaryContainer, + foregroundColor: Theme.of( + context, + ).colorScheme.onPrimaryContainer, + ) + // border if not selected + : TextButton.styleFrom( + side: BorderSide( + color: Theme.of( + context, + ).colorScheme.primaryContainer, + ), + ), + onPressed: () async { + // animate the wheel to the selected speed + var index = allPossibleDurations.indexOf(timerDuration); + // if the speed is not in the list + if (index == -1) { + // find the nearest speed + final nearestDuration = allPossibleDurations.firstWhere( + (element) => element > timerDuration, + orElse: () => allPossibleDurations.last, + ); + index = allPossibleDurations.indexOf(nearestDuration); + } + await scrollController.animateToItem( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + child: Text(timerDuration.smartBinaryFormat), + ), + ) + .toList(), + ), + ), + ], + ); + } +} + +class RemainingSleepTimeDisplay extends HookConsumerWidget { + const RemainingSleepTimeDisplay({super.key, required this.timer}); + + final SleepTimer timer; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final remainingTime = useStream(timer.remainingTimeStream).data; + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + child: Text( + timer.timer == null + ? timer.duration.smartBinaryFormat + : remainingTime?.smartBinaryFormat ?? '', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ); + } +} + +class SleepTimerWheel extends StatelessWidget { + const SleepTimerWheel({ + super.key, + required this.availableDurations, + required this.scrollController, + required this.durationState, + this.showIncrementButtons = true, + }); + + final List availableDurations; + final ValueNotifier durationState; + final FixedExtentScrollController scrollController; + final bool showIncrementButtons; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // a minus button to decrease the speed + if (showIncrementButtons) + IconButton.filledTonal( + icon: const Icon(Icons.remove), + onPressed: () { + // animate to index - 1 + final index = availableDurations.indexOf( + durationState.value ?? Duration.zero, + ); + if (index > 0) { + scrollController.animateToItem( + index - 1, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + ), + // the speed selector wheel + Flexible( + child: ListWheelScrollViewX( + controller: scrollController, + scrollDirection: Axis.horizontal, + itemExtent: itemExtent, + diameterRatio: 1.5, + squeeze: 1.2, + // useMagnifier: true, + // magnification: 1.5, + physics: const FixedExtentScrollPhysics(), + children: availableDurations + .map((duration) => DurationLine(duration: duration)) + .toList(), + onSelectedItemChanged: (index) { + durationState.value = availableDurations[index]; + }, + ), + ), + + if (showIncrementButtons) + // a plus button to increase the speed + IconButton.filledTonal( + icon: const Icon(Icons.add), + onPressed: () { + // animate to index + 1 + final index = availableDurations.indexOf( + durationState.value ?? Duration.zero, + ); + if (index < availableDurations.length - 1) { + scrollController.animateToItem( + index + 1, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + ), + ], + ); + } +} + +class DurationLine extends StatelessWidget { + const DurationLine({super.key, required this.duration}); + + final Duration duration; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // a vertical line + Expanded( + child: Container( + // thick if multiple of 1, thin if multiple of 0.5 and transparent if multiple of 0.05 + width: duration.inMinutes % 5 == 0 + ? 3 + : duration.inMinutes % 2.5 == 0 + ? 2 + : 0.5, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Opacity( + opacity: duration.inMinutes % 2.5 == 0 ? 1 : 0, + child: Text( + '${duration.inMinutes}m', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall?.fontSize, + ), + ), + ), + ], + ); + } +} diff --git a/lib/features/you/view/server_manager.dart b/lib/features/you/view/server_manager.dart index 2ab70ea..f8d13b0 100644 --- a/lib/features/you/view/server_manager.dart +++ b/lib/features/you/view/server_manager.dart @@ -2,452 +2,333 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/api/api_provider.dart'; -import 'package:vaani/api/authenticated_user_provider.dart'; -import 'package:vaani/api/server_provider.dart'; -import 'package:vaani/models/error_response.dart'; -import 'package:vaani/router/router.dart'; -import 'package:vaani/settings/api_settings_provider.dart'; +import 'package:vaani/api/api_provider.dart' show makeBaseUrl; +import 'package:vaani/api/authenticated_users_provider.dart' + show authenticatedUsersProvider; +import 'package:vaani/api/server_provider.dart' + show ServerAlreadyExistsException, audiobookShelfServerProvider; +import 'package:vaani/features/onboarding/view/user_login.dart' + show UserLoginWidget; +import 'package:vaani/features/player/view/mini_player_bottom_padding.dart' + show MiniPlayerBottomPadding; +import 'package:vaani/main.dart' show appLogger; +import 'package:vaani/router/router.dart' show Routes; +import 'package:vaani/settings/api_settings_provider.dart' + show apiSettingsProvider; import 'package:vaani/settings/models/models.dart' as model; -import 'package:vaani/shared/widgets/add_new_server.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart' show ObfuscateSet; +import 'package:vaani/shared/widgets/add_new_server.dart' show AddNewServer; class ServerManagerPage extends HookConsumerWidget { - const ServerManagerPage({ - super.key, - }); + const ServerManagerPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final apiSettings = ref.watch(apiSettingsProvider); - final registeredServers = ref.watch(audiobookShelfServerProvider); - final registeredServersAsList = registeredServers.toList(); - final availableUsers = ref.watch(authenticatedUserProvider); - final serverURIController = useTextEditingController(); - final formKey = GlobalKey(); - - debugPrint('registered servers: $registeredServers'); - debugPrint('available users: $availableUsers'); return Scaffold( - appBar: AppBar( - title: const Text('Manage Accounts'), - ), + appBar: AppBar(title: const Text('Manage Accounts')), body: Center( child: Padding( padding: const EdgeInsets.all(8.0), - child: Column( - // crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - const Text( - 'Registered Servers', - ), - Expanded( - child: ListView.builder( - itemCount: registeredServers.length, - reverse: true, - itemBuilder: (context, index) { - var registeredServer = registeredServersAsList[index]; - return ExpansionTile( - title: Text(registeredServer.serverUrl.toString()), - subtitle: Text( - 'Users: ${availableUsers.where((element) => element.server == registeredServer).length}', - ), - // trailing: _DeleteServerButton( - // registeredServer: registeredServer, - // ), - // children are list of users of this server - children: availableUsers - .where( - (element) => element.server == registeredServer, - ) - .map( - (e) => ListTile( - selected: apiSettings.activeUser == e, - leading: apiSettings.activeUser == e - ? const Icon(Icons.person) - : const Icon(Icons.person_off_outlined), - title: Text(e.username ?? 'Anonymous'), - onTap: apiSettings.activeUser == e - ? null - : () { - ref - .read(apiSettingsProvider.notifier) - .updateState( - apiSettings.copyWith( - activeUser: e, - ), - ); - // pop all routes and go to the home page - // while (context.canPop()) { - // context.pop(); - // } - context.goNamed( - Routes.home.name, - ); - }, - trailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Delete User'), - content: const Text( - 'Are you sure you want to delete this user?', - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - ref - .read( - authenticatedUserProvider - .notifier, - ) - .removeUser(e); - Navigator.of(context).pop(); - }, - child: const Text('Delete'), - ), - ], - ); - }, - ); - }, - ), - ), - ) - .nonNulls - .toList() - - // add buttons of delete server and add user to server at the end - ..addAll([ - ListTile( - leading: const Icon(Icons.person_add), - title: const Text('Add User'), - onTap: () async { - // open a dialog to add a new user with username and password or another method using only auth token - final addedUser = await showDialog( - context: context, - builder: (context) { - return _AddUserDialog( - server: registeredServer, - ); - }, - ); - - // if (addedUser != null) { - // // show a snackbar that the user has been added and ask if change to this user - // ScaffoldMessenger.of(context).showSnackBar( - // SnackBar( - // content: const Text( - // 'User added successfully, do you want to switch to this user?', - // ), - // action: SnackBarAction( - // label: 'Switch', - // onPressed: () { - // // set the active user - // ref - // .read(apiSettingsProvider.notifier) - // .updateState( - // apiSettings.copyWith( - // activeUser: addedUser, - // ), - // ); - - // context.goNamed(Routes.home.name); - // }, - // ), - // ), - // ); - // } - }, - ), - ListTile( - leading: const Icon(Icons.delete), - title: const Text('Delete Server'), - onTap: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Delete Server'), - content: const Text( - 'Are you sure you want to delete this server and all its users?', - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - ref - .read( - audiobookShelfServerProvider - .notifier, - ) - .removeServer( - registeredServer, - removeUsers: true, - ); - Navigator.of(context).pop(); - }, - child: const Text('Delete'), - ), - ], - ); - }, - ); - }, - ), - ]), - ); - }, - ), - ), - const SizedBox(height: 20), - const Padding( - padding: EdgeInsets.all(8.0), - child: Text('Add New Server'), - ), - Form( - key: formKey, - autovalidateMode: AutovalidateMode.onUserInteraction, - child: AddNewServer( - controller: serverURIController, - onPressed: () { - if (formKey.currentState!.validate()) { - try { - final newServer = model.AudiobookShelfServer( - serverUrl: makeBaseUrl(serverURIController.text), - ); - ref - .read(audiobookShelfServerProvider.notifier) - .addServer( - newServer, - ); - ref.read(apiSettingsProvider.notifier).updateState( - apiSettings.copyWith( - activeServer: newServer, - ), - ); - serverURIController.clear(); - } on ServerAlreadyExistsException catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(e.toString()), - ), - ); - } - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Invalid URL'), - ), - ); - } - }, - ), - ), - ], - ), + child: ServerManagerBody(), ), ), ); } } -class _AddUserDialog extends HookConsumerWidget { - const _AddUserDialog({ - super.key, - required this.server, - }); +class ServerManagerBody extends HookConsumerWidget { + const ServerManagerBody({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final registeredServers = ref.watch(audiobookShelfServerProvider); + final registeredServersAsList = registeredServers.toList(); + final availableUsers = ref.watch(authenticatedUsersProvider); + final apiSettings = ref.watch(apiSettingsProvider); + final serverURIController = useTextEditingController(); + final formKey = GlobalKey(); + + appLogger.fine('registered servers: ${registeredServers.obfuscate()}'); + appLogger.fine('available users: ${availableUsers.obfuscate()}'); + + return Column( + // crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const Text('Registered Servers'), + Expanded( + child: ListView.builder( + itemCount: registeredServers.length, + reverse: true, + itemBuilder: (context, index) { + var registeredServer = registeredServersAsList[index]; + return ExpansionTile( + title: Text(registeredServer.serverUrl.toString()), + subtitle: Text( + 'Users: ${availableUsers.where((element) => element.server == registeredServer).length}', + ), + // children are list of users of this server + children: + availableUsers + .where((element) => element.server == registeredServer) + .map((e) => AvailableUserTile(user: e)) + .nonNulls + .toList() + // add buttons of delete server and add user to server at the end + ..addAll([ + AddUserTile(server: registeredServer), + DeleteServerTile(server: registeredServer), + ]), + ); + }, + ), + ), + const SizedBox(height: 20), + const Padding( + padding: EdgeInsets.all(8.0), + child: Text('Add New Server'), + ), + Form( + key: formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: AddNewServer( + controller: serverURIController, + onPressed: () { + if (formKey.currentState!.validate()) { + try { + final newServer = model.AudiobookShelfServer( + serverUrl: makeBaseUrl(serverURIController.text), + ); + ref + .read(audiobookShelfServerProvider.notifier) + .addServer(newServer); + ref + .read(apiSettingsProvider.notifier) + .updateState( + apiSettings.copyWith(activeServer: newServer), + ); + serverURIController.clear(); + } on ServerAlreadyExistsException catch (e) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } else { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Invalid URL'))); + } + }, + ), + ), + MiniPlayerBottomPadding(), + ], + ); + } +} + +class DeleteServerTile extends HookConsumerWidget { + const DeleteServerTile({super.key, required this.server}); final model.AudiobookShelfServer server; @override Widget build(BuildContext context, WidgetRef ref) { - final usernameController = useTextEditingController(); - final passwordController = useTextEditingController(); - final authTokensController = useTextEditingController(); - final isPasswordVisible = useState(false); - final apiSettings = ref.watch(apiSettingsProvider); - final isMethodAuth = useState(false); - final api = ref.watch(audiobookshelfApiProvider(server.serverUrl)); - - final formKey = GlobalKey(); - - final serverErrorResponse = ErrorResponseHandler(); - - /// Login to the server and save the user - Future loginAndSave() async { - model.AuthenticatedUser? authenticatedUser; - if (isMethodAuth.value) { - api.token = authTokensController.text; - final success = await api.misc.authorize( - responseErrorHandler: serverErrorResponse.storeError, - ); - if (success != null) { - authenticatedUser = model.AuthenticatedUser( - server: server, - id: success.user.id, - username: success.user.username, - authToken: api.token!, - ); - } - } else { - final username = usernameController.text; - final password = passwordController.text; - final success = await api.login( - username: username, - password: password, - responseErrorHandler: serverErrorResponse.storeError, - ); - if (success != null) { - authenticatedUser = model.AuthenticatedUser( - server: server, - id: success.user.id, - username: username, - authToken: api.token!, - ); - } - } - // add the user to the list of users - if (authenticatedUser != null) { - ref.read(authenticatedUserProvider.notifier).addUser(authenticatedUser); - } else { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Login failed. Got response: ${serverErrorResponse.response.body} (${serverErrorResponse.response.statusCode})', - ), - ), - ); - } - return authenticatedUser; - } - - return AlertDialog( - // title: Text('Add User for ${server.serverUrl}'), - title: Text.rich( - TextSpan( - children: [ - TextSpan( - text: 'Add User for ', - style: Theme.of(context).textTheme.labelLarge, - ), - TextSpan( - text: server.serverUrl.toString(), - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, + return ListTile( + leading: const Icon(Icons.delete), + title: const Text('Delete Server'), + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Remove Server and Users'), + // Make content scrollable in case of smaller screens/keyboard + content: SingleChildScrollView( + child: Text.rich( + TextSpan( + children: [ + const TextSpan(text: 'This will remove the server '), + TextSpan( + text: server.serverUrl.host, + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + const TextSpan( + text: ' and all its users\' login info from this app.', + ), + ], ), - ), - ], - ), - ), - content: Form( - key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Wrap( - alignment: WrapAlignment.center, - spacing: 8.0, - children: [ - ChoiceChip( - label: const Text('Username/Password'), - selected: !isMethodAuth.value, - onSelected: (selected) { - isMethodAuth.value = !selected; - }, ), - ChoiceChip( - label: const Text('Auth Token'), - selected: isMethodAuth.value, - onSelected: (selected) { - isMethodAuth.value = selected; + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + ref + .read(audiobookShelfServerProvider.notifier) + .removeServer(server, removeUsers: true); + Navigator.of(context).pop(); + }, + child: const Text('Delete'), ), ], - ), - const SizedBox(height: 16), - if (isMethodAuth.value) - TextFormField( - controller: authTokensController, - decoration: const InputDecoration(labelText: 'Auth Token'), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter an auth token'; - } - return null; - }, - ) - else ...[ - TextFormField( - controller: usernameController, - decoration: const InputDecoration(labelText: 'Username'), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a username'; - } - return null; - }, - ), - TextFormField( - controller: passwordController, - decoration: InputDecoration( - labelText: 'Password', - suffixIcon: IconButton( - icon: Icon( - isPasswordVisible.value - ? Icons.visibility - : Icons.visibility_off, - ), - onPressed: () { - isPasswordVisible.value = !isPasswordVisible.value; - }, - ), - ), - obscureText: !isPasswordVisible.value, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a password'; - } - return null; - }, - ), - ], - ], - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); + ); }, - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: () async { - if (formKey.currentState!.validate()) { - final addedUser = await loginAndSave(); - if (addedUser != null) { - Navigator.of(context).pop(addedUser); - } - } - }, - child: const Text('Add User'), - ), - ], + ); + }, + ); + } +} + +class AddUserTile extends HookConsumerWidget { + const AddUserTile({super.key, required this.server}); + + final model.AudiobookShelfServer server; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListTile( + leading: const Icon(Icons.person_add), + title: const Text('Add User'), + onTap: () async { + await showDialog( + context: context, + // barrierDismissible: false, // Optional: prevent closing by tapping outside + builder: (dialogContext) { + // Use a different context name to avoid conflicts + return AlertDialog( + title: Text('Add User to ${server.serverUrl.host}'), + // Make content scrollable in case of smaller screens/keyboard + content: SingleChildScrollView( + child: UserLoginWidget( + server: server.serverUrl, + // Pass the callback to pop the dialog on success + onSuccess: (user) { + // Add the user to the server + ref.read(authenticatedUsersProvider.notifier).addUser(user); + Navigator.of(dialogContext).pop(); // Close the dialog + // Optional: Show a confirmation SnackBar + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('User added successfully! Switch?'), + action: SnackBarAction( + label: 'Switch', + onPressed: () { + // Switch to the new user + ref + .read(apiSettingsProvider.notifier) + .updateState( + ref + .read(apiSettingsProvider) + .copyWith(activeUser: user), + ); + context.goNamed(Routes.home.name); + }, + ), + ), + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(dialogContext).pop(); // Close the dialog + }, + child: const Text('Cancel'), + ), + ], + ); + }, + ); + // No need for the SnackBar asking to switch user here anymore. + }, + ); + } +} + +class AvailableUserTile extends HookConsumerWidget { + const AvailableUserTile({super.key, required this.user}); + + final model.AuthenticatedUser user; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final apiSettings = ref.watch(apiSettingsProvider); + + return ListTile( + selected: apiSettings.activeUser == user, + leading: apiSettings.activeUser == user + ? const Icon(Icons.person) + : const Icon(Icons.person_off_outlined), + title: Text(user.username ?? 'Anonymous'), + onTap: apiSettings.activeUser == user + ? null + : () { + ref + .read(apiSettingsProvider.notifier) + .updateState(apiSettings.copyWith(activeUser: user)); + // pop all routes and go to the home page + // while (context.canPop()) { + // context.pop(); + // } + context.goNamed(Routes.home.name); + }, + trailing: IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Remove User Login'), + content: Text.rich( + TextSpan( + children: [ + const TextSpan( + text: 'This will remove login details of the user ', + ), + TextSpan( + text: user.username ?? 'Anonymous', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Theme.of(context).colorScheme.primary, + ), + ), + const TextSpan(text: ' from this app.'), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + ref + .read(authenticatedUsersProvider.notifier) + .removeUser(user); + Navigator.of(context).pop(); + }, + child: const Text('Delete'), + ), + ], + ); + }, + ); + }, + ), ); } } diff --git a/lib/features/you/view/widgets/library_switch_chip.dart b/lib/features/you/view/widgets/library_switch_chip.dart new file mode 100644 index 0000000..2255c7a --- /dev/null +++ b/lib/features/you/view/widgets/library_switch_chip.dart @@ -0,0 +1,216 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shelfsdk/audiobookshelf_api.dart' show Library; +import 'package:vaani/api/library_provider.dart'; +import 'package:vaani/settings/api_settings_provider.dart' + show apiSettingsProvider; +import 'package:vaani/shared/icons/abs_icons.dart'; +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart'; +import 'package:vaani/main.dart' show appLogger; + +class LibrarySwitchChip extends HookConsumerWidget { + const LibrarySwitchChip({super.key, required this.libraries}); + final List libraries; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final apiSettings = ref.watch(apiSettingsProvider); + + return ActionChip( + avatar: Icon( + AbsIcons.getIconByName( + apiSettings.activeLibraryId != null + ? libraries + .firstWhere((lib) => lib.id == apiSettings.activeLibraryId) + .icon + : libraries.first.icon, + ), + ), // Replace with your icon + label: const Text('Change Library'), + // Enable only if libraries are loaded and not empty + onPressed: libraries.isNotEmpty + ? () => showLibrarySwitcher(context, ref) + : null, // Disable if no libraries + ); + } +} + +// --- Helper Function to Show the Switcher --- +void showLibrarySwitcher(BuildContext context, WidgetRef ref) { + final content = _LibrarySelectionContent(); + + // --- Platform-Specific UI --- + bool isDesktop = false; + if (!kIsWeb) { + // dart:io Platform is not available on web + isDesktop = Platform.isLinux || Platform.isMacOS || Platform.isWindows; + } else { + // Basic web detection (might need refinement based on screen size) + // Consider using MediaQuery for a size-based check instead for web/tablet + final size = MediaQuery.of(context).size; + isDesktop = size.width > 600; // Example threshold for "desktop-like" layout + } + + if (isDesktop) { + // --- Desktop: Use AlertDialog --- + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: const Text('Select Library'), + content: SizedBox( + // Constrain size for dialogs + width: 300, // Adjust as needed + // Make content scrollable if list is long + child: Scrollbar(child: content), + ), + actions: [ + TextButton( + onPressed: () { + // Invalidate the provider to trigger a refetch + ref.invalidate(librariesProvider); + Navigator.pop(dialogContext); + }, + child: const Text('Refresh'), + ), + TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: const Text('Cancel'), + ), + ], + ), + ); + } else { + // --- Mobile/Tablet: Use BottomSheet --- + showModalBottomSheet( + context: context, + // Make it scrollable and control height + isScrollControlled: true, + constraints: BoxConstraints( + maxHeight: + MediaQuery.of(context).size.height * 0.6, // Max 60% of screen + ), + builder: (sheetContext) => Padding( + // Add padding within the bottom sheet + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, // Take minimum necessary height + children: [ + const Text( + 'Select Library', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10), + const Divider(), + Flexible( + // Allow the list to take remaining space and scroll + child: Scrollbar(child: content), + ), + const SizedBox(height: 10), + ElevatedButton.icon( + icon: const Icon(Icons.refresh), + label: const Text('Refresh'), + onPressed: () { + // Invalidate the provider to trigger a refetch + ref.invalidate(librariesProvider); + }, + ), + ], + ), + ), + ); + } +} + +// --- Widget for the Selection List Content (Reusable) --- +class _LibrarySelectionContent extends ConsumerWidget { + @override + Widget build(BuildContext context, WidgetRef ref) { + final librariesAsyncValue = ref.watch(librariesProvider); + final currentLibraryId = ref.watch( + apiSettingsProvider.select((settings) => settings.activeLibraryId), + ); + final errorColor = Theme.of(context).colorScheme.error; + return librariesAsyncValue.when( + // --- Loading State --- + loading: () => const Center(child: CircularProgressIndicator()), + + // --- Error State --- + error: (error, stackTrace) => Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error_outline, color: errorColor), + const SizedBox(height: 10), + Text( + 'Error loading libraries: $error', + textAlign: TextAlign.center, + style: TextStyle(color: errorColor), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + onPressed: () { + // Invalidate the provider to trigger a refetch + ref.invalidate(librariesProvider); + }, + ), + ], + ), + ), + ), + + // --- Data State --- + data: (libraries) { + // Handle case where data loaded successfully but is empty + if (libraries.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: Text('No libraries available.'), + ), + ); + } + + // Build the list if libraries are available + return Scrollbar( + // Add scrollbar for potentially long lists + child: ListView.builder( + shrinkWrap: true, // Important for Dialog/BottomSheet sizing + itemCount: libraries.length, + itemBuilder: (context, index) { + final library = libraries[index]; + final bool isSelected = library.id == currentLibraryId; + + return ListTile( + title: Text(library.name), + leading: Icon(AbsIcons.getIconByName(library.icon)), + selected: isSelected, + trailing: isSelected ? const Icon(Icons.check) : null, + onTap: () { + appLogger.info( + 'Selected library: ${library.name} (ID: ${library.id})', + ); + // Get current settings state + final currentSettings = ref.read(apiSettingsProvider); + // Update the active library ID + ref + .read(apiSettingsProvider.notifier) + .updateState( + currentSettings.copyWith(activeLibraryId: library.id), + ); + // Close the dialog/bottom sheet + Navigator.pop(context); + }, + ); + }, + ), + ); + }, + ); + } +} diff --git a/lib/features/you/view/you_page.dart b/lib/features/you/view/you_page.dart index a171a70..fba9048 100644 --- a/lib/features/you/view/you_page.dart +++ b/lib/features/you/view/you_page.dart @@ -1,54 +1,42 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; +import 'package:vaani/api/library_provider.dart' show librariesProvider; +import 'package:vaani/features/player/view/mini_player_bottom_padding.dart'; +import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'; import 'package:vaani/router/router.dart'; +import 'package:vaani/settings/constants.dart'; import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/widgets/not_implemented.dart'; +import 'package:vaani/shared/widgets/vaani_logo.dart'; class YouPage extends HookConsumerWidget { - const YouPage({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final me = ref.watch(meProvider); - return me.when( - data: (data) { - return _YouPage(userData: data); - }, - loading: () => const CircularProgressIndicator(), - error: (error, stack) => Text('Error: $error'), - ); - } -} - -class _YouPage extends HookConsumerWidget { - const _YouPage({ - super.key, - required this.userData, - }); - - final User userData; + const YouPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final api = ref.watch(authenticatedApiProvider); + final librariesAsyncValue = ref.watch(librariesProvider); return Scaffold( appBar: AppBar( // title: const Text('You'), - backgroundColor: Colors.transparent, actions: [ + IconButton( + tooltip: 'Logs', + icon: const Icon(Icons.bug_report), + onPressed: () { + context.pushNamed(Routes.logs.name); + }, + ), // IconButton( // icon: const Icon(Icons.edit), // onPressed: () { // // Handle edit profile // }, // ), - // settings button IconButton( + tooltip: 'Settings', icon: const Icon(Icons.settings), onPressed: () { context.pushNamed(Routes.settings.name); @@ -64,30 +52,7 @@ class _YouPage extends HookConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( - children: [ - CircleAvatar( - radius: 40, - // backgroundImage: NetworkImage(userData.avatarUrl), - // first letter of the username - child: Text( - userData.username[0].toUpperCase(), - style: const TextStyle( - fontSize: 32, - fontWeight: FontWeight.bold, - ), - ), - ), - const SizedBox(width: 16), - Text( - userData.username, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - ), - ], - ), + UserBar(), const SizedBox(height: 16), Wrap( spacing: 8, @@ -99,7 +64,36 @@ class _YouPage extends HookConsumerWidget { context.pushNamed(Routes.userManagement.name); }, ), - // ActionChip( + librariesAsyncValue.when( + data: (libraries) => + LibrarySwitchChip(libraries: libraries), + loading: () => const ActionChip( + avatar: SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ), + label: Text('Loading Libs...'), + onPressed: null, // Disable while loading + ), + error: (error, stack) => ActionChip( + avatar: Icon( + Icons.error_outline, + color: Theme.of(context).colorScheme.error, + ), + label: const Text('Error Loading Libs'), + onPressed: () { + // Maybe show error details or allow retry + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Failed to load libraries: $error', + ), + ), + ); + }, + ), + ), // ActionChip( // avatar: const Icon(Icons.logout), // label: const Text('Logout'), // onPressed: () { @@ -121,21 +115,6 @@ class _YouPage extends HookConsumerWidget { title: const Text('My Playlists'), onTap: () { // Handle navigation to playlists - }, - ), - ListTile( - leading: const Icon(Icons.help), - title: const Text('Help'), - onTap: () { - // Handle navigation to help website - showNotImplementedToast(context); - }, - ), - ListTile( - leading: const Icon(Icons.info), - title: const Text('About'), - onTap: () { - // Handle navigation to about showNotImplementedToast(context); }, ), @@ -149,16 +128,102 @@ class _YouPage extends HookConsumerWidget { ); }, ), - // const SizedBox(height: 16), - // const Text('App Version: 1.0.0'), - // const Text('Server Version: 1.0.0'), - // const Text('Author: Your Name'), + ListTile( + leading: const Icon(Icons.help), + title: const Text('Help'), + onTap: () { + // Handle navigation to help website + showNotImplementedToast(context); + }, + ), + AboutListTile( + icon: const Icon(Icons.info), + applicationName: AppMetadata.appName, + applicationVersion: AppMetadata.version, + applicationLegalese: + 'Made with ❤️ by ${AppMetadata.author}', + aboutBoxChildren: [ + // link to github repo + ListTile( + leading: Icon(Icons.code), + title: Text('Source Code'), + onTap: () { + handleLaunchUrl(AppMetadata.githubRepo); + }, + ), + ], + // apply blend mode to the icon to match the primary color + applicationIcon: ColorFiltered( + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.primary, + BlendMode.srcIn, + ), + child: const VaaniLogo(size: 48), + ), + ), ], ), ), ), + SliverToBoxAdapter(child: MiniPlayerBottomPadding()), ], ), ); } } + +class UserBar extends HookConsumerWidget { + const UserBar({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final me = ref.watch(meProvider); + final api = ref.watch(authenticatedApiProvider); + + final themeData = Theme.of(context); + final textTheme = themeData.textTheme; + + return me.when( + data: (userData) { + return Row( + children: [ + CircleAvatar( + radius: 40, + // backgroundImage: NetworkImage(userData.avatarUrl), + // first letter of the username + child: Text( + userData.username[0].toUpperCase(), + style: textTheme.headlineLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + userData.username, + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + api.baseUrl.toString(), + style: textTheme.bodyMedium?.copyWith( + color: themeData.colorScheme.onSurface.withValues( + alpha: 0.6, + ), + ), + ), + ], + ), + ], + ); + }, + loading: () => const CircularProgressIndicator(), + error: (error, stack) => Text('Error: $error'), + ); + } +} diff --git a/lib/hacks/fix_autofill_losing_focus.dart b/lib/hacks/fix_autofill_losing_focus.dart index ffa7da6..3763654 100644 --- a/lib/hacks/fix_autofill_losing_focus.dart +++ b/lib/hacks/fix_autofill_losing_focus.dart @@ -14,10 +14,7 @@ import 'package:flutter/material.dart'; class InactiveFocusScopeObserver extends StatefulWidget { final Widget child; - const InactiveFocusScopeObserver({ - super.key, - required this.child, - }); + const InactiveFocusScopeObserver({super.key, required this.child}); @override State createState() => @@ -39,10 +36,8 @@ class _InactiveFocusScopeObserverState } @override - Widget build(BuildContext context) => FocusScope( - node: _focusScope, - child: widget.child, - ); + Widget build(BuildContext context) => + FocusScope(node: _focusScope, child: widget.child); @override void dispose() { diff --git a/lib/main.dart b/lib/main.dart index 2630802..0eb4cc0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,22 +1,22 @@ -import 'package:audio_session/audio_session.dart'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:just_audio_background/just_audio_background.dart' - show JustAudioBackground; -import 'package:just_audio_media_kit/just_audio_media_kit.dart' - show JustAudioMediaKit; import 'package:logging/logging.dart'; import 'package:vaani/api/server_provider.dart'; import 'package:vaani/db/storage.dart'; -import 'package:vaani/features/downloads/core/download_manager.dart'; import 'package:vaani/features/downloads/providers/download_manager.dart'; +import 'package:vaani/features/logging/core/logger.dart'; import 'package:vaani/features/playback_reporting/providers/playback_reporter_provider.dart'; -import 'package:vaani/features/player/providers/audiobook_player.dart'; +import 'package:vaani/features/player/core/init.dart'; +import 'package:vaani/features/player/providers/audiobook_player.dart' + show audiobookPlayerProvider, simpleAudiobookPlayerProvider; +import 'package:vaani/features/shake_detection/providers/shake_detector.dart'; import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/shared/extensions/duration_format.dart'; +import 'package:vaani/theme/providers/system_theme_provider.dart'; +import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; import 'package:vaani/theme/theme.dart'; final appLogger = Logger('vaani'); @@ -24,40 +24,16 @@ final appLogger = Logger('vaani'); void main() async { WidgetsFlutterBinding.ensureInitialized(); // Configure the root Logger - Logger.root.level = Level.ALL; // Capture all logs - Logger.root.onRecord.listen((record) { - // Print log records to the console - debugPrint( - '${record.loggerName}: ${record.level.name}: ${record.time.time}: ${record.message}', - ); - }); - - // for playing audio on windows, linux - JustAudioMediaKit.ensureInitialized(); + await initLogging(); // initialize the storage await initStorage(); - // for configuring how this app will interact with other audio apps - final session = await AudioSession.instance; - await session.configure(const AudioSessionConfiguration.speech()); - - // for playing audio in the background - await JustAudioBackground.init( - androidNotificationChannelId: 'com.vaani.bg_demo.channel.audio', - androidNotificationChannelName: 'Audio playback', - androidNotificationOngoing: true, - ); - - // for initializing the download manager - await initDownloadManager(); + // initialize audio player + await configurePlayer(); // run the app - runApp( - const ProviderScope( - child: _EagerInitialization(child: MyApp()), - ), - ); + runApp(const ProviderScope(child: _EagerInitialization(child: MyApp()))); } var routerConfig = const MyAppRouter().config; @@ -75,19 +51,85 @@ class MyApp extends ConsumerWidget { if (needOnboarding) { routerConfig.goNamed(Routes.onboarding.name); } + final appSettings = ref.watch(appSettingsProvider); + final themeSettings = appSettings.themeSettings; + ColorScheme lightColorScheme = brandLightColorScheme; + ColorScheme darkColorScheme = brandDarkColorScheme; + + final shouldUseHighContrast = + themeSettings.highContrast || MediaQuery.of(context).highContrast; + + if (shouldUseHighContrast) { + lightColorScheme = lightColorScheme.copyWith(surface: Colors.white); + darkColorScheme = darkColorScheme.copyWith(surface: Colors.black); + } + + if (themeSettings.useMaterialThemeFromSystem) { + var themes = ref.watch( + systemThemeProvider(highContrast: shouldUseHighContrast), + ); + if (themes.value != null) { + lightColorScheme = themes.value!.$1; + darkColorScheme = themes.value!.$2; + } + } + + if (themeSettings.useCurrentPlayerThemeThroughoutApp) { + try { + final player = ref.watch(audiobookPlayerProvider); + if (player.book != null) { + final themeLight = ref.watch( + themeOfLibraryItemProvider( + player.book!.libraryItemId, + highContrast: shouldUseHighContrast, + brightness: Brightness.light, + ), + ); + final themeDark = ref.watch( + themeOfLibraryItemProvider( + player.book!.libraryItemId, + highContrast: shouldUseHighContrast, + brightness: Brightness.dark, + ), + ); + if (themeLight.value != null && themeDark.value != null) { + lightColorScheme = themeLight.value!; + darkColorScheme = themeDark.value!; + } + } + } catch (e) { + debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); + appLogger.severe('not building with player theme'); + appLogger.severe(e.toString()); + } + } + final appThemeLight = ThemeData( + useMaterial3: true, + colorScheme: lightColorScheme.harmonized(), + ); + final appThemeDark = ThemeData( + useMaterial3: true, + colorScheme: darkColorScheme.harmonized(), + brightness: Brightness.dark, + // TODO bottom sheet theme is not working + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: darkColorScheme.surface, + ), + ); try { return MaterialApp.router( // debugShowCheckedModeBanner: false, - theme: lightTheme, - darkTheme: darkTheme, - themeMode: ref.watch(appSettingsProvider).themeSettings.isDarkMode - ? ThemeMode.dark - : ThemeMode.light, + theme: appThemeLight, + darkTheme: appThemeDark, + themeMode: themeSettings.themeMode, routerConfig: routerConfig, + themeAnimationCurve: Curves.easeInOut, ); } catch (e) { debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); + appLogger.severe(e.toString()); + if (needOnboarding) { routerConfig.goNamed(Routes.onboarding.name); } @@ -111,8 +153,10 @@ class _EagerInitialization extends ConsumerWidget { ref.watch(sleepTimerProvider); ref.watch(playbackReporterProvider); ref.watch(simpleDownloadManagerProvider); + ref.watch(shakeDetectorProvider); } catch (e) { debugPrintStack(stackTrace: StackTrace.current, label: e.toString()); + appLogger.severe(e.toString()); } return child; diff --git a/lib/models/error_response.dart b/lib/models/error_response.dart index 2e4c926..13352be 100644 --- a/lib/models/error_response.dart +++ b/lib/models/error_response.dart @@ -1,19 +1,24 @@ import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; +import 'package:vaani/shared/extensions/obfuscation.dart'; final _logger = Logger('ErrorResponse'); class ErrorResponseHandler { String? name; http.Response _response; + bool logRawResponse; ErrorResponseHandler({ this.name, http.Response? response, + this.logRawResponse = false, }) : _response = response ?? http.Response('', 418); void storeError(http.Response response, [Object? error]) { - _logger.fine('for $name got response: $response'); + if (logRawResponse) { + _logger.fine('for $name got response: ${response.obfuscate()}'); + } _response = response; } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 5b3ab41..202ee79 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -3,8 +3,11 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/api_provider.dart'; +import 'package:vaani/main.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/api_settings_provider.dart'; +import 'package:vaani/settings/app_settings_provider.dart' + show appSettingsProvider; import '../shared/widgets/shelves/home_shelf.dart'; @@ -16,9 +19,10 @@ class HomePage extends HookConsumerWidget { final views = ref.watch(personalizedViewProvider); final apiSettings = ref.watch(apiSettingsProvider); final scrollController = useScrollController(); + final appSettings = ref.watch(appSettingsProvider); + final homePageSettings = appSettings.homePageSettings; return Scaffold( appBar: AppBar( - backgroundColor: Colors.transparent, title: GestureDetector( child: Text( 'Vaani', @@ -48,6 +52,11 @@ class HomePage extends HookConsumerWidget { // try again button ElevatedButton( onPressed: () { + ref + .read(apiSettingsProvider.notifier) + .updateState( + apiSettings.copyWith(activeLibraryId: null), + ); ref.invalidate(personalizedViewProvider); }, child: const Text('Try again'), @@ -59,12 +68,25 @@ class HomePage extends HookConsumerWidget { final shelvesToDisplay = data // .where((element) => !element.id.contains('discover')) .map((shelf) { - debugPrint('building shelf ${shelf.label}'); - return HomeShelf( - title: shelf.label, - shelf: shelf, - ); - }).toList(); + appLogger.fine('building shelf ${shelf.label}'); + // check if showPlayButton is enabled for the shelf + // using the id of the shelf + final showPlayButton = switch (shelf.id) { + 'continue-listening' => + homePageSettings.showPlayButtonOnContinueListeningShelf, + 'continue-series' => + homePageSettings.showPlayButtonOnContinueSeriesShelf, + 'listen-again' => + homePageSettings.showPlayButtonOnListenAgainShelf, + _ => homePageSettings.showPlayButtonOnAllRemainingShelves, + }; + return HomeShelf( + title: shelf.label, + shelf: shelf, + showPlayButton: showPlayButton, + ); + }) + .toList(); return RefreshIndicator( onRefresh: () async { return ref.refresh(personalizedViewProvider); @@ -72,7 +94,7 @@ class HomePage extends HookConsumerWidget { child: ListView.separated( itemBuilder: (context, index) => shelvesToDisplay[index], separatorBuilder: (context, index) => Divider( - color: Theme.of(context).dividerColor.withOpacity(0.1), + color: Theme.of(context).dividerColor.withValues(alpha: 0.1), indent: 16, endIndent: 16, ), @@ -113,10 +135,6 @@ class HomePageSkeleton extends StatelessWidget { @override Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ); + return const Scaffold(body: Center(child: CircularProgressIndicator())); } } diff --git a/lib/pages/library_page.dart b/lib/pages/library_page.dart index fb97221..d203987 100644 --- a/lib/pages/library_page.dart +++ b/lib/pages/library_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/api_provider.dart'; +import 'package:vaani/main.dart'; import 'package:vaani/settings/api_settings_provider.dart'; import '../shared/widgets/drawer.dart'; @@ -16,7 +17,9 @@ class LibraryPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { // set the library id as the active library if (libraryId != null) { - ref.read(apiSettingsProvider.notifier).updateState( + ref + .read(apiSettingsProvider.notifier) + .updateState( ref.watch(apiSettingsProvider).copyWith(activeLibraryId: libraryId), ); } @@ -47,12 +50,10 @@ class LibraryPage extends HookConsumerWidget { final shelvesToDisplay = data // .where((element) => !element.id.contains('discover')) .map((shelf) { - debugPrint('building shelf ${shelf.label}'); - return HomeShelf( - title: shelf.label, - shelf: shelf, - ); - }).toList(); + appLogger.fine('building shelf ${shelf.label}'); + return HomeShelf(title: shelf.label, shelf: shelf); + }) + .toList(); return RefreshIndicator( onRefresh: () async { return ref.refresh(personalizedViewProvider); @@ -60,7 +61,7 @@ class LibraryPage extends HookConsumerWidget { child: ListView.separated( itemBuilder: (context, index) => shelvesToDisplay[index], separatorBuilder: (context, index) => Divider( - color: Theme.of(context).dividerColor.withOpacity(0.1), + color: Theme.of(context).dividerColor.withValues(alpha: 0.1), indent: 16, endIndent: 16, ), @@ -84,10 +85,6 @@ class LibraryPageSkeleton extends StatelessWidget { @override Widget build(BuildContext context) { - return const Scaffold( - body: Center( - child: CircularProgressIndicator(), - ), - ); + return const Scaffold(body: Center(child: CircularProgressIndicator())); } } diff --git a/lib/router/constants.dart b/lib/router/constants.dart index 5073e60..3a556bf 100644 --- a/lib/router/constants.dart +++ b/lib/router/constants.dart @@ -3,14 +3,8 @@ part of 'router.dart'; class Routes { - static const home = _SimpleRoute( - pathName: '', - name: 'home', - ); - static const onboarding = _SimpleRoute( - pathName: 'login', - name: 'onboarding', - ); + static const home = _SimpleRoute(pathName: '', name: 'home'); + static const onboarding = _SimpleRoute(pathName: 'login', name: 'onboarding'); static const library = _SimpleRoute( pathName: 'library', pathParamName: 'libraryId', @@ -23,14 +17,36 @@ class Routes { ); // Local settings - static const settings = _SimpleRoute( - pathName: 'config', - name: 'settings', + static const settings = _SimpleRoute(pathName: 'config', name: 'settings'); + static const themeSettings = _SimpleRoute( + pathName: 'theme', + name: 'themeSettings', + parentRoute: settings, ); static const autoSleepTimerSettings = _SimpleRoute( - pathName: 'autosleeptimer', + pathName: 'autoSleepTimer', name: 'autoSleepTimerSettings', - // parentRoute: settings, + parentRoute: settings, + ); + static const notificationSettings = _SimpleRoute( + pathName: 'notifications', + name: 'notificationSettings', + parentRoute: settings, + ); + static const playerSettings = _SimpleRoute( + pathName: 'player', + name: 'playerSettings', + parentRoute: settings, + ); + static const shakeDetectorSettings = _SimpleRoute( + pathName: 'shakeDetector', + name: 'shakeDetectorSettings', + parentRoute: settings, + ); + static const homePageSettings = _SimpleRoute( + pathName: 'homePage', + name: 'homePageSettings', + parentRoute: settings, ); // search and explore @@ -39,10 +55,7 @@ class Routes { name: 'search', // parentRoute: library, ); - static const explore = _SimpleRoute( - pathName: 'explore', - name: 'explore', - ); + static const explore = _SimpleRoute(pathName: 'explore', name: 'explore'); // downloads static const downloads = _SimpleRoute( @@ -58,10 +71,7 @@ class Routes { ); // you page for the user - static const you = _SimpleRoute( - pathName: 'you', - name: 'you', - ); + static const you = _SimpleRoute(pathName: 'you', name: 'you'); // user management static const userManagement = _SimpleRoute( @@ -75,6 +85,9 @@ class Routes { name: 'openIDCallback', parentRoute: onboarding, ); + + // logs page + static const logs = _SimpleRoute(pathName: 'logs', name: 'logs'); } // a class to store path diff --git a/lib/router/models/library_item_extras.dart b/lib/router/models/library_item_extras.dart index 089d649..e67a508 100644 --- a/lib/router/models/library_item_extras.dart +++ b/lib/router/models/library_item_extras.dart @@ -1,7 +1,5 @@ // a freezed class to store the settings of the app -import 'dart:typed_data'; - import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; @@ -13,11 +11,9 @@ part 'library_item_extras.freezed.dart'; /// [book] is the book that the item represents /// [heroTagSuffix] is the suffix to use for the hero tag to avoid conflicts @freezed -class LibraryItemExtras with _$LibraryItemExtras { +sealed class LibraryItemExtras with _$LibraryItemExtras { const factory LibraryItemExtras({ BookMinified? book, @Default('') String heroTagSuffix, - Uint8List? coverImage, }) = _LibraryItemExtras; - } diff --git a/lib/router/models/library_item_extras.freezed.dart b/lib/router/models/library_item_extras.freezed.dart index 00c6385..fe6ae17 100644 --- a/lib/router/models/library_item_extras.freezed.dart +++ b/lib/router/models/library_item_extras.freezed.dart @@ -1,5 +1,5 @@ -// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file // 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 @@ -9,176 +9,260 @@ part of 'library_item_extras.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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 _$LibraryItemExtras { - BookMinified? get book => throw _privateConstructorUsedError; - String get heroTagSuffix => throw _privateConstructorUsedError; - Uint8List? get coverImage => throw _privateConstructorUsedError; - /// Create a copy of LibraryItemExtras - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $LibraryItemExtrasCopyWith get copyWith => - throw _privateConstructorUsedError; + BookMinified? get book; String get heroTagSuffix; +/// Create a copy of LibraryItemExtras +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$LibraryItemExtrasCopyWith get copyWith => _$LibraryItemExtrasCopyWithImpl(this as LibraryItemExtras, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is LibraryItemExtras&&(identical(other.book, book) || other.book == book)&&(identical(other.heroTagSuffix, heroTagSuffix) || other.heroTagSuffix == heroTagSuffix)); +} + + +@override +int get hashCode => Object.hash(runtimeType,book,heroTagSuffix); + +@override +String toString() { + return 'LibraryItemExtras(book: $book, heroTagSuffix: $heroTagSuffix)'; +} + + } /// @nodoc -abstract class $LibraryItemExtrasCopyWith<$Res> { - factory $LibraryItemExtrasCopyWith( - LibraryItemExtras value, $Res Function(LibraryItemExtras) then) = - _$LibraryItemExtrasCopyWithImpl<$Res, LibraryItemExtras>; - @useResult - $Res call({BookMinified? book, String heroTagSuffix, Uint8List? coverImage}); -} +abstract mixin class $LibraryItemExtrasCopyWith<$Res> { + factory $LibraryItemExtrasCopyWith(LibraryItemExtras value, $Res Function(LibraryItemExtras) _then) = _$LibraryItemExtrasCopyWithImpl; +@useResult +$Res call({ + BookMinified? book, String heroTagSuffix +}); + + + +} /// @nodoc -class _$LibraryItemExtrasCopyWithImpl<$Res, $Val extends LibraryItemExtras> +class _$LibraryItemExtrasCopyWithImpl<$Res> implements $LibraryItemExtrasCopyWith<$Res> { - _$LibraryItemExtrasCopyWithImpl(this._value, this._then); + _$LibraryItemExtrasCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final LibraryItemExtras _self; + final $Res Function(LibraryItemExtras) _then; - /// Create a copy of LibraryItemExtras - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? book = freezed, - Object? heroTagSuffix = null, - Object? coverImage = freezed, - }) { - return _then(_value.copyWith( - book: freezed == book - ? _value.book - : book // ignore: cast_nullable_to_non_nullable - as BookMinified?, - heroTagSuffix: null == heroTagSuffix - ? _value.heroTagSuffix - : heroTagSuffix // ignore: cast_nullable_to_non_nullable - as String, - coverImage: freezed == coverImage - ? _value.coverImage - : coverImage // ignore: cast_nullable_to_non_nullable - as Uint8List?, - ) as $Val); - } +/// Create a copy of LibraryItemExtras +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? book = freezed,Object? heroTagSuffix = null,}) { + return _then(_self.copyWith( +book: freezed == book ? _self.book : book // ignore: cast_nullable_to_non_nullable +as BookMinified?,heroTagSuffix: null == heroTagSuffix ? _self.heroTagSuffix : heroTagSuffix // ignore: cast_nullable_to_non_nullable +as String, + )); } -/// @nodoc -abstract class _$$LibraryItemExtrasImplCopyWith<$Res> - implements $LibraryItemExtrasCopyWith<$Res> { - factory _$$LibraryItemExtrasImplCopyWith(_$LibraryItemExtrasImpl value, - $Res Function(_$LibraryItemExtrasImpl) then) = - __$$LibraryItemExtrasImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({BookMinified? book, String heroTagSuffix, Uint8List? coverImage}); } -/// @nodoc -class __$$LibraryItemExtrasImplCopyWithImpl<$Res> - extends _$LibraryItemExtrasCopyWithImpl<$Res, _$LibraryItemExtrasImpl> - implements _$$LibraryItemExtrasImplCopyWith<$Res> { - __$$LibraryItemExtrasImplCopyWithImpl(_$LibraryItemExtrasImpl _value, - $Res Function(_$LibraryItemExtrasImpl) _then) - : super(_value, _then); - /// Create a copy of LibraryItemExtras - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? book = freezed, - Object? heroTagSuffix = null, - Object? coverImage = freezed, - }) { - return _then(_$LibraryItemExtrasImpl( - book: freezed == book - ? _value.book - : book // ignore: cast_nullable_to_non_nullable - as BookMinified?, - heroTagSuffix: null == heroTagSuffix - ? _value.heroTagSuffix - : heroTagSuffix // ignore: cast_nullable_to_non_nullable - as String, - coverImage: freezed == coverImage - ? _value.coverImage - : coverImage // ignore: cast_nullable_to_non_nullable - as Uint8List?, - )); - } +/// Adds pattern-matching-related methods to [LibraryItemExtras]. +extension LibraryItemExtrasPatterns on LibraryItemExtras { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _LibraryItemExtras value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _LibraryItemExtras() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _LibraryItemExtras value) $default,){ +final _that = this; +switch (_that) { +case _LibraryItemExtras(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _LibraryItemExtras value)? $default,){ +final _that = this; +switch (_that) { +case _LibraryItemExtras() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( BookMinified? book, String heroTagSuffix)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _LibraryItemExtras() when $default != null: +return $default(_that.book,_that.heroTagSuffix);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( BookMinified? book, String heroTagSuffix) $default,) {final _that = this; +switch (_that) { +case _LibraryItemExtras(): +return $default(_that.book,_that.heroTagSuffix);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( BookMinified? book, String heroTagSuffix)? $default,) {final _that = this; +switch (_that) { +case _LibraryItemExtras() when $default != null: +return $default(_that.book,_that.heroTagSuffix);case _: + return null; + +} +} + } /// @nodoc -class _$LibraryItemExtrasImpl implements _LibraryItemExtras { - const _$LibraryItemExtrasImpl( - {this.book, this.heroTagSuffix = '', this.coverImage}); - @override - final BookMinified? book; - @override - @JsonKey() - final String heroTagSuffix; - @override - final Uint8List? coverImage; +class _LibraryItemExtras implements LibraryItemExtras { + const _LibraryItemExtras({this.book, this.heroTagSuffix = ''}); + - @override - String toString() { - return 'LibraryItemExtras(book: $book, heroTagSuffix: $heroTagSuffix, coverImage: $coverImage)'; - } +@override final BookMinified? book; +@override@JsonKey() final String heroTagSuffix; - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$LibraryItemExtrasImpl && - (identical(other.book, book) || other.book == book) && - (identical(other.heroTagSuffix, heroTagSuffix) || - other.heroTagSuffix == heroTagSuffix) && - const DeepCollectionEquality() - .equals(other.coverImage, coverImage)); - } +/// Create a copy of LibraryItemExtras +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$LibraryItemExtrasCopyWith<_LibraryItemExtras> get copyWith => __$LibraryItemExtrasCopyWithImpl<_LibraryItemExtras>(this, _$identity); - @override - int get hashCode => Object.hash(runtimeType, book, heroTagSuffix, - const DeepCollectionEquality().hash(coverImage)); - /// Create a copy of LibraryItemExtras - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$LibraryItemExtrasImplCopyWith<_$LibraryItemExtrasImpl> get copyWith => - __$$LibraryItemExtrasImplCopyWithImpl<_$LibraryItemExtrasImpl>( - this, _$identity); + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _LibraryItemExtras&&(identical(other.book, book) || other.book == book)&&(identical(other.heroTagSuffix, heroTagSuffix) || other.heroTagSuffix == heroTagSuffix)); } -abstract class _LibraryItemExtras implements LibraryItemExtras { - const factory _LibraryItemExtras( - {final BookMinified? book, - final String heroTagSuffix, - final Uint8List? coverImage}) = _$LibraryItemExtrasImpl; - @override - BookMinified? get book; - @override - String get heroTagSuffix; - @override - Uint8List? get coverImage; +@override +int get hashCode => Object.hash(runtimeType,book,heroTagSuffix); - /// Create a copy of LibraryItemExtras - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$LibraryItemExtrasImplCopyWith<_$LibraryItemExtrasImpl> get copyWith => - throw _privateConstructorUsedError; +@override +String toString() { + return 'LibraryItemExtras(book: $book, heroTagSuffix: $heroTagSuffix)'; } + + +} + +/// @nodoc +abstract mixin class _$LibraryItemExtrasCopyWith<$Res> implements $LibraryItemExtrasCopyWith<$Res> { + factory _$LibraryItemExtrasCopyWith(_LibraryItemExtras value, $Res Function(_LibraryItemExtras) _then) = __$LibraryItemExtrasCopyWithImpl; +@override @useResult +$Res call({ + BookMinified? book, String heroTagSuffix +}); + + + + +} +/// @nodoc +class __$LibraryItemExtrasCopyWithImpl<$Res> + implements _$LibraryItemExtrasCopyWith<$Res> { + __$LibraryItemExtrasCopyWithImpl(this._self, this._then); + + final _LibraryItemExtras _self; + final $Res Function(_LibraryItemExtras) _then; + +/// Create a copy of LibraryItemExtras +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? book = freezed,Object? heroTagSuffix = null,}) { + return _then(_LibraryItemExtras( +book: freezed == book ? _self.book : book // ignore: cast_nullable_to_non_nullable +as BookMinified?,heroTagSuffix: null == heroTagSuffix ? _self.heroTagSuffix : heroTagSuffix // ignore: cast_nullable_to_non_nullable +as String, + )); +} + + +} + +// dart format on diff --git a/lib/router/router.dart b/lib/router/router.dart index b3085ee..949d0ee 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -5,21 +5,29 @@ import 'package:vaani/features/explore/view/explore_page.dart'; import 'package:vaani/features/explore/view/search_result_page.dart'; import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/library_browser/view/library_browser_page.dart'; +import 'package:vaani/features/logging/view/logs_page.dart'; import 'package:vaani/features/onboarding/view/callback_page.dart'; import 'package:vaani/features/onboarding/view/onboarding_single_page.dart'; import 'package:vaani/features/you/view/server_manager.dart'; import 'package:vaani/features/you/view/you_page.dart'; +import 'package:vaani/main.dart'; import 'package:vaani/pages/home_page.dart'; import 'package:vaani/settings/view/app_settings_page.dart'; import 'package:vaani/settings/view/auto_sleep_timer_settings_page.dart'; +import 'package:vaani/settings/view/notification_settings_page.dart'; +import 'package:vaani/settings/view/player_settings_page.dart'; +import 'package:vaani/settings/view/shake_detector_settings_page.dart'; +import 'package:vaani/settings/view/theme_settings_page.dart'; +import 'package:vaani/settings/view/home_page_settings_page.dart'; import 'scaffold_with_nav_bar.dart'; import 'transitions/slide.dart'; part 'constants.dart'; -final GlobalKey rootNavigatorKey = - GlobalKey(debugLabel: 'root'); +final GlobalKey rootNavigatorKey = GlobalKey( + debugLabel: 'root', +); final GlobalKey sectionHomeNavigatorKey = GlobalKey(debugLabel: 'HomeNavigator'); @@ -28,34 +36,35 @@ class MyAppRouter { const MyAppRouter(); GoRouter get config => GoRouter( - initialLocation: Routes.home.localPath, - debugLogDiagnostics: true, + initialLocation: Routes.home.localPath, + debugLogDiagnostics: true, + routes: [ + // sign in page + GoRoute( + path: Routes.onboarding.localPath, + name: Routes.onboarding.name, + builder: (context, state) => const OnboardingSinglePage(), routes: [ - // sign in page + // open id callback GoRoute( - path: Routes.onboarding.localPath, - name: Routes.onboarding.name, - builder: (context, state) => const OnboardingSinglePage(), - routes: [ - // open id callback - GoRoute( - path: Routes.openIDCallback.pathName, - name: Routes.openIDCallback.name, - pageBuilder: handleCallback, - ), - ], - ), - // callback for open id - // need to duplicate because of https://github.com/flutter/flutter/issues/100624 - GoRoute( - path: Routes.openIDCallback.localPath, - // name: Routes.openIDCallback.name, - // builder: handleCallback, + path: Routes.openIDCallback.pathName, + name: Routes.openIDCallback.name, pageBuilder: handleCallback, ), - // The main app shell - StatefulShellRoute.indexedStack( - builder: ( + ], + ), + // callback for open id + // need to duplicate because of https://github.com/flutter/flutter/issues/100624 + GoRoute( + path: Routes.openIDCallback.localPath, + // name: Routes.openIDCallback.name, + // builder: handleCallback, + pageBuilder: handleCallback, + ), + // The main app shell + StatefulShellRoute.indexedStack( + builder: + ( BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell, @@ -66,146 +75,187 @@ class MyAppRouter { // branches in a stateful way. return ScaffoldWithNavBar(navigationShell: navigationShell); }, - branches: [ - // The route branch for the first tab of the bottom navigation bar. - StatefulShellBranch( - navigatorKey: sectionHomeNavigatorKey, - routes: [ - GoRoute( - path: Routes.home.localPath, - name: Routes.home.name, - // builder: (context, state) => const HomePage(), - pageBuilder: defaultPageBuilder(const HomePage()), - ), - GoRoute( - path: Routes.libraryItem.localPath, - name: Routes.libraryItem.name, - // builder: (context, state) { - // final itemId = state - // .pathParameters[Routes.libraryItem.pathParamName]!; - // return LibraryItemPage( - // itemId: itemId, extra: state.extra); - // }, - pageBuilder: (context, state) { - final itemId = state - .pathParameters[Routes.libraryItem.pathParamName]!; - final child = - LibraryItemPage(itemId: itemId, extra: state.extra); - return buildPageWithDefaultTransition( - context: context, - state: state, - child: child, - ); - }, - ), - // downloads page - GoRoute( - path: Routes.downloads.localPath, - name: Routes.downloads.name, - pageBuilder: defaultPageBuilder(const DownloadsPage()), - ), - ], + branches: [ + // The route branch for the first tab of the bottom navigation bar. + StatefulShellBranch( + navigatorKey: sectionHomeNavigatorKey, + routes: [ + GoRoute( + path: Routes.home.localPath, + name: Routes.home.name, + // builder: (context, state) => const HomePage(), + pageBuilder: defaultPageBuilder(const HomePage()), ), + GoRoute( + path: Routes.libraryItem.localPath, + name: Routes.libraryItem.name, + // builder: (context, state) { + // final itemId = state + // .pathParameters[Routes.libraryItem.pathParamName]!; + // return LibraryItemPage( + // itemId: itemId, extra: state.extra); + // }, + pageBuilder: (context, state) { + final itemId = + state.pathParameters[Routes.libraryItem.pathParamName]!; + final child = LibraryItemPage( + itemId: itemId, + extra: state.extra, + ); + return buildPageWithDefaultTransition( + context: context, + state: state, + child: child, + ); + }, + ), + // downloads page + GoRoute( + path: Routes.downloads.localPath, + name: Routes.downloads.name, + pageBuilder: defaultPageBuilder(const DownloadsPage()), + ), + ], + ), - // Library page - StatefulShellBranch( - routes: [ - GoRoute( - path: Routes.libraryBrowser.localPath, - name: Routes.libraryBrowser.name, - pageBuilder: defaultPageBuilder(const LibraryBrowserPage()), - ), - ], + // Library page + StatefulShellBranch( + routes: [ + GoRoute( + path: Routes.libraryBrowser.localPath, + name: Routes.libraryBrowser.name, + pageBuilder: defaultPageBuilder(const LibraryBrowserPage()), ), - // search/explore page - StatefulShellBranch( - routes: [ - GoRoute( - path: Routes.explore.localPath, - name: Routes.explore.name, - // builder: (context, state) => const ExplorePage(), - pageBuilder: defaultPageBuilder(const ExplorePage()), - ), - // search page - GoRoute( - path: Routes.search.localPath, - name: Routes.search.name, - // builder: (context, state) { - // final libraryId = state - // .pathParameters[Routes.library.pathParamName]!; - // return LibrarySearchPage( - // libraryId: libraryId, - // extra: state.extra, - // ); - // }, - pageBuilder: (context, state) { - final queryParam = state.uri.queryParameters['q']!; - final category = state.uri.queryParameters['category']; - final child = SearchResultPage( - extra: state.extra, - query: queryParam, - category: category != null - ? SearchResultCategory.values.firstWhere( - (e) => e.toString().split('.').last == category, - ) - : null, - ); - return buildPageWithDefaultTransition( - context: context, - state: state, - child: child, - ); - }, - ), - ], + ], + ), + // search/explore page + StatefulShellBranch( + routes: [ + GoRoute( + path: Routes.explore.localPath, + name: Routes.explore.name, + // builder: (context, state) => const ExplorePage(), + pageBuilder: defaultPageBuilder(const ExplorePage()), ), - // you page - StatefulShellBranch( - routes: [ + // search page + GoRoute( + path: Routes.search.localPath, + name: Routes.search.name, + // builder: (context, state) { + // final libraryId = state + // .pathParameters[Routes.library.pathParamName]!; + // return LibrarySearchPage( + // libraryId: libraryId, + // extra: state.extra, + // ); + // }, + pageBuilder: (context, state) { + final queryParam = state.uri.queryParameters['q']!; + final category = state.uri.queryParameters['category']; + final child = SearchResultPage( + extra: state.extra, + query: queryParam, + category: category != null + ? SearchResultCategory.values.firstWhere( + (e) => e.toString().split('.').last == category, + ) + : null, + ); + return buildPageWithDefaultTransition( + context: context, + state: state, + child: child, + ); + }, + ), + ], + ), + // you page + StatefulShellBranch( + routes: [ + GoRoute( + path: Routes.you.localPath, + name: Routes.you.name, + pageBuilder: defaultPageBuilder(const YouPage()), + ), + GoRoute( + path: Routes.settings.localPath, + name: Routes.settings.name, + // builder: (context, state) => const AppSettingsPage(), + pageBuilder: defaultPageBuilder(const AppSettingsPage()), + routes: [ GoRoute( - path: Routes.you.localPath, - name: Routes.you.name, - pageBuilder: defaultPageBuilder(const YouPage()), + path: Routes.themeSettings.pathName, + name: Routes.themeSettings.name, + pageBuilder: defaultPageBuilder(const ThemeSettingsPage()), ), GoRoute( - path: Routes.settings.localPath, - name: Routes.settings.name, - // builder: (context, state) => const AppSettingsPage(), - pageBuilder: defaultPageBuilder(const AppSettingsPage()), - ), - GoRoute( - path: Routes.autoSleepTimerSettings.localPath, + path: Routes.autoSleepTimerSettings.pathName, name: Routes.autoSleepTimerSettings.name, - // builder: (context, state) => - // const AutoSleepTimerSettingsPage(), pageBuilder: defaultPageBuilder( const AutoSleepTimerSettingsPage(), ), ), GoRoute( - path: Routes.userManagement.localPath, - name: Routes.userManagement.name, - // builder: (context, state) => const UserManagementPage(), - pageBuilder: defaultPageBuilder(const ServerManagerPage()), + path: Routes.notificationSettings.pathName, + name: Routes.notificationSettings.name, + pageBuilder: defaultPageBuilder( + const NotificationSettingsPage(), + ), + ), + GoRoute( + path: Routes.playerSettings.pathName, + name: Routes.playerSettings.name, + pageBuilder: defaultPageBuilder(const PlayerSettingsPage()), + ), + GoRoute( + path: Routes.shakeDetectorSettings.pathName, + name: Routes.shakeDetectorSettings.name, + pageBuilder: defaultPageBuilder( + const ShakeDetectorSettingsPage(), + ), + ), + GoRoute( + path: Routes.homePageSettings.pathName, + name: Routes.homePageSettings.name, + pageBuilder: defaultPageBuilder( + const HomePageSettingsPage(), + ), ), ], ), + GoRoute( + path: Routes.userManagement.localPath, + name: Routes.userManagement.name, + // builder: (context, state) => const UserManagementPage(), + pageBuilder: defaultPageBuilder(const ServerManagerPage()), + ), ], ), ], - ); + ), - Page handleCallback( - BuildContext context, - GoRouterState state, - ) { + // loggers page + GoRoute( + path: Routes.logs.localPath, + name: Routes.logs.name, + // builder: (context, state) => const LogsPage(), + pageBuilder: defaultPageBuilder(const LogsPage()), + ), + ], + ); + + Page handleCallback(BuildContext context, GoRouterState state) { // extract the code and state from the uri final code = state.uri.queryParameters['code']; final stateParam = state.uri.queryParameters['state']; - debugPrint('deep linking callback: code: $code, state: $stateParam'); + appLogger.fine('deep linking callback: code: $code, state: $stateParam'); - var callbackPage = - CallbackPage(code: code, state: stateParam, key: ValueKey(stateParam)); + var callbackPage = CallbackPage( + code: code, + state: stateParam, + key: ValueKey(stateParam), + ); return buildPageWithDefaultTransition( context: context, state: state, diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index 0062919..5f0b026 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -2,24 +2,29 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:miniplayer/miniplayer.dart'; +import 'package:vaani/api/library_provider.dart' show currentLibraryProvider; import 'package:vaani/features/explore/providers/search_controller.dart'; import 'package:vaani/features/player/providers/player_form.dart'; import 'package:vaani/features/player/view/audiobook_player.dart'; import 'package:vaani/features/player/view/player_when_expanded.dart'; +import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'; +import 'package:vaani/main.dart'; +import 'package:vaani/router/router.dart'; +import 'package:vaani/shared/icons/abs_icons.dart' show AbsIcons; // stack to track changes in navigationShell.currentIndex // home is always at index 0 and at the start and should be the last before popping // if stack is empty, push home, if already contains home, pop it final Set navigationShellStack = {}; +const bottomBarHeight = 64; + /// Builds the "shell" for the app by building a Scaffold with a /// BottomNavigationBar, where [child] is placed in the body of the Scaffold. class ScaffoldWithNavBar extends HookConsumerWidget { /// Constructs an [ScaffoldWithNavBar]. - const ScaffoldWithNavBar({ - required this.navigationShell, - Key? key, - }) : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); + const ScaffoldWithNavBar({required this.navigationShell, Key? key}) + : super(key: key ?? const ValueKey('ScaffoldWithNavBar')); /// The navigation shell and container for the branch Navigators. final StatefulNavigationShell navigationShell; @@ -28,26 +33,25 @@ class ScaffoldWithNavBar extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { // playerExpandProgress is used to animate bottom navigation bar to opacity 0 and slide down when player is expanded // final playerProgress = - // useValueListenable(ref.watch(playerExpandProgressNotifierProvider)); + // useValueListenable(ref.watch(playerExpandProgressProvider)); final playerProgress = ref.watch(playerHeightProvider); final playerMaxHeight = MediaQuery.of(context).size.height; - var percentExpanded = (playerProgress - playerMinHeight) / + var percentExpandedMiniPlayer = + (playerProgress - playerMinHeight) / (playerMaxHeight - playerMinHeight); // Clamp the value between 0 and 1 - percentExpanded = percentExpanded.clamp(0.0, 1.0); + percentExpandedMiniPlayer = percentExpandedMiniPlayer.clamp(0.0, 1.0); onBackButtonPressed() async { final isPlayerExpanded = playerProgress != playerMinHeight; - debugPrint( + appLogger.fine( 'BackButtonListener: Back button pressed, isPlayerExpanded: $isPlayerExpanded, stack: $navigationShellStack, pendingPlayerModals: $pendingPlayerModals', ); // close miniplayer if it is open if (isPlayerExpanded && pendingPlayerModals == 0) { - debugPrint( - 'BackButtonListener: closing the player', - ); + appLogger.fine('BackButtonListener: closing the player'); audioBookMiniplayerController.animateToHeight(state: PanelState.MIN); return true; } @@ -56,7 +60,7 @@ class ScaffoldWithNavBar extends HookConsumerWidget { final canPop = GoRouter.of(context).canPop(); if (canPop) { - debugPrint( + appLogger.fine( 'BackButtonListener: passing it to the router as canPop is true', ); return false; @@ -66,7 +70,7 @@ class ScaffoldWithNavBar extends HookConsumerWidget { // pop the last index from the stack and navigate to it final index = navigationShellStack.last; navigationShellStack.remove(index); - debugPrint('BackButtonListener: popping the stack, index: $index'); + appLogger.fine('BackButtonListener: popping the stack, index: $index'); // if the stack is empty, navigate to home else navigate to the last index if (navigationShellStack.isNotEmpty) { @@ -76,12 +80,12 @@ class ScaffoldWithNavBar extends HookConsumerWidget { } if (navigationShell.currentIndex != 0) { // if the stack is empty and the current branch is not home, navigate to home - debugPrint('BackButtonListener: navigating to home'); + appLogger.fine('BackButtonListener: navigating to home'); navigationShell.goBranch(0); return true; } - debugPrint('BackButtonListener: passing it to the router'); + appLogger.fine('BackButtonListener: passing it to the router'); return false; } @@ -89,49 +93,51 @@ class ScaffoldWithNavBar extends HookConsumerWidget { return BackButtonListener( onBackButtonPressed: onBackButtonPressed, child: Scaffold( - body: Stack( - children: [ - navigationShell, - const AudiobookPlayer(), - ], - ), + body: Stack(children: [navigationShell, const AudiobookPlayer()]), bottomNavigationBar: Opacity( // Opacity is interpolated from 1 to 0 when player is expanded - opacity: 1 - percentExpanded, - child: SizedBox( - // height is interpolated from 0 to 56 when player is expanded - height: 56 * (1 - percentExpanded), + opacity: 1 - percentExpandedMiniPlayer, + child: NavigationBar( + elevation: 0.0, + height: bottomBarHeight * (1 - percentExpandedMiniPlayer), - child: BottomNavigationBar( - elevation: 0.0, - landscapeLayout: BottomNavigationBarLandscapeLayout.centered, - selectedFontSize: - Theme.of(context).textTheme.labelMedium!.fontSize!, - unselectedFontSize: - Theme.of(context).textTheme.labelMedium!.fontSize!, - showUnselectedLabels: false, - fixedColor: Theme.of(context).colorScheme.onSurface, - enableFeedback: true, - type: BottomNavigationBarType.fixed, - - // Here, the items of BottomNavigationBar are hard coded. In a real - // world scenario, the items would most likely be generated from the - // branches of the shell route, which can be fetched using - // `navigationShell.route.branches`. - items: _navigationItems - .map( - (item) => BottomNavigationBarItem( - icon: Icon(item.icon), - activeIcon: item.activeIcon != null - ? Icon(item.activeIcon) - : Icon(item.icon), - label: item.name, - ), - ) - .toList(), - currentIndex: navigationShell.currentIndex, - onTap: (int index) => _onTap(context, index, ref), - ), + // TODO: get destinations from the navigationShell + // Here, the items of BottomNavigationBar are hard coded. In a real + // world scenario, the items would most likely be generated from the + // branches of the shell route, which can be fetched using + // `navigationShell.route.branches`. + destinations: _navigationItems.map((item) { + final isDestinationLibrary = item.name == 'Library'; + var currentLibrary = ref.watch(currentLibraryProvider).value; + final libraryIcon = AbsIcons.getIconByName(currentLibrary?.icon); + final destinationWidget = NavigationDestination( + icon: Icon( + isDestinationLibrary ? libraryIcon ?? item.icon : item.icon, + ), + selectedIcon: Icon( + isDestinationLibrary + ? libraryIcon ?? item.activeIcon + : item.activeIcon, + ), + label: isDestinationLibrary + ? currentLibrary?.name ?? item.name + : item.name, + tooltip: item.tooltip, + ); + if (isDestinationLibrary) { + return GestureDetector( + onSecondaryTap: () => showLibrarySwitcher(context, ref), + onDoubleTap: () => showLibrarySwitcher(context, ref), + child: + destinationWidget, // Wrap the actual NavigationDestination + ); + } else { + // Return the unwrapped destination for other items + return destinationWidget; + } + }).toList(), + selectedIndex: navigationShell.currentIndex, + onDestinationSelected: (int index) => _onTap(context, index, ref), ), ), ), @@ -158,11 +164,12 @@ class ScaffoldWithNavBar extends HookConsumerWidget { navigationShellStack.remove(index); } navigationShellStack.add(index); - debugPrint('Tapped index: $index, stack: $navigationShellStack'); + appLogger.fine('Tapped index: $index, stack: $navigationShellStack'); // Check if the current branch is the same as the branch that was tapped. - // If it is, debugPrint a message to the console. if (index == navigationShell.currentIndex) { + appLogger.fine('Tapped the current branch'); + // if current branch is explore, open the search view if (index == 2) { final searchController = ref.read(globalSearchControllerProvider); @@ -173,7 +180,14 @@ class ScaffoldWithNavBar extends HookConsumerWidget { searchController.closeView(null); } } - debugPrint('Tapped the current branch'); + + // open settings page if the current branch is you + if (index == 3) { + // do not open settings page if it is already open + if (GoRouterState.of(context).topRoute?.name != Routes.settings.name) { + GoRouter.of(context).pushNamed(Routes.settings.name); + } + } } } } @@ -191,16 +205,19 @@ const _navigationItems = [ name: 'Library', icon: Icons.book_outlined, activeIcon: Icons.book, + tooltip: 'Browse your library', ), _NavigationItem( name: 'Explore', icon: Icons.search_outlined, activeIcon: Icons.search, + tooltip: 'Search and Explore', ), _NavigationItem( name: 'You', icon: Icons.account_circle_outlined, activeIcon: Icons.account_circle, + tooltip: 'Your Profile and Settings', ), ]; @@ -208,10 +225,12 @@ class _NavigationItem { const _NavigationItem({ required this.name, required this.icon, - this.activeIcon, + required this.activeIcon, + this.tooltip, }); final String name; final IconData icon; - final IconData? activeIcon; + final IconData activeIcon; + final String? tooltip; } diff --git a/lib/router/transitions/slide.dart b/lib/router/transitions/slide.dart index 969edea..79c6364 100644 --- a/lib/router/transitions/slide.dart +++ b/lib/router/transitions/slide.dart @@ -33,29 +33,26 @@ CustomTransitionPage buildPageWithDefaultTransition({ child: child, transitionsBuilder: (context, animation, secondaryAnimation, child) => FadeTransition( - opacity: animation, - child: SlideTransition( - position: animation.drive( - Tween( - begin: const Offset(0, 1.50), - end: Offset.zero, - ).chain( - CurveTween(curve: Curves.easeOut), + opacity: animation, + child: SlideTransition( + position: animation.drive( + Tween( + begin: const Offset(0, 1.50), + end: Offset.zero, + ).chain(CurveTween(curve: Curves.easeOut)), + ), + child: child, ), ), - child: child, - ), - ), ); } Page Function(BuildContext, GoRouterState) defaultPageBuilder( Widget child, -) => - (BuildContext context, GoRouterState state) { - return buildPageWithDefaultTransition( - context: context, - state: state, - child: child, - ); - }; +) => (BuildContext context, GoRouterState state) { + return buildPageWithDefaultTransition( + context: context, + state: state, + child: child, + ); +}; diff --git a/lib/settings/api_settings_provider.dart b/lib/settings/api_settings_provider.dart index ecd297e..132ac7b 100644 --- a/lib/settings/api_settings_provider.dart +++ b/lib/settings/api_settings_provider.dart @@ -4,6 +4,7 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:vaani/db/available_boxes.dart'; import 'package:vaani/settings/models/api_settings.dart' as model; +import 'package:vaani/shared/extensions/obfuscation.dart'; part 'api_settings_provider.g.dart'; @@ -16,9 +17,10 @@ class ApiSettings extends _$ApiSettings { @override model.ApiSettings build() { state = readFromBoxOrCreate(); - ref.listenSelf((_, __) { + listenSelf((_, __) { writeToBox(); }); + return state; } @@ -33,7 +35,7 @@ class ApiSettings extends _$ApiSettings { activeServer: foundSettings.activeUser?.server, ); } - _logger.fine('found api settings in box: $foundSettings'); + _logger.fine('found api settings in box: ${foundSettings.obfuscate()}'); return foundSettings; } else { // create a new settings object @@ -47,7 +49,7 @@ class ApiSettings extends _$ApiSettings { void writeToBox() { _box.clear(); _box.add(state); - _logger.fine('wrote api settings to box: $state'); + _logger.fine('wrote api settings to box: ${state.obfuscate()}'); } void updateState(model.ApiSettings newSettings, {bool force = false}) { diff --git a/lib/settings/api_settings_provider.g.dart b/lib/settings/api_settings_provider.g.dart index 0bfacbf..752dfcc 100644 --- a/lib/settings/api_settings_provider.g.dart +++ b/lib/settings/api_settings_provider.g.dart @@ -6,20 +6,57 @@ part of 'api_settings_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$apiSettingsHash() => r'26e7e09e7369bac9fbf0589da9fd97d1f15b7926'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning -/// See also [ApiSettings]. @ProviderFor(ApiSettings) -final apiSettingsProvider = - NotifierProvider.internal( - ApiSettings.new, - name: r'apiSettingsProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$apiSettingsHash, - dependencies: null, - allTransitiveDependencies: null, -); +final apiSettingsProvider = ApiSettingsProvider._(); -typedef _$ApiSettings = 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 +final class ApiSettingsProvider + extends $NotifierProvider { + ApiSettingsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'apiSettingsProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$apiSettingsHash(); + + @$internal + @override + ApiSettings create() => ApiSettings(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(model.ApiSettings value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$apiSettingsHash() => r'02af850985338eade33d76fc9965808bed548290'; + +abstract class _$ApiSettings extends $Notifier { + model.ApiSettings build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + model.ApiSettings, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/settings/app_settings_provider.dart b/lib/settings/app_settings_provider.dart index b23d18d..ae843c3 100644 --- a/lib/settings/app_settings_provider.dart +++ b/lib/settings/app_settings_provider.dart @@ -11,26 +11,32 @@ final _box = AvailableHiveBoxes.userPrefsBox; final _logger = Logger('AppSettingsProvider'); -model.AppSettings readFromBoxOrCreate() { +model.AppSettings loadOrCreateAppSettings() { // see if the settings are already in the box + model.AppSettings? settings; if (_box.isNotEmpty) { - final foundSettings = _box.getAt(0); - _logger.fine('found settings in box: $foundSettings'); - return foundSettings; + try { + settings = _box.getAt(0); + _logger.fine('found settings in box: $settings'); + } catch (e) { + _logger.warning( + 'error reading settings from box: $e' + '\nclearing box', + ); + _box.clear(); + } } else { - // create a new settings object - const settings = model.AppSettings(); - _logger.fine('created new settings: $settings'); - return settings; + _logger.fine('no settings found in box, creating new settings'); } + return settings ?? const model.AppSettings(); } @Riverpod(keepAlive: true) class AppSettings extends _$AppSettings { @override model.AppSettings build() { - state = readFromBoxOrCreate(); - ref.listenSelf((_, __) { + state = loadOrCreateAppSettings(); + listenSelf((_, __) { writeToBox(); }); return state; @@ -43,11 +49,6 @@ class AppSettings extends _$AppSettings { _logger.fine('wrote settings to box: $state'); } - void toggleDarkMode() { - state = state.copyWith - .themeSettings(isDarkMode: !state.themeSettings.isDarkMode); - } - void update(model.AppSettings newSettings) { state = newSettings; } @@ -56,3 +57,19 @@ class AppSettings extends _$AppSettings { state = const model.AppSettings(); } } + +// SleepTimerSettings provider but only rebuilds when the sleep timer settings change +@Riverpod(keepAlive: true) +class SleepTimerSettings extends _$SleepTimerSettings { + @override + model.SleepTimerSettings build() { + final settings = ref.read(appSettingsProvider).sleepTimerSettings; + state = settings; + ref.listen(appSettingsProvider, (a, b) { + if (a?.sleepTimerSettings != b.sleepTimerSettings) { + state = b.sleepTimerSettings; + } + }); + return state; + } +} diff --git a/lib/settings/app_settings_provider.g.dart b/lib/settings/app_settings_provider.g.dart index 4956783..8cb4a0f 100644 --- a/lib/settings/app_settings_provider.g.dart +++ b/lib/settings/app_settings_provider.g.dart @@ -6,20 +6,112 @@ part of 'app_settings_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$appSettingsHash() => r'e0e132b782b97f11d9791d4f1e45bf4ee67dd99b'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning -/// See also [AppSettings]. @ProviderFor(AppSettings) -final appSettingsProvider = - NotifierProvider.internal( - AppSettings.new, - name: r'appSettingsProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$appSettingsHash, - dependencies: null, - allTransitiveDependencies: null, -); +final appSettingsProvider = AppSettingsProvider._(); -typedef _$AppSettings = 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 +final class AppSettingsProvider + extends $NotifierProvider { + AppSettingsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'appSettingsProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$appSettingsHash(); + + @$internal + @override + AppSettings create() => AppSettings(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(model.AppSettings value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$appSettingsHash() => r'744d7e0157eb3b089c4187b35b845fc78547a44e'; + +abstract class _$AppSettings extends $Notifier { + model.AppSettings build(); + @$mustCallSuper + @override + void runBuild() { + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + model.AppSettings, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} + +@ProviderFor(SleepTimerSettings) +final sleepTimerSettingsProvider = SleepTimerSettingsProvider._(); + +final class SleepTimerSettingsProvider + extends $NotifierProvider { + SleepTimerSettingsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'sleepTimerSettingsProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$sleepTimerSettingsHash(); + + @$internal + @override + SleepTimerSettings create() => SleepTimerSettings(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(model.SleepTimerSettings value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$sleepTimerSettingsHash() => + r'85bb3d3fb292b9a3a5b771d86e5fc57718519c69'; + +abstract class _$SleepTimerSettings + extends $Notifier { + model.SleepTimerSettings build(); + @$mustCallSuper + @override + void runBuild() { + final ref = + this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + model.SleepTimerSettings, + Object?, + Object? + >; + element.handleCreate(ref, build); + } +} diff --git a/lib/settings/constants.dart b/lib/settings/constants.dart index 2429545..3b836d3 100644 --- a/lib/settings/constants.dart +++ b/lib/settings/constants.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart' show immutable; - @immutable class AppMetadata { const AppMetadata._(); @@ -10,5 +9,10 @@ class AppMetadata { // for deeplinking static const String appScheme = 'vaani'; + static const version = '1.0.0'; + static const author = 'Dr.Blank'; + + static Uri githubRepo = Uri.parse('https://github.com/Dr-Blank/Vaani'); + static get appNameLowerCase => appName.toLowerCase().replaceAll(' ', '_'); } diff --git a/lib/settings/metadata/metadata_provider.dart b/lib/settings/metadata/metadata_provider.dart index 053a67b..a1ce12f 100644 --- a/lib/settings/metadata/metadata_provider.dart +++ b/lib/settings/metadata/metadata_provider.dart @@ -1,92 +1,93 @@ import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'metadata_provider.g.dart'; @Riverpod(keepAlive: true) -Future deviceName(DeviceNameRef ref) async { +Future deviceName(Ref ref) async { final data = await _getDeviceData(DeviceInfoPlugin()); // try different keys to get the device name return - // android - data['product'] ?? - // ios - data['name'] ?? - // linux - data['name'] ?? - // windows - data['computerName'] ?? - // macos - data['model'] ?? - // web - data['browserName'] ?? - 'Unknown name'; + // android + data['product'] ?? + // ios + data['name'] ?? + // linux + data['name'] ?? + // windows + data['computerName'] ?? + // macos + data['model'] ?? + // web + data['browserName'] ?? + 'Unknown name'; } @Riverpod(keepAlive: true) -Future deviceModel(DeviceModelRef ref) async { +Future deviceModel(Ref ref) async { final data = await _getDeviceData(DeviceInfoPlugin()); // try different keys to get the device model return - // android, eg: Google Pixel 4 + // android, eg: Google Pixel 4 + data['model'] ?? + // ios, eg: iPhone 12 Pro + data['name'] ?? + // linux, eg: Linux Mint 20.1 + data['name'] ?? + // windows, eg: Surface Pro 7 + data['productId'] ?? + // macos, eg: MacBook Pro (13-inch, M1, 2020) data['model'] ?? - // ios, eg: iPhone 12 Pro - data['name'] ?? - // linux, eg: Linux Mint 20.1 - data['name'] ?? - // windows, eg: Surface Pro 7 - data['productId'] ?? - // macos, eg: MacBook Pro (13-inch, M1, 2020) - data['model'] ?? - // web, eg: Chrome 87.0.4280.88 - data['browserName'] ?? - 'Unknown model'; + // web, eg: Chrome 87.0.4280.88 + data['browserName'] ?? + 'Unknown model'; } @Riverpod(keepAlive: true) -Future deviceSdkVersion(DeviceSdkVersionRef ref) async { +Future deviceSdkVersion(Ref ref) async { final data = await _getDeviceData(DeviceInfoPlugin()); // try different keys to get the device sdk version return - // android, eg: 30 - data['version.sdkInt']?.toString() ?? - // ios, eg: 14.4 - data['systemVersion'] ?? - // linux, eg: 5.4.0-66-generic - data['version'] ?? - // windows, eg: 10.0.19042 - data['displayVersion'] ?? - // macos, eg: 11.2.1 - data['osRelease'] ?? - // web, eg: 87.0.4280.88 - data['appVersion'] ?? - 'Unknown sdk version'; + // android, eg: 30 + data['version.sdkInt']?.toString() ?? + // ios, eg: 14.4 + data['systemVersion'] ?? + // linux, eg: 5.4.0-66-generic + data['version'] ?? + // windows, eg: 10.0.19042 + data['displayVersion'] ?? + // macos, eg: 11.2.1 + data['osRelease'] ?? + // web, eg: 87.0.4280.88 + data['appVersion'] ?? + 'Unknown sdk version'; } @Riverpod(keepAlive: true) -Future deviceManufacturer(DeviceManufacturerRef ref) async { +Future deviceManufacturer(Ref ref) async { final data = await _getDeviceData(DeviceInfoPlugin()); // try different keys to get the device manufacturer return - // android, eg: Google + // android, eg: Google + data['manufacturer'] ?? + // ios, eg: Apple data['manufacturer'] ?? - // ios, eg: Apple - data['manufacturer'] ?? - // linux, eg: Linux - data['idLike'] ?? - // windows, eg: Microsoft - data['productName'] ?? - // macos, eg: Apple - data['manufacturer'] ?? - // web, eg: Google Inc. - data['vendor'] ?? - 'Unknown manufacturer'; + // linux, eg: Linux + data['idLike'] ?? + // windows, eg: Microsoft + data['productName'] ?? + // macos, eg: Apple + data['manufacturer'] ?? + // web, eg: Google Inc. + data['vendor'] ?? + 'Unknown manufacturer'; } // copied from https://pub.dev/packages/device_info_plus/example @@ -233,25 +234,28 @@ Future> _getDeviceData( deviceData = _readWebBrowserInfo(await deviceInfoPlugin.webBrowserInfo); } else { deviceData = switch (defaultTargetPlatform) { - TargetPlatform.android => - _readAndroidBuildData(await deviceInfoPlugin.androidInfo), - TargetPlatform.iOS => - _readIosDeviceInfo(await deviceInfoPlugin.iosInfo), - TargetPlatform.linux => - _readLinuxDeviceInfo(await deviceInfoPlugin.linuxInfo), - TargetPlatform.windows => - _readWindowsDeviceInfo(await deviceInfoPlugin.windowsInfo), - TargetPlatform.macOS => - _readMacOsDeviceInfo(await deviceInfoPlugin.macOsInfo), + TargetPlatform.android => _readAndroidBuildData( + await deviceInfoPlugin.androidInfo, + ), + TargetPlatform.iOS => _readIosDeviceInfo( + await deviceInfoPlugin.iosInfo, + ), + TargetPlatform.linux => _readLinuxDeviceInfo( + await deviceInfoPlugin.linuxInfo, + ), + TargetPlatform.windows => _readWindowsDeviceInfo( + await deviceInfoPlugin.windowsInfo, + ), + TargetPlatform.macOS => _readMacOsDeviceInfo( + await deviceInfoPlugin.macOsInfo, + ), TargetPlatform.fuchsia => { - errorKey: 'Fuchsia platform isn\'t supported', - }, + errorKey: 'Fuchsia platform isn\'t supported', + }, }; } } on PlatformException { - deviceData = { - errorKey: 'Failed to get platform version.', - }; + deviceData = {errorKey: 'Failed to get platform version.'}; } return deviceData; } diff --git a/lib/settings/metadata/metadata_provider.g.dart b/lib/settings/metadata/metadata_provider.g.dart index 858c351..50c6f1f 100644 --- a/lib/settings/metadata/metadata_provider.g.dart +++ b/lib/settings/metadata/metadata_provider.g.dart @@ -6,64 +6,138 @@ part of 'metadata_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$deviceNameHash() => r'bc206a3a8c14f3da6e257e92e1ccdc79364f4e28'; +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning -/// See also [deviceName]. @ProviderFor(deviceName) -final deviceNameProvider = FutureProvider.internal( - deviceName, - name: r'deviceNameProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$deviceNameHash, - dependencies: null, - allTransitiveDependencies: null, -); +final deviceNameProvider = DeviceNameProvider._(); -typedef DeviceNameRef = FutureProviderRef; -String _$deviceModelHash() => r'3d7e8ef4a37b90f98e38dc8d5f16ca30f71e15b2'; +final class DeviceNameProvider + extends $FunctionalProvider, String, FutureOr> + with $FutureModifier, $FutureProvider { + DeviceNameProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'deviceNameProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$deviceNameHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return deviceName(ref); + } +} + +String _$deviceNameHash() => r'9e38adda74e70a91851a682f05228bd759356dcc'; -/// See also [deviceModel]. @ProviderFor(deviceModel) -final deviceModelProvider = FutureProvider.internal( - deviceModel, - name: r'deviceModelProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$deviceModelHash, - dependencies: null, - allTransitiveDependencies: null, -); +final deviceModelProvider = DeviceModelProvider._(); -typedef DeviceModelRef = FutureProviderRef; -String _$deviceSdkVersionHash() => r'501b01ae679e02fc5082feabea81cea0fa74afd7'; +final class DeviceModelProvider + extends $FunctionalProvider, String, FutureOr> + with $FutureModifier, $FutureProvider { + DeviceModelProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'deviceModelProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$deviceModelHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return deviceModel(ref); + } +} + +String _$deviceModelHash() => r'922b13d9e35b5b5c5b8e96f2f2c2ae594f4f41f2'; -/// See also [deviceSdkVersion]. @ProviderFor(deviceSdkVersion) -final deviceSdkVersionProvider = FutureProvider.internal( - deviceSdkVersion, - name: r'deviceSdkVersionProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$deviceSdkVersionHash, - dependencies: null, - allTransitiveDependencies: null, -); +final deviceSdkVersionProvider = DeviceSdkVersionProvider._(); -typedef DeviceSdkVersionRef = FutureProviderRef; -String _$deviceManufacturerHash() => - r'f0a57e6a92b551fbe266d0a6a29d35dc497882a9'; +final class DeviceSdkVersionProvider + extends $FunctionalProvider, String, FutureOr> + with $FutureModifier, $FutureProvider { + DeviceSdkVersionProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'deviceSdkVersionProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$deviceSdkVersionHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return deviceSdkVersion(ref); + } +} + +String _$deviceSdkVersionHash() => r'33178d80590808d1f4cca2be8a3b52c6f6724cac'; -/// See also [deviceManufacturer]. @ProviderFor(deviceManufacturer) -final deviceManufacturerProvider = FutureProvider.internal( - deviceManufacturer, - name: r'deviceManufacturerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$deviceManufacturerHash, - dependencies: null, - allTransitiveDependencies: null, -); +final deviceManufacturerProvider = DeviceManufacturerProvider._(); -typedef DeviceManufacturerRef = FutureProviderRef; -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +final class DeviceManufacturerProvider + extends $FunctionalProvider, String, FutureOr> + with $FutureModifier, $FutureProvider { + DeviceManufacturerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'deviceManufacturerProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$deviceManufacturerHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return deviceManufacturer(ref); + } +} + +String _$deviceManufacturerHash() => + r'39250767deb8635fa7c7e18bae23576b9b863e04'; diff --git a/lib/settings/models/api_settings.dart b/lib/settings/models/api_settings.dart index 6410938..dd7aada 100644 --- a/lib/settings/models/api_settings.dart +++ b/lib/settings/models/api_settings.dart @@ -11,7 +11,7 @@ part 'api_settings.g.dart'; /// /// all settings that are needed to interact with the server are stored here @freezed -class ApiSettings with _$ApiSettings { +sealed class ApiSettings with _$ApiSettings { const factory ApiSettings({ AudiobookShelfServer? activeServer, AuthenticatedUser? activeUser, diff --git a/lib/settings/models/api_settings.freezed.dart b/lib/settings/models/api_settings.freezed.dart index 2f3c6e5..6669a2b 100644 --- a/lib/settings/models/api_settings.freezed.dart +++ b/lib/settings/models/api_settings.freezed.dart @@ -1,5 +1,5 @@ -// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file // 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 @@ -9,238 +9,317 @@ part of 'api_settings.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -ApiSettings _$ApiSettingsFromJson(Map json) { - return _ApiSettings.fromJson(json); -} - /// @nodoc mixin _$ApiSettings { - AudiobookShelfServer? get activeServer => throw _privateConstructorUsedError; - AuthenticatedUser? get activeUser => throw _privateConstructorUsedError; - String? get activeLibraryId => throw _privateConstructorUsedError; + + AudiobookShelfServer? get activeServer; AuthenticatedUser? get activeUser; String? get activeLibraryId; +/// Create a copy of ApiSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ApiSettingsCopyWith get copyWith => _$ApiSettingsCopyWithImpl(this as ApiSettings, _$identity); /// Serializes this ApiSettings to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ApiSettings&&(identical(other.activeServer, activeServer) || other.activeServer == activeServer)&&(identical(other.activeUser, activeUser) || other.activeUser == activeUser)&&(identical(other.activeLibraryId, activeLibraryId) || other.activeLibraryId == activeLibraryId)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,activeServer,activeUser,activeLibraryId); + +@override +String toString() { + return 'ApiSettings(activeServer: $activeServer, activeUser: $activeUser, activeLibraryId: $activeLibraryId)'; +} + - /// Create a copy of ApiSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $ApiSettingsCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $ApiSettingsCopyWith<$Res> { - factory $ApiSettingsCopyWith( - ApiSettings value, $Res Function(ApiSettings) then) = - _$ApiSettingsCopyWithImpl<$Res, ApiSettings>; - @useResult - $Res call( - {AudiobookShelfServer? activeServer, - AuthenticatedUser? activeUser, - String? activeLibraryId}); +abstract mixin class $ApiSettingsCopyWith<$Res> { + factory $ApiSettingsCopyWith(ApiSettings value, $Res Function(ApiSettings) _then) = _$ApiSettingsCopyWithImpl; +@useResult +$Res call({ + AudiobookShelfServer? activeServer, AuthenticatedUser? activeUser, String? activeLibraryId +}); + + +$AudiobookShelfServerCopyWith<$Res>? get activeServer;$AuthenticatedUserCopyWith<$Res>? get activeUser; - $AudiobookShelfServerCopyWith<$Res>? get activeServer; - $AuthenticatedUserCopyWith<$Res>? get activeUser; } - /// @nodoc -class _$ApiSettingsCopyWithImpl<$Res, $Val extends ApiSettings> +class _$ApiSettingsCopyWithImpl<$Res> implements $ApiSettingsCopyWith<$Res> { - _$ApiSettingsCopyWithImpl(this._value, this._then); + _$ApiSettingsCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final ApiSettings _self; + final $Res Function(ApiSettings) _then; - /// Create a copy of ApiSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? activeServer = freezed, - Object? activeUser = freezed, - Object? activeLibraryId = freezed, - }) { - return _then(_value.copyWith( - activeServer: freezed == activeServer - ? _value.activeServer - : activeServer // ignore: cast_nullable_to_non_nullable - as AudiobookShelfServer?, - activeUser: freezed == activeUser - ? _value.activeUser - : activeUser // ignore: cast_nullable_to_non_nullable - as AuthenticatedUser?, - activeLibraryId: freezed == activeLibraryId - ? _value.activeLibraryId - : activeLibraryId // ignore: cast_nullable_to_non_nullable - as String?, - ) as $Val); +/// Create a copy of ApiSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? activeServer = freezed,Object? activeUser = freezed,Object? activeLibraryId = freezed,}) { + return _then(_self.copyWith( +activeServer: freezed == activeServer ? _self.activeServer : activeServer // ignore: cast_nullable_to_non_nullable +as AudiobookShelfServer?,activeUser: freezed == activeUser ? _self.activeUser : activeUser // ignore: cast_nullable_to_non_nullable +as AuthenticatedUser?,activeLibraryId: freezed == activeLibraryId ? _self.activeLibraryId : activeLibraryId // ignore: cast_nullable_to_non_nullable +as String?, + )); +} +/// Create a copy of ApiSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$AudiobookShelfServerCopyWith<$Res>? get activeServer { + if (_self.activeServer == null) { + return null; } - /// Create a copy of ApiSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $AudiobookShelfServerCopyWith<$Res>? get activeServer { - if (_value.activeServer == null) { - return null; - } - - return $AudiobookShelfServerCopyWith<$Res>(_value.activeServer!, (value) { - return _then(_value.copyWith(activeServer: value) as $Val); - }); + return $AudiobookShelfServerCopyWith<$Res>(_self.activeServer!, (value) { + return _then(_self.copyWith(activeServer: value)); + }); +}/// Create a copy of ApiSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$AuthenticatedUserCopyWith<$Res>? get activeUser { + if (_self.activeUser == null) { + return null; } - /// Create a copy of ApiSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $AuthenticatedUserCopyWith<$Res>? get activeUser { - if (_value.activeUser == null) { - return null; - } - - return $AuthenticatedUserCopyWith<$Res>(_value.activeUser!, (value) { - return _then(_value.copyWith(activeUser: value) as $Val); - }); - } + return $AuthenticatedUserCopyWith<$Res>(_self.activeUser!, (value) { + return _then(_self.copyWith(activeUser: value)); + }); +} } -/// @nodoc -abstract class _$$ApiSettingsImplCopyWith<$Res> - implements $ApiSettingsCopyWith<$Res> { - factory _$$ApiSettingsImplCopyWith( - _$ApiSettingsImpl value, $Res Function(_$ApiSettingsImpl) then) = - __$$ApiSettingsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {AudiobookShelfServer? activeServer, - AuthenticatedUser? activeUser, - String? activeLibraryId}); - @override - $AudiobookShelfServerCopyWith<$Res>? get activeServer; - @override - $AuthenticatedUserCopyWith<$Res>? get activeUser; +/// Adds pattern-matching-related methods to [ApiSettings]. +extension ApiSettingsPatterns on ApiSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ApiSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ApiSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ApiSettings value) $default,){ +final _that = this; +switch (_that) { +case _ApiSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ApiSettings value)? $default,){ +final _that = this; +switch (_that) { +case _ApiSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( AudiobookShelfServer? activeServer, AuthenticatedUser? activeUser, String? activeLibraryId)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ApiSettings() when $default != null: +return $default(_that.activeServer,_that.activeUser,_that.activeLibraryId);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( AudiobookShelfServer? activeServer, AuthenticatedUser? activeUser, String? activeLibraryId) $default,) {final _that = this; +switch (_that) { +case _ApiSettings(): +return $default(_that.activeServer,_that.activeUser,_that.activeLibraryId);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( AudiobookShelfServer? activeServer, AuthenticatedUser? activeUser, String? activeLibraryId)? $default,) {final _that = this; +switch (_that) { +case _ApiSettings() when $default != null: +return $default(_that.activeServer,_that.activeUser,_that.activeLibraryId);case _: + return null; + +} } -/// @nodoc -class __$$ApiSettingsImplCopyWithImpl<$Res> - extends _$ApiSettingsCopyWithImpl<$Res, _$ApiSettingsImpl> - implements _$$ApiSettingsImplCopyWith<$Res> { - __$$ApiSettingsImplCopyWithImpl( - _$ApiSettingsImpl _value, $Res Function(_$ApiSettingsImpl) _then) - : super(_value, _then); - - /// Create a copy of ApiSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? activeServer = freezed, - Object? activeUser = freezed, - Object? activeLibraryId = freezed, - }) { - return _then(_$ApiSettingsImpl( - activeServer: freezed == activeServer - ? _value.activeServer - : activeServer // ignore: cast_nullable_to_non_nullable - as AudiobookShelfServer?, - activeUser: freezed == activeUser - ? _value.activeUser - : activeUser // ignore: cast_nullable_to_non_nullable - as AuthenticatedUser?, - activeLibraryId: freezed == activeLibraryId - ? _value.activeLibraryId - : activeLibraryId // ignore: cast_nullable_to_non_nullable - as String?, - )); - } } /// @nodoc @JsonSerializable() -class _$ApiSettingsImpl implements _ApiSettings { - const _$ApiSettingsImpl( - {this.activeServer, this.activeUser, this.activeLibraryId}); - factory _$ApiSettingsImpl.fromJson(Map json) => - _$$ApiSettingsImplFromJson(json); +class _ApiSettings implements ApiSettings { + const _ApiSettings({this.activeServer, this.activeUser, this.activeLibraryId}); + factory _ApiSettings.fromJson(Map json) => _$ApiSettingsFromJson(json); - @override - final AudiobookShelfServer? activeServer; - @override - final AuthenticatedUser? activeUser; - @override - final String? activeLibraryId; +@override final AudiobookShelfServer? activeServer; +@override final AuthenticatedUser? activeUser; +@override final String? activeLibraryId; - @override - String toString() { - return 'ApiSettings(activeServer: $activeServer, activeUser: $activeUser, activeLibraryId: $activeLibraryId)'; - } +/// Create a copy of ApiSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ApiSettingsCopyWith<_ApiSettings> get copyWith => __$ApiSettingsCopyWithImpl<_ApiSettings>(this, _$identity); - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ApiSettingsImpl && - (identical(other.activeServer, activeServer) || - other.activeServer == activeServer) && - (identical(other.activeUser, activeUser) || - other.activeUser == activeUser) && - (identical(other.activeLibraryId, activeLibraryId) || - other.activeLibraryId == activeLibraryId)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => - Object.hash(runtimeType, activeServer, activeUser, activeLibraryId); - - /// Create a copy of ApiSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith => - __$$ApiSettingsImplCopyWithImpl<_$ApiSettingsImpl>(this, _$identity); - - @override - Map toJson() { - return _$$ApiSettingsImplToJson( - this, - ); - } +@override +Map toJson() { + return _$ApiSettingsToJson(this, ); } -abstract class _ApiSettings implements ApiSettings { - const factory _ApiSettings( - {final AudiobookShelfServer? activeServer, - final AuthenticatedUser? activeUser, - final String? activeLibraryId}) = _$ApiSettingsImpl; - - factory _ApiSettings.fromJson(Map json) = - _$ApiSettingsImpl.fromJson; - - @override - AudiobookShelfServer? get activeServer; - @override - AuthenticatedUser? get activeUser; - @override - String? get activeLibraryId; - - /// Create a copy of ApiSettings - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$ApiSettingsImplCopyWith<_$ApiSettingsImpl> get copyWith => - throw _privateConstructorUsedError; +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ApiSettings&&(identical(other.activeServer, activeServer) || other.activeServer == activeServer)&&(identical(other.activeUser, activeUser) || other.activeUser == activeUser)&&(identical(other.activeLibraryId, activeLibraryId) || other.activeLibraryId == activeLibraryId)); } + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,activeServer,activeUser,activeLibraryId); + +@override +String toString() { + return 'ApiSettings(activeServer: $activeServer, activeUser: $activeUser, activeLibraryId: $activeLibraryId)'; +} + + +} + +/// @nodoc +abstract mixin class _$ApiSettingsCopyWith<$Res> implements $ApiSettingsCopyWith<$Res> { + factory _$ApiSettingsCopyWith(_ApiSettings value, $Res Function(_ApiSettings) _then) = __$ApiSettingsCopyWithImpl; +@override @useResult +$Res call({ + AudiobookShelfServer? activeServer, AuthenticatedUser? activeUser, String? activeLibraryId +}); + + +@override $AudiobookShelfServerCopyWith<$Res>? get activeServer;@override $AuthenticatedUserCopyWith<$Res>? get activeUser; + +} +/// @nodoc +class __$ApiSettingsCopyWithImpl<$Res> + implements _$ApiSettingsCopyWith<$Res> { + __$ApiSettingsCopyWithImpl(this._self, this._then); + + final _ApiSettings _self; + final $Res Function(_ApiSettings) _then; + +/// Create a copy of ApiSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? activeServer = freezed,Object? activeUser = freezed,Object? activeLibraryId = freezed,}) { + return _then(_ApiSettings( +activeServer: freezed == activeServer ? _self.activeServer : activeServer // ignore: cast_nullable_to_non_nullable +as AudiobookShelfServer?,activeUser: freezed == activeUser ? _self.activeUser : activeUser // ignore: cast_nullable_to_non_nullable +as AuthenticatedUser?,activeLibraryId: freezed == activeLibraryId ? _self.activeLibraryId : activeLibraryId // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +/// Create a copy of ApiSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$AudiobookShelfServerCopyWith<$Res>? get activeServer { + if (_self.activeServer == null) { + return null; + } + + return $AudiobookShelfServerCopyWith<$Res>(_self.activeServer!, (value) { + return _then(_self.copyWith(activeServer: value)); + }); +}/// Create a copy of ApiSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$AuthenticatedUserCopyWith<$Res>? get activeUser { + if (_self.activeUser == null) { + return null; + } + + return $AuthenticatedUserCopyWith<$Res>(_self.activeUser!, (value) { + return _then(_self.copyWith(activeUser: value)); + }); +} +} + +// dart format on diff --git a/lib/settings/models/api_settings.g.dart b/lib/settings/models/api_settings.g.dart index 568754a..13b6359 100644 --- a/lib/settings/models/api_settings.g.dart +++ b/lib/settings/models/api_settings.g.dart @@ -6,20 +6,19 @@ part of 'api_settings.dart'; // JsonSerializableGenerator // ************************************************************************** -_$ApiSettingsImpl _$$ApiSettingsImplFromJson(Map json) => - _$ApiSettingsImpl( - activeServer: json['activeServer'] == null - ? null - : AudiobookShelfServer.fromJson( - json['activeServer'] as Map), - activeUser: json['activeUser'] == null - ? null - : AuthenticatedUser.fromJson( - json['activeUser'] as Map), - activeLibraryId: json['activeLibraryId'] as String?, - ); +_ApiSettings _$ApiSettingsFromJson(Map json) => _ApiSettings( + activeServer: json['activeServer'] == null + ? null + : AudiobookShelfServer.fromJson( + json['activeServer'] as Map, + ), + activeUser: json['activeUser'] == null + ? null + : AuthenticatedUser.fromJson(json['activeUser'] as Map), + activeLibraryId: json['activeLibraryId'] as String?, +); -Map _$$ApiSettingsImplToJson(_$ApiSettingsImpl instance) => +Map _$ApiSettingsToJson(_ApiSettings instance) => { 'activeServer': instance.activeServer, 'activeUser': instance.activeUser, diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index 0591e9f..41fddd0 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -1,5 +1,6 @@ // a freezed class to store the settings of the app +import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'app_settings.freezed.dart'; @@ -9,11 +10,16 @@ part 'app_settings.g.dart'; /// /// only the visual settings are stored here @freezed -class AppSettings with _$AppSettings { +sealed class AppSettings with _$AppSettings { const factory AppSettings({ @Default(ThemeSettings()) ThemeSettings themeSettings, @Default(PlayerSettings()) PlayerSettings playerSettings, + @Default(SleepTimerSettings()) SleepTimerSettings sleepTimerSettings, @Default(DownloadSettings()) DownloadSettings downloadSettings, + @Default(NotificationSettings()) NotificationSettings notificationSettings, + @Default(ShakeDetectionSettings()) + ShakeDetectionSettings shakeDetectionSettings, + @Default(HomePageSettings()) HomePageSettings homePageSettings, }) = _AppSettings; factory AppSettings.fromJson(Map json) => @@ -21,9 +27,12 @@ class AppSettings with _$AppSettings { } @freezed -class ThemeSettings with _$ThemeSettings { +sealed class ThemeSettings with _$ThemeSettings { const factory ThemeSettings({ - @Default(true) bool isDarkMode, + @Default(ThemeMode.system) ThemeMode themeMode, + @Default(false) bool highContrast, + @Default(false) bool useMaterialThemeFromSystem, + @Default('#FF311B92') String customThemeColor, @Default(true) bool useMaterialThemeOnItemPage, @Default(true) bool useCurrentPlayerThemeThroughoutApp, }) = _ThemeSettings; @@ -33,7 +42,7 @@ class ThemeSettings with _$ThemeSettings { } @freezed -class PlayerSettings with _$PlayerSettings { +sealed class PlayerSettings with _$PlayerSettings { const factory PlayerSettings({ @Default(MinimizedPlayerSettings()) MinimizedPlayerSettings miniPlayerSettings, @@ -41,9 +50,13 @@ class PlayerSettings with _$PlayerSettings { ExpandedPlayerSettings expandedPlayerSettings, @Default(1) double preferredDefaultVolume, @Default(1) double preferredDefaultSpeed, - @Default([0.75, 1, 1.25, 1.5, 1.75, 2]) List speedOptions, - @Default(SleepTimerSettings()) SleepTimerSettings sleepTimerSettings, + @Default([1, 1.25, 1.5, 1.75, 2]) List speedOptions, + @Default(0.05) double speedIncrement, + @Default(0.1) double minSpeed, + @Default(4) double maxSpeed, + @Default(Duration(seconds: 10)) Duration minimumPositionForReporting, @Default(Duration(seconds: 10)) Duration playbackReportInterval, + @Default(Duration(seconds: 15)) Duration markCompleteWhenTimeLeft, @Default(true) bool configurePlayerForEveryBook, }) = _PlayerSettings; @@ -52,7 +65,7 @@ class PlayerSettings with _$PlayerSettings { } @freezed -class ExpandedPlayerSettings with _$ExpandedPlayerSettings { +sealed class ExpandedPlayerSettings with _$ExpandedPlayerSettings { const factory ExpandedPlayerSettings({ @Default(false) bool showTotalProgress, @Default(true) bool showChapterProgress, @@ -63,31 +76,29 @@ class ExpandedPlayerSettings with _$ExpandedPlayerSettings { } @freezed -class MinimizedPlayerSettings with _$MinimizedPlayerSettings { - const factory MinimizedPlayerSettings({ - @Default(false) bool useChapterInfo, - }) = _MinimizedPlayerSettings; +sealed class MinimizedPlayerSettings with _$MinimizedPlayerSettings { + const factory MinimizedPlayerSettings({@Default(false) bool useChapterInfo}) = + _MinimizedPlayerSettings; factory MinimizedPlayerSettings.fromJson(Map json) => _$MinimizedPlayerSettingsFromJson(json); } -enum SleepTimerShakeSenseMode { never, always, nearEnds } - @freezed -class SleepTimerSettings with _$SleepTimerSettings { +sealed class SleepTimerSettings with _$SleepTimerSettings { const factory SleepTimerSettings({ @Default(Duration(minutes: 15)) Duration defaultDuration, - @Default(SleepTimerShakeSenseMode.always) - SleepTimerShakeSenseMode shakeSenseMode, - - /// the duration in which the shake is detected before the end of the timer and after the timer ends - /// only used if [shakeSenseMode] is [SleepTimerShakeSenseMode.nearEnds] - @Default(Duration(seconds: 30)) Duration shakeSenseDuration, - @Default(true) bool vibrateWhenReset, - @Default(false) bool beepWhenReset, + @Default([ + Duration(minutes: 5), + Duration(minutes: 10), + Duration(minutes: 15), + Duration(minutes: 20), + Duration(minutes: 30), + ]) + List presetDurations, + @Default(Duration(minutes: 100)) Duration maxDuration, @Default(false) bool fadeOutAudio, - @Default(0.5) double shakeDetectThreshold, + @Default(Duration(seconds: 20)) Duration fadeOutDuration, /// if true, the player will automatically rewind the audio when the sleep timer is stopped @Default(false) bool autoRewindWhenStopped, @@ -106,7 +117,7 @@ class SleepTimerSettings with _$SleepTimerSettings { @Default(false) bool autoTurnOnTimer, /// always auto turn on timer settings or during specific times - @Default(true) bool alwaysAutoTurnOnTimer, + @Default(false) bool alwaysAutoTurnOnTimer, /// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false /// @@ -120,7 +131,7 @@ class SleepTimerSettings with _$SleepTimerSettings { } @freezed -class DownloadSettings with _$DownloadSettings { +sealed class DownloadSettings with _$DownloadSettings { const factory DownloadSettings({ @Default(true) bool requiresWiFi, @Default(3) int retries, @@ -133,3 +144,92 @@ class DownloadSettings with _$DownloadSettings { factory DownloadSettings.fromJson(Map json) => _$DownloadSettingsFromJson(json); } + +@freezed +sealed class NotificationSettings with _$NotificationSettings { + const factory NotificationSettings({ + @Default(Duration(seconds: 30)) Duration fastForwardInterval, + @Default(Duration(seconds: 10)) Duration rewindInterval, + @Default(true) bool progressBarIsChapterProgress, + @Default('\$bookTitle') String primaryTitle, + @Default('\$author') String secondaryTitle, + @Default([ + NotificationMediaControl.rewind, + NotificationMediaControl.fastForward, + NotificationMediaControl.skipToPreviousChapter, + NotificationMediaControl.skipToNextChapter, + ]) + List mediaControls, + }) = _NotificationSettings; + + factory NotificationSettings.fromJson(Map json) => + _$NotificationSettingsFromJson(json); +} + +enum NotificationTitleType { + chapterTitle, + bookTitle, + author, + subtitle, + series, + narrator, + year, +} + +enum NotificationMediaControl { + fastForward(Icons.fast_forward), + rewind(Icons.fast_rewind), + speedToggle(Icons.speed), + stop(Icons.stop), + skipToNextChapter(Icons.skip_next), + skipToPreviousChapter(Icons.skip_previous); + + const NotificationMediaControl(this.icon); + + final IconData icon; +} + +/// Shake Detection Settings +@freezed +sealed class ShakeDetectionSettings with _$ShakeDetectionSettings { + const factory ShakeDetectionSettings({ + @Default(true) bool isEnabled, + @Default(ShakeDirection.horizontal) ShakeDirection direction, + @Default(5) double threshold, + @Default(ShakeAction.resetSleepTimer) ShakeAction shakeAction, + @Default({ShakeDetectedFeedback.vibrate}) + Set feedback, + @Default(0.5) double beepVolume, + + /// the duration to wait before the shake detection is enabled again + @Default(Duration(seconds: 2)) Duration shakeTriggerCoolDown, + + /// the number of shakes required to trigger the action + @Default(2) int shakeTriggerCount, + + /// acceleration sampling interval + @Default(Duration(milliseconds: 100)) Duration samplingPeriod, + }) = _ShakeDetectionSettings; + + factory ShakeDetectionSettings.fromJson(Map json) => + _$ShakeDetectionSettingsFromJson(json); +} + +enum ShakeDirection { horizontal, vertical } + +enum ShakeAction { none, playPause, resetSleepTimer, fastForward, rewind } + +enum ShakeDetectedFeedback { vibrate, beep } + +@freezed +sealed class HomePageSettings with _$HomePageSettings { + const factory HomePageSettings({ + @Default(true) bool showPlayButtonOnContinueListeningShelf, + @Default(false) bool showPlayButtonOnContinueSeriesShelf, + @Default(false) bool showPlayButtonOnAllRemainingShelves, + @Default(false) bool showPlayButtonOnListenAgainShelf, + }) = _HomePageSettings; + + factory HomePageSettings.fromJson(Map json) => + _$HomePageSettingsFromJson(json); +} diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart index 90d2cd2..3d17503 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -1,5 +1,5 @@ -// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file // 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 @@ -9,1929 +9,2951 @@ part of 'app_settings.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -AppSettings _$AppSettingsFromJson(Map json) { - return _AppSettings.fromJson(json); -} - /// @nodoc mixin _$AppSettings { - ThemeSettings get themeSettings => throw _privateConstructorUsedError; - PlayerSettings get playerSettings => throw _privateConstructorUsedError; - DownloadSettings get downloadSettings => throw _privateConstructorUsedError; + + ThemeSettings get themeSettings; PlayerSettings get playerSettings; SleepTimerSettings get sleepTimerSettings; DownloadSettings get downloadSettings; NotificationSettings get notificationSettings; ShakeDetectionSettings get shakeDetectionSettings; HomePageSettings get homePageSettings; +/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AppSettingsCopyWith get copyWith => _$AppSettingsCopyWithImpl(this as AppSettings, _$identity); /// Serializes this AppSettings to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AppSettings&&(identical(other.themeSettings, themeSettings) || other.themeSettings == themeSettings)&&(identical(other.playerSettings, playerSettings) || other.playerSettings == playerSettings)&&(identical(other.sleepTimerSettings, sleepTimerSettings) || other.sleepTimerSettings == sleepTimerSettings)&&(identical(other.downloadSettings, downloadSettings) || other.downloadSettings == downloadSettings)&&(identical(other.notificationSettings, notificationSettings) || other.notificationSettings == notificationSettings)&&(identical(other.shakeDetectionSettings, shakeDetectionSettings) || other.shakeDetectionSettings == shakeDetectionSettings)&&(identical(other.homePageSettings, homePageSettings) || other.homePageSettings == homePageSettings)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,themeSettings,playerSettings,sleepTimerSettings,downloadSettings,notificationSettings,shakeDetectionSettings,homePageSettings); + +@override +String toString() { + return 'AppSettings(themeSettings: $themeSettings, playerSettings: $playerSettings, sleepTimerSettings: $sleepTimerSettings, downloadSettings: $downloadSettings, notificationSettings: $notificationSettings, shakeDetectionSettings: $shakeDetectionSettings, homePageSettings: $homePageSettings)'; +} + - /// Create a copy of AppSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $AppSettingsCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $AppSettingsCopyWith<$Res> { - factory $AppSettingsCopyWith( - AppSettings value, $Res Function(AppSettings) then) = - _$AppSettingsCopyWithImpl<$Res, AppSettings>; - @useResult - $Res call( - {ThemeSettings themeSettings, - PlayerSettings playerSettings, - DownloadSettings downloadSettings}); +abstract mixin class $AppSettingsCopyWith<$Res> { + factory $AppSettingsCopyWith(AppSettings value, $Res Function(AppSettings) _then) = _$AppSettingsCopyWithImpl; +@useResult +$Res call({ + ThemeSettings themeSettings, PlayerSettings playerSettings, SleepTimerSettings sleepTimerSettings, DownloadSettings downloadSettings, NotificationSettings notificationSettings, ShakeDetectionSettings shakeDetectionSettings, HomePageSettings homePageSettings +}); + + +$ThemeSettingsCopyWith<$Res> get themeSettings;$PlayerSettingsCopyWith<$Res> get playerSettings;$SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings;$DownloadSettingsCopyWith<$Res> get downloadSettings;$NotificationSettingsCopyWith<$Res> get notificationSettings;$ShakeDetectionSettingsCopyWith<$Res> get shakeDetectionSettings;$HomePageSettingsCopyWith<$Res> get homePageSettings; - $ThemeSettingsCopyWith<$Res> get themeSettings; - $PlayerSettingsCopyWith<$Res> get playerSettings; - $DownloadSettingsCopyWith<$Res> get downloadSettings; } - /// @nodoc -class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings> +class _$AppSettingsCopyWithImpl<$Res> implements $AppSettingsCopyWith<$Res> { - _$AppSettingsCopyWithImpl(this._value, this._then); + _$AppSettingsCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final AppSettings _self; + final $Res Function(AppSettings) _then; - /// Create a copy of AppSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? themeSettings = null, - Object? playerSettings = null, - Object? downloadSettings = null, - }) { - return _then(_value.copyWith( - themeSettings: null == themeSettings - ? _value.themeSettings - : themeSettings // ignore: cast_nullable_to_non_nullable - as ThemeSettings, - playerSettings: null == playerSettings - ? _value.playerSettings - : playerSettings // ignore: cast_nullable_to_non_nullable - as PlayerSettings, - downloadSettings: null == downloadSettings - ? _value.downloadSettings - : downloadSettings // ignore: cast_nullable_to_non_nullable - as DownloadSettings, - ) as $Val); - } - - /// Create a copy of AppSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $ThemeSettingsCopyWith<$Res> get themeSettings { - return $ThemeSettingsCopyWith<$Res>(_value.themeSettings, (value) { - return _then(_value.copyWith(themeSettings: value) as $Val); - }); - } - - /// Create a copy of AppSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $PlayerSettingsCopyWith<$Res> get playerSettings { - return $PlayerSettingsCopyWith<$Res>(_value.playerSettings, (value) { - return _then(_value.copyWith(playerSettings: value) as $Val); - }); - } - - /// Create a copy of AppSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $DownloadSettingsCopyWith<$Res> get downloadSettings { - return $DownloadSettingsCopyWith<$Res>(_value.downloadSettings, (value) { - return _then(_value.copyWith(downloadSettings: value) as $Val); - }); - } +/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? themeSettings = null,Object? playerSettings = null,Object? sleepTimerSettings = null,Object? downloadSettings = null,Object? notificationSettings = null,Object? shakeDetectionSettings = null,Object? homePageSettings = null,}) { + return _then(_self.copyWith( +themeSettings: null == themeSettings ? _self.themeSettings : themeSettings // ignore: cast_nullable_to_non_nullable +as ThemeSettings,playerSettings: null == playerSettings ? _self.playerSettings : playerSettings // ignore: cast_nullable_to_non_nullable +as PlayerSettings,sleepTimerSettings: null == sleepTimerSettings ? _self.sleepTimerSettings : sleepTimerSettings // ignore: cast_nullable_to_non_nullable +as SleepTimerSettings,downloadSettings: null == downloadSettings ? _self.downloadSettings : downloadSettings // ignore: cast_nullable_to_non_nullable +as DownloadSettings,notificationSettings: null == notificationSettings ? _self.notificationSettings : notificationSettings // ignore: cast_nullable_to_non_nullable +as NotificationSettings,shakeDetectionSettings: null == shakeDetectionSettings ? _self.shakeDetectionSettings : shakeDetectionSettings // ignore: cast_nullable_to_non_nullable +as ShakeDetectionSettings,homePageSettings: null == homePageSettings ? _self.homePageSettings : homePageSettings // ignore: cast_nullable_to_non_nullable +as HomePageSettings, + )); +} +/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ThemeSettingsCopyWith<$Res> get themeSettings { + + return $ThemeSettingsCopyWith<$Res>(_self.themeSettings, (value) { + return _then(_self.copyWith(themeSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PlayerSettingsCopyWith<$Res> get playerSettings { + + return $PlayerSettingsCopyWith<$Res>(_self.playerSettings, (value) { + return _then(_self.copyWith(playerSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings { + + return $SleepTimerSettingsCopyWith<$Res>(_self.sleepTimerSettings, (value) { + return _then(_self.copyWith(sleepTimerSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$DownloadSettingsCopyWith<$Res> get downloadSettings { + + return $DownloadSettingsCopyWith<$Res>(_self.downloadSettings, (value) { + return _then(_self.copyWith(downloadSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$NotificationSettingsCopyWith<$Res> get notificationSettings { + + return $NotificationSettingsCopyWith<$Res>(_self.notificationSettings, (value) { + return _then(_self.copyWith(notificationSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ShakeDetectionSettingsCopyWith<$Res> get shakeDetectionSettings { + + return $ShakeDetectionSettingsCopyWith<$Res>(_self.shakeDetectionSettings, (value) { + return _then(_self.copyWith(shakeDetectionSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$HomePageSettingsCopyWith<$Res> get homePageSettings { + + return $HomePageSettingsCopyWith<$Res>(_self.homePageSettings, (value) { + return _then(_self.copyWith(homePageSettings: value)); + }); +} } -/// @nodoc -abstract class _$$AppSettingsImplCopyWith<$Res> - implements $AppSettingsCopyWith<$Res> { - factory _$$AppSettingsImplCopyWith( - _$AppSettingsImpl value, $Res Function(_$AppSettingsImpl) then) = - __$$AppSettingsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {ThemeSettings themeSettings, - PlayerSettings playerSettings, - DownloadSettings downloadSettings}); - @override - $ThemeSettingsCopyWith<$Res> get themeSettings; - @override - $PlayerSettingsCopyWith<$Res> get playerSettings; - @override - $DownloadSettingsCopyWith<$Res> get downloadSettings; +/// Adds pattern-matching-related methods to [AppSettings]. +extension AppSettingsPatterns on AppSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _AppSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AppSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _AppSettings value) $default,){ +final _that = this; +switch (_that) { +case _AppSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AppSettings value)? $default,){ +final _that = this; +switch (_that) { +case _AppSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( ThemeSettings themeSettings, PlayerSettings playerSettings, SleepTimerSettings sleepTimerSettings, DownloadSettings downloadSettings, NotificationSettings notificationSettings, ShakeDetectionSettings shakeDetectionSettings, HomePageSettings homePageSettings)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AppSettings() when $default != null: +return $default(_that.themeSettings,_that.playerSettings,_that.sleepTimerSettings,_that.downloadSettings,_that.notificationSettings,_that.shakeDetectionSettings,_that.homePageSettings);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( ThemeSettings themeSettings, PlayerSettings playerSettings, SleepTimerSettings sleepTimerSettings, DownloadSettings downloadSettings, NotificationSettings notificationSettings, ShakeDetectionSettings shakeDetectionSettings, HomePageSettings homePageSettings) $default,) {final _that = this; +switch (_that) { +case _AppSettings(): +return $default(_that.themeSettings,_that.playerSettings,_that.sleepTimerSettings,_that.downloadSettings,_that.notificationSettings,_that.shakeDetectionSettings,_that.homePageSettings);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( ThemeSettings themeSettings, PlayerSettings playerSettings, SleepTimerSettings sleepTimerSettings, DownloadSettings downloadSettings, NotificationSettings notificationSettings, ShakeDetectionSettings shakeDetectionSettings, HomePageSettings homePageSettings)? $default,) {final _that = this; +switch (_that) { +case _AppSettings() when $default != null: +return $default(_that.themeSettings,_that.playerSettings,_that.sleepTimerSettings,_that.downloadSettings,_that.notificationSettings,_that.shakeDetectionSettings,_that.homePageSettings);case _: + return null; + +} } -/// @nodoc -class __$$AppSettingsImplCopyWithImpl<$Res> - extends _$AppSettingsCopyWithImpl<$Res, _$AppSettingsImpl> - implements _$$AppSettingsImplCopyWith<$Res> { - __$$AppSettingsImplCopyWithImpl( - _$AppSettingsImpl _value, $Res Function(_$AppSettingsImpl) _then) - : super(_value, _then); - - /// Create a copy of AppSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? themeSettings = null, - Object? playerSettings = null, - Object? downloadSettings = null, - }) { - return _then(_$AppSettingsImpl( - themeSettings: null == themeSettings - ? _value.themeSettings - : themeSettings // ignore: cast_nullable_to_non_nullable - as ThemeSettings, - playerSettings: null == playerSettings - ? _value.playerSettings - : playerSettings // ignore: cast_nullable_to_non_nullable - as PlayerSettings, - downloadSettings: null == downloadSettings - ? _value.downloadSettings - : downloadSettings // ignore: cast_nullable_to_non_nullable - as DownloadSettings, - )); - } } /// @nodoc @JsonSerializable() -class _$AppSettingsImpl implements _AppSettings { - const _$AppSettingsImpl( - {this.themeSettings = const ThemeSettings(), - this.playerSettings = const PlayerSettings(), - this.downloadSettings = const DownloadSettings()}); - factory _$AppSettingsImpl.fromJson(Map json) => - _$$AppSettingsImplFromJson(json); +class _AppSettings implements AppSettings { + const _AppSettings({this.themeSettings = const ThemeSettings(), this.playerSettings = const PlayerSettings(), this.sleepTimerSettings = const SleepTimerSettings(), this.downloadSettings = const DownloadSettings(), this.notificationSettings = const NotificationSettings(), this.shakeDetectionSettings = const ShakeDetectionSettings(), this.homePageSettings = const HomePageSettings()}); + factory _AppSettings.fromJson(Map json) => _$AppSettingsFromJson(json); - @override - @JsonKey() - final ThemeSettings themeSettings; - @override - @JsonKey() - final PlayerSettings playerSettings; - @override - @JsonKey() - final DownloadSettings downloadSettings; +@override@JsonKey() final ThemeSettings themeSettings; +@override@JsonKey() final PlayerSettings playerSettings; +@override@JsonKey() final SleepTimerSettings sleepTimerSettings; +@override@JsonKey() final DownloadSettings downloadSettings; +@override@JsonKey() final NotificationSettings notificationSettings; +@override@JsonKey() final ShakeDetectionSettings shakeDetectionSettings; +@override@JsonKey() final HomePageSettings homePageSettings; - @override - String toString() { - return 'AppSettings(themeSettings: $themeSettings, playerSettings: $playerSettings, downloadSettings: $downloadSettings)'; - } +/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AppSettingsCopyWith<_AppSettings> get copyWith => __$AppSettingsCopyWithImpl<_AppSettings>(this, _$identity); - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$AppSettingsImpl && - (identical(other.themeSettings, themeSettings) || - other.themeSettings == themeSettings) && - (identical(other.playerSettings, playerSettings) || - other.playerSettings == playerSettings) && - (identical(other.downloadSettings, downloadSettings) || - other.downloadSettings == downloadSettings)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => - Object.hash(runtimeType, themeSettings, playerSettings, downloadSettings); - - /// Create a copy of AppSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith => - __$$AppSettingsImplCopyWithImpl<_$AppSettingsImpl>(this, _$identity); - - @override - Map toJson() { - return _$$AppSettingsImplToJson( - this, - ); - } +@override +Map toJson() { + return _$AppSettingsToJson(this, ); } -abstract class _AppSettings implements AppSettings { - const factory _AppSettings( - {final ThemeSettings themeSettings, - final PlayerSettings playerSettings, - final DownloadSettings downloadSettings}) = _$AppSettingsImpl; - - factory _AppSettings.fromJson(Map json) = - _$AppSettingsImpl.fromJson; - - @override - ThemeSettings get themeSettings; - @override - PlayerSettings get playerSettings; - @override - DownloadSettings get downloadSettings; - - /// Create a copy of AppSettings - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$AppSettingsImplCopyWith<_$AppSettingsImpl> get copyWith => - throw _privateConstructorUsedError; +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppSettings&&(identical(other.themeSettings, themeSettings) || other.themeSettings == themeSettings)&&(identical(other.playerSettings, playerSettings) || other.playerSettings == playerSettings)&&(identical(other.sleepTimerSettings, sleepTimerSettings) || other.sleepTimerSettings == sleepTimerSettings)&&(identical(other.downloadSettings, downloadSettings) || other.downloadSettings == downloadSettings)&&(identical(other.notificationSettings, notificationSettings) || other.notificationSettings == notificationSettings)&&(identical(other.shakeDetectionSettings, shakeDetectionSettings) || other.shakeDetectionSettings == shakeDetectionSettings)&&(identical(other.homePageSettings, homePageSettings) || other.homePageSettings == homePageSettings)); } -ThemeSettings _$ThemeSettingsFromJson(Map json) { - return _ThemeSettings.fromJson(json); +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,themeSettings,playerSettings,sleepTimerSettings,downloadSettings,notificationSettings,shakeDetectionSettings,homePageSettings); + +@override +String toString() { + return 'AppSettings(themeSettings: $themeSettings, playerSettings: $playerSettings, sleepTimerSettings: $sleepTimerSettings, downloadSettings: $downloadSettings, notificationSettings: $notificationSettings, shakeDetectionSettings: $shakeDetectionSettings, homePageSettings: $homePageSettings)'; } + +} + +/// @nodoc +abstract mixin class _$AppSettingsCopyWith<$Res> implements $AppSettingsCopyWith<$Res> { + factory _$AppSettingsCopyWith(_AppSettings value, $Res Function(_AppSettings) _then) = __$AppSettingsCopyWithImpl; +@override @useResult +$Res call({ + ThemeSettings themeSettings, PlayerSettings playerSettings, SleepTimerSettings sleepTimerSettings, DownloadSettings downloadSettings, NotificationSettings notificationSettings, ShakeDetectionSettings shakeDetectionSettings, HomePageSettings homePageSettings +}); + + +@override $ThemeSettingsCopyWith<$Res> get themeSettings;@override $PlayerSettingsCopyWith<$Res> get playerSettings;@override $SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings;@override $DownloadSettingsCopyWith<$Res> get downloadSettings;@override $NotificationSettingsCopyWith<$Res> get notificationSettings;@override $ShakeDetectionSettingsCopyWith<$Res> get shakeDetectionSettings;@override $HomePageSettingsCopyWith<$Res> get homePageSettings; + +} +/// @nodoc +class __$AppSettingsCopyWithImpl<$Res> + implements _$AppSettingsCopyWith<$Res> { + __$AppSettingsCopyWithImpl(this._self, this._then); + + final _AppSettings _self; + final $Res Function(_AppSettings) _then; + +/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? themeSettings = null,Object? playerSettings = null,Object? sleepTimerSettings = null,Object? downloadSettings = null,Object? notificationSettings = null,Object? shakeDetectionSettings = null,Object? homePageSettings = null,}) { + return _then(_AppSettings( +themeSettings: null == themeSettings ? _self.themeSettings : themeSettings // ignore: cast_nullable_to_non_nullable +as ThemeSettings,playerSettings: null == playerSettings ? _self.playerSettings : playerSettings // ignore: cast_nullable_to_non_nullable +as PlayerSettings,sleepTimerSettings: null == sleepTimerSettings ? _self.sleepTimerSettings : sleepTimerSettings // ignore: cast_nullable_to_non_nullable +as SleepTimerSettings,downloadSettings: null == downloadSettings ? _self.downloadSettings : downloadSettings // ignore: cast_nullable_to_non_nullable +as DownloadSettings,notificationSettings: null == notificationSettings ? _self.notificationSettings : notificationSettings // ignore: cast_nullable_to_non_nullable +as NotificationSettings,shakeDetectionSettings: null == shakeDetectionSettings ? _self.shakeDetectionSettings : shakeDetectionSettings // ignore: cast_nullable_to_non_nullable +as ShakeDetectionSettings,homePageSettings: null == homePageSettings ? _self.homePageSettings : homePageSettings // ignore: cast_nullable_to_non_nullable +as HomePageSettings, + )); +} + +/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ThemeSettingsCopyWith<$Res> get themeSettings { + + return $ThemeSettingsCopyWith<$Res>(_self.themeSettings, (value) { + return _then(_self.copyWith(themeSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$PlayerSettingsCopyWith<$Res> get playerSettings { + + return $PlayerSettingsCopyWith<$Res>(_self.playerSettings, (value) { + return _then(_self.copyWith(playerSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings { + + return $SleepTimerSettingsCopyWith<$Res>(_self.sleepTimerSettings, (value) { + return _then(_self.copyWith(sleepTimerSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$DownloadSettingsCopyWith<$Res> get downloadSettings { + + return $DownloadSettingsCopyWith<$Res>(_self.downloadSettings, (value) { + return _then(_self.copyWith(downloadSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$NotificationSettingsCopyWith<$Res> get notificationSettings { + + return $NotificationSettingsCopyWith<$Res>(_self.notificationSettings, (value) { + return _then(_self.copyWith(notificationSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ShakeDetectionSettingsCopyWith<$Res> get shakeDetectionSettings { + + return $ShakeDetectionSettingsCopyWith<$Res>(_self.shakeDetectionSettings, (value) { + return _then(_self.copyWith(shakeDetectionSettings: value)); + }); +}/// Create a copy of AppSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$HomePageSettingsCopyWith<$Res> get homePageSettings { + + return $HomePageSettingsCopyWith<$Res>(_self.homePageSettings, (value) { + return _then(_self.copyWith(homePageSettings: value)); + }); +} +} + + /// @nodoc mixin _$ThemeSettings { - bool get isDarkMode => throw _privateConstructorUsedError; - bool get useMaterialThemeOnItemPage => throw _privateConstructorUsedError; - bool get useCurrentPlayerThemeThroughoutApp => - throw _privateConstructorUsedError; + + ThemeMode get themeMode; bool get highContrast; bool get useMaterialThemeFromSystem; String get customThemeColor; bool get useMaterialThemeOnItemPage; bool get useCurrentPlayerThemeThroughoutApp; +/// Create a copy of ThemeSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ThemeSettingsCopyWith get copyWith => _$ThemeSettingsCopyWithImpl(this as ThemeSettings, _$identity); /// Serializes this ThemeSettings to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ThemeSettings&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.useMaterialThemeFromSystem, useMaterialThemeFromSystem) || other.useMaterialThemeFromSystem == useMaterialThemeFromSystem)&&(identical(other.customThemeColor, customThemeColor) || other.customThemeColor == customThemeColor)&&(identical(other.useMaterialThemeOnItemPage, useMaterialThemeOnItemPage) || other.useMaterialThemeOnItemPage == useMaterialThemeOnItemPage)&&(identical(other.useCurrentPlayerThemeThroughoutApp, useCurrentPlayerThemeThroughoutApp) || other.useCurrentPlayerThemeThroughoutApp == useCurrentPlayerThemeThroughoutApp)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,themeMode,highContrast,useMaterialThemeFromSystem,customThemeColor,useMaterialThemeOnItemPage,useCurrentPlayerThemeThroughoutApp); + +@override +String toString() { + return 'ThemeSettings(themeMode: $themeMode, highContrast: $highContrast, useMaterialThemeFromSystem: $useMaterialThemeFromSystem, customThemeColor: $customThemeColor, useMaterialThemeOnItemPage: $useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp: $useCurrentPlayerThemeThroughoutApp)'; +} + - /// Create a copy of ThemeSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $ThemeSettingsCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $ThemeSettingsCopyWith<$Res> { - factory $ThemeSettingsCopyWith( - ThemeSettings value, $Res Function(ThemeSettings) then) = - _$ThemeSettingsCopyWithImpl<$Res, ThemeSettings>; - @useResult - $Res call( - {bool isDarkMode, - bool useMaterialThemeOnItemPage, - bool useCurrentPlayerThemeThroughoutApp}); -} +abstract mixin class $ThemeSettingsCopyWith<$Res> { + factory $ThemeSettingsCopyWith(ThemeSettings value, $Res Function(ThemeSettings) _then) = _$ThemeSettingsCopyWithImpl; +@useResult +$Res call({ + ThemeMode themeMode, bool highContrast, bool useMaterialThemeFromSystem, String customThemeColor, bool useMaterialThemeOnItemPage, bool useCurrentPlayerThemeThroughoutApp +}); + + + +} /// @nodoc -class _$ThemeSettingsCopyWithImpl<$Res, $Val extends ThemeSettings> +class _$ThemeSettingsCopyWithImpl<$Res> implements $ThemeSettingsCopyWith<$Res> { - _$ThemeSettingsCopyWithImpl(this._value, this._then); + _$ThemeSettingsCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final ThemeSettings _self; + final $Res Function(ThemeSettings) _then; - /// Create a copy of ThemeSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? isDarkMode = null, - Object? useMaterialThemeOnItemPage = null, - Object? useCurrentPlayerThemeThroughoutApp = null, - }) { - return _then(_value.copyWith( - isDarkMode: null == isDarkMode - ? _value.isDarkMode - : isDarkMode // ignore: cast_nullable_to_non_nullable - as bool, - useMaterialThemeOnItemPage: null == useMaterialThemeOnItemPage - ? _value.useMaterialThemeOnItemPage - : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable - as bool, - useCurrentPlayerThemeThroughoutApp: null == - useCurrentPlayerThemeThroughoutApp - ? _value.useCurrentPlayerThemeThroughoutApp - : useCurrentPlayerThemeThroughoutApp // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } +/// Create a copy of ThemeSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? themeMode = null,Object? highContrast = null,Object? useMaterialThemeFromSystem = null,Object? customThemeColor = null,Object? useMaterialThemeOnItemPage = null,Object? useCurrentPlayerThemeThroughoutApp = null,}) { + return _then(_self.copyWith( +themeMode: null == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable +as ThemeMode,highContrast: null == highContrast ? _self.highContrast : highContrast // ignore: cast_nullable_to_non_nullable +as bool,useMaterialThemeFromSystem: null == useMaterialThemeFromSystem ? _self.useMaterialThemeFromSystem : useMaterialThemeFromSystem // ignore: cast_nullable_to_non_nullable +as bool,customThemeColor: null == customThemeColor ? _self.customThemeColor : customThemeColor // ignore: cast_nullable_to_non_nullable +as String,useMaterialThemeOnItemPage: null == useMaterialThemeOnItemPage ? _self.useMaterialThemeOnItemPage : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable +as bool,useCurrentPlayerThemeThroughoutApp: null == useCurrentPlayerThemeThroughoutApp ? _self.useCurrentPlayerThemeThroughoutApp : useCurrentPlayerThemeThroughoutApp // ignore: cast_nullable_to_non_nullable +as bool, + )); } -/// @nodoc -abstract class _$$ThemeSettingsImplCopyWith<$Res> - implements $ThemeSettingsCopyWith<$Res> { - factory _$$ThemeSettingsImplCopyWith( - _$ThemeSettingsImpl value, $Res Function(_$ThemeSettingsImpl) then) = - __$$ThemeSettingsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {bool isDarkMode, - bool useMaterialThemeOnItemPage, - bool useCurrentPlayerThemeThroughoutApp}); } -/// @nodoc -class __$$ThemeSettingsImplCopyWithImpl<$Res> - extends _$ThemeSettingsCopyWithImpl<$Res, _$ThemeSettingsImpl> - implements _$$ThemeSettingsImplCopyWith<$Res> { - __$$ThemeSettingsImplCopyWithImpl( - _$ThemeSettingsImpl _value, $Res Function(_$ThemeSettingsImpl) _then) - : super(_value, _then); - /// Create a copy of ThemeSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? isDarkMode = null, - Object? useMaterialThemeOnItemPage = null, - Object? useCurrentPlayerThemeThroughoutApp = null, - }) { - return _then(_$ThemeSettingsImpl( - isDarkMode: null == isDarkMode - ? _value.isDarkMode - : isDarkMode // ignore: cast_nullable_to_non_nullable - as bool, - useMaterialThemeOnItemPage: null == useMaterialThemeOnItemPage - ? _value.useMaterialThemeOnItemPage - : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable - as bool, - useCurrentPlayerThemeThroughoutApp: null == - useCurrentPlayerThemeThroughoutApp - ? _value.useCurrentPlayerThemeThroughoutApp - : useCurrentPlayerThemeThroughoutApp // ignore: cast_nullable_to_non_nullable - as bool, - )); - } +/// Adds pattern-matching-related methods to [ThemeSettings]. +extension ThemeSettingsPatterns on ThemeSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ThemeSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ThemeSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ThemeSettings value) $default,){ +final _that = this; +switch (_that) { +case _ThemeSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ThemeSettings value)? $default,){ +final _that = this; +switch (_that) { +case _ThemeSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( ThemeMode themeMode, bool highContrast, bool useMaterialThemeFromSystem, String customThemeColor, bool useMaterialThemeOnItemPage, bool useCurrentPlayerThemeThroughoutApp)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ThemeSettings() when $default != null: +return $default(_that.themeMode,_that.highContrast,_that.useMaterialThemeFromSystem,_that.customThemeColor,_that.useMaterialThemeOnItemPage,_that.useCurrentPlayerThemeThroughoutApp);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( ThemeMode themeMode, bool highContrast, bool useMaterialThemeFromSystem, String customThemeColor, bool useMaterialThemeOnItemPage, bool useCurrentPlayerThemeThroughoutApp) $default,) {final _that = this; +switch (_that) { +case _ThemeSettings(): +return $default(_that.themeMode,_that.highContrast,_that.useMaterialThemeFromSystem,_that.customThemeColor,_that.useMaterialThemeOnItemPage,_that.useCurrentPlayerThemeThroughoutApp);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( ThemeMode themeMode, bool highContrast, bool useMaterialThemeFromSystem, String customThemeColor, bool useMaterialThemeOnItemPage, bool useCurrentPlayerThemeThroughoutApp)? $default,) {final _that = this; +switch (_that) { +case _ThemeSettings() when $default != null: +return $default(_that.themeMode,_that.highContrast,_that.useMaterialThemeFromSystem,_that.customThemeColor,_that.useMaterialThemeOnItemPage,_that.useCurrentPlayerThemeThroughoutApp);case _: + return null; + +} +} + } /// @nodoc @JsonSerializable() -class _$ThemeSettingsImpl implements _ThemeSettings { - const _$ThemeSettingsImpl( - {this.isDarkMode = true, - this.useMaterialThemeOnItemPage = true, - this.useCurrentPlayerThemeThroughoutApp = true}); - factory _$ThemeSettingsImpl.fromJson(Map json) => - _$$ThemeSettingsImplFromJson(json); +class _ThemeSettings implements ThemeSettings { + const _ThemeSettings({this.themeMode = ThemeMode.system, this.highContrast = false, this.useMaterialThemeFromSystem = false, this.customThemeColor = '#FF311B92', this.useMaterialThemeOnItemPage = true, this.useCurrentPlayerThemeThroughoutApp = true}); + factory _ThemeSettings.fromJson(Map json) => _$ThemeSettingsFromJson(json); - @override - @JsonKey() - final bool isDarkMode; - @override - @JsonKey() - final bool useMaterialThemeOnItemPage; - @override - @JsonKey() - final bool useCurrentPlayerThemeThroughoutApp; +@override@JsonKey() final ThemeMode themeMode; +@override@JsonKey() final bool highContrast; +@override@JsonKey() final bool useMaterialThemeFromSystem; +@override@JsonKey() final String customThemeColor; +@override@JsonKey() final bool useMaterialThemeOnItemPage; +@override@JsonKey() final bool useCurrentPlayerThemeThroughoutApp; - @override - String toString() { - return 'ThemeSettings(isDarkMode: $isDarkMode, useMaterialThemeOnItemPage: $useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp: $useCurrentPlayerThemeThroughoutApp)'; - } +/// Create a copy of ThemeSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ThemeSettingsCopyWith<_ThemeSettings> get copyWith => __$ThemeSettingsCopyWithImpl<_ThemeSettings>(this, _$identity); - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ThemeSettingsImpl && - (identical(other.isDarkMode, isDarkMode) || - other.isDarkMode == isDarkMode) && - (identical(other.useMaterialThemeOnItemPage, - useMaterialThemeOnItemPage) || - other.useMaterialThemeOnItemPage == - useMaterialThemeOnItemPage) && - (identical(other.useCurrentPlayerThemeThroughoutApp, - useCurrentPlayerThemeThroughoutApp) || - other.useCurrentPlayerThemeThroughoutApp == - useCurrentPlayerThemeThroughoutApp)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, isDarkMode, - useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp); - - /// Create a copy of ThemeSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$ThemeSettingsImplCopyWith<_$ThemeSettingsImpl> get copyWith => - __$$ThemeSettingsImplCopyWithImpl<_$ThemeSettingsImpl>(this, _$identity); - - @override - Map toJson() { - return _$$ThemeSettingsImplToJson( - this, - ); - } +@override +Map toJson() { + return _$ThemeSettingsToJson(this, ); } -abstract class _ThemeSettings implements ThemeSettings { - const factory _ThemeSettings( - {final bool isDarkMode, - final bool useMaterialThemeOnItemPage, - final bool useCurrentPlayerThemeThroughoutApp}) = _$ThemeSettingsImpl; - - factory _ThemeSettings.fromJson(Map json) = - _$ThemeSettingsImpl.fromJson; - - @override - bool get isDarkMode; - @override - bool get useMaterialThemeOnItemPage; - @override - bool get useCurrentPlayerThemeThroughoutApp; - - /// Create a copy of ThemeSettings - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$ThemeSettingsImplCopyWith<_$ThemeSettingsImpl> get copyWith => - throw _privateConstructorUsedError; +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ThemeSettings&&(identical(other.themeMode, themeMode) || other.themeMode == themeMode)&&(identical(other.highContrast, highContrast) || other.highContrast == highContrast)&&(identical(other.useMaterialThemeFromSystem, useMaterialThemeFromSystem) || other.useMaterialThemeFromSystem == useMaterialThemeFromSystem)&&(identical(other.customThemeColor, customThemeColor) || other.customThemeColor == customThemeColor)&&(identical(other.useMaterialThemeOnItemPage, useMaterialThemeOnItemPage) || other.useMaterialThemeOnItemPage == useMaterialThemeOnItemPage)&&(identical(other.useCurrentPlayerThemeThroughoutApp, useCurrentPlayerThemeThroughoutApp) || other.useCurrentPlayerThemeThroughoutApp == useCurrentPlayerThemeThroughoutApp)); } -PlayerSettings _$PlayerSettingsFromJson(Map json) { - return _PlayerSettings.fromJson(json); +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,themeMode,highContrast,useMaterialThemeFromSystem,customThemeColor,useMaterialThemeOnItemPage,useCurrentPlayerThemeThroughoutApp); + +@override +String toString() { + return 'ThemeSettings(themeMode: $themeMode, highContrast: $highContrast, useMaterialThemeFromSystem: $useMaterialThemeFromSystem, customThemeColor: $customThemeColor, useMaterialThemeOnItemPage: $useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp: $useCurrentPlayerThemeThroughoutApp)'; } + +} + +/// @nodoc +abstract mixin class _$ThemeSettingsCopyWith<$Res> implements $ThemeSettingsCopyWith<$Res> { + factory _$ThemeSettingsCopyWith(_ThemeSettings value, $Res Function(_ThemeSettings) _then) = __$ThemeSettingsCopyWithImpl; +@override @useResult +$Res call({ + ThemeMode themeMode, bool highContrast, bool useMaterialThemeFromSystem, String customThemeColor, bool useMaterialThemeOnItemPage, bool useCurrentPlayerThemeThroughoutApp +}); + + + + +} +/// @nodoc +class __$ThemeSettingsCopyWithImpl<$Res> + implements _$ThemeSettingsCopyWith<$Res> { + __$ThemeSettingsCopyWithImpl(this._self, this._then); + + final _ThemeSettings _self; + final $Res Function(_ThemeSettings) _then; + +/// Create a copy of ThemeSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? themeMode = null,Object? highContrast = null,Object? useMaterialThemeFromSystem = null,Object? customThemeColor = null,Object? useMaterialThemeOnItemPage = null,Object? useCurrentPlayerThemeThroughoutApp = null,}) { + return _then(_ThemeSettings( +themeMode: null == themeMode ? _self.themeMode : themeMode // ignore: cast_nullable_to_non_nullable +as ThemeMode,highContrast: null == highContrast ? _self.highContrast : highContrast // ignore: cast_nullable_to_non_nullable +as bool,useMaterialThemeFromSystem: null == useMaterialThemeFromSystem ? _self.useMaterialThemeFromSystem : useMaterialThemeFromSystem // ignore: cast_nullable_to_non_nullable +as bool,customThemeColor: null == customThemeColor ? _self.customThemeColor : customThemeColor // ignore: cast_nullable_to_non_nullable +as String,useMaterialThemeOnItemPage: null == useMaterialThemeOnItemPage ? _self.useMaterialThemeOnItemPage : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable +as bool,useCurrentPlayerThemeThroughoutApp: null == useCurrentPlayerThemeThroughoutApp ? _self.useCurrentPlayerThemeThroughoutApp : useCurrentPlayerThemeThroughoutApp // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + + /// @nodoc mixin _$PlayerSettings { - MinimizedPlayerSettings get miniPlayerSettings => - throw _privateConstructorUsedError; - ExpandedPlayerSettings get expandedPlayerSettings => - throw _privateConstructorUsedError; - double get preferredDefaultVolume => throw _privateConstructorUsedError; - double get preferredDefaultSpeed => throw _privateConstructorUsedError; - List get speedOptions => throw _privateConstructorUsedError; - SleepTimerSettings get sleepTimerSettings => - throw _privateConstructorUsedError; - Duration get playbackReportInterval => throw _privateConstructorUsedError; - bool get configurePlayerForEveryBook => throw _privateConstructorUsedError; + + MinimizedPlayerSettings get miniPlayerSettings; ExpandedPlayerSettings get expandedPlayerSettings; double get preferredDefaultVolume; double get preferredDefaultSpeed; List get speedOptions; double get speedIncrement; double get minSpeed; double get maxSpeed; Duration get minimumPositionForReporting; Duration get playbackReportInterval; Duration get markCompleteWhenTimeLeft; bool get configurePlayerForEveryBook; +/// Create a copy of PlayerSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$PlayerSettingsCopyWith get copyWith => _$PlayerSettingsCopyWithImpl(this as PlayerSettings, _$identity); /// Serializes this PlayerSettings to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is PlayerSettings&&(identical(other.miniPlayerSettings, miniPlayerSettings) || other.miniPlayerSettings == miniPlayerSettings)&&(identical(other.expandedPlayerSettings, expandedPlayerSettings) || other.expandedPlayerSettings == expandedPlayerSettings)&&(identical(other.preferredDefaultVolume, preferredDefaultVolume) || other.preferredDefaultVolume == preferredDefaultVolume)&&(identical(other.preferredDefaultSpeed, preferredDefaultSpeed) || other.preferredDefaultSpeed == preferredDefaultSpeed)&&const DeepCollectionEquality().equals(other.speedOptions, speedOptions)&&(identical(other.speedIncrement, speedIncrement) || other.speedIncrement == speedIncrement)&&(identical(other.minSpeed, minSpeed) || other.minSpeed == minSpeed)&&(identical(other.maxSpeed, maxSpeed) || other.maxSpeed == maxSpeed)&&(identical(other.minimumPositionForReporting, minimumPositionForReporting) || other.minimumPositionForReporting == minimumPositionForReporting)&&(identical(other.playbackReportInterval, playbackReportInterval) || other.playbackReportInterval == playbackReportInterval)&&(identical(other.markCompleteWhenTimeLeft, markCompleteWhenTimeLeft) || other.markCompleteWhenTimeLeft == markCompleteWhenTimeLeft)&&(identical(other.configurePlayerForEveryBook, configurePlayerForEveryBook) || other.configurePlayerForEveryBook == configurePlayerForEveryBook)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,miniPlayerSettings,expandedPlayerSettings,preferredDefaultVolume,preferredDefaultSpeed,const DeepCollectionEquality().hash(speedOptions),speedIncrement,minSpeed,maxSpeed,minimumPositionForReporting,playbackReportInterval,markCompleteWhenTimeLeft,configurePlayerForEveryBook); + +@override +String toString() { + return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, speedIncrement: $speedIncrement, minSpeed: $minSpeed, maxSpeed: $maxSpeed, minimumPositionForReporting: $minimumPositionForReporting, playbackReportInterval: $playbackReportInterval, markCompleteWhenTimeLeft: $markCompleteWhenTimeLeft, configurePlayerForEveryBook: $configurePlayerForEveryBook)'; +} + - /// Create a copy of PlayerSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $PlayerSettingsCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $PlayerSettingsCopyWith<$Res> { - factory $PlayerSettingsCopyWith( - PlayerSettings value, $Res Function(PlayerSettings) then) = - _$PlayerSettingsCopyWithImpl<$Res, PlayerSettings>; - @useResult - $Res call( - {MinimizedPlayerSettings miniPlayerSettings, - ExpandedPlayerSettings expandedPlayerSettings, - double preferredDefaultVolume, - double preferredDefaultSpeed, - List speedOptions, - SleepTimerSettings sleepTimerSettings, - Duration playbackReportInterval, - bool configurePlayerForEveryBook}); +abstract mixin class $PlayerSettingsCopyWith<$Res> { + factory $PlayerSettingsCopyWith(PlayerSettings value, $Res Function(PlayerSettings) _then) = _$PlayerSettingsCopyWithImpl; +@useResult +$Res call({ + MinimizedPlayerSettings miniPlayerSettings, ExpandedPlayerSettings expandedPlayerSettings, double preferredDefaultVolume, double preferredDefaultSpeed, List speedOptions, double speedIncrement, double minSpeed, double maxSpeed, Duration minimumPositionForReporting, Duration playbackReportInterval, Duration markCompleteWhenTimeLeft, bool configurePlayerForEveryBook +}); + + +$MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings;$ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings; - $MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings; - $ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings; - $SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings; } - /// @nodoc -class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings> +class _$PlayerSettingsCopyWithImpl<$Res> implements $PlayerSettingsCopyWith<$Res> { - _$PlayerSettingsCopyWithImpl(this._value, this._then); + _$PlayerSettingsCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final PlayerSettings _self; + final $Res Function(PlayerSettings) _then; - /// Create a copy of PlayerSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? miniPlayerSettings = null, - Object? expandedPlayerSettings = null, - Object? preferredDefaultVolume = null, - Object? preferredDefaultSpeed = null, - Object? speedOptions = null, - Object? sleepTimerSettings = null, - Object? playbackReportInterval = null, - Object? configurePlayerForEveryBook = null, - }) { - return _then(_value.copyWith( - miniPlayerSettings: null == miniPlayerSettings - ? _value.miniPlayerSettings - : miniPlayerSettings // ignore: cast_nullable_to_non_nullable - as MinimizedPlayerSettings, - expandedPlayerSettings: null == expandedPlayerSettings - ? _value.expandedPlayerSettings - : expandedPlayerSettings // ignore: cast_nullable_to_non_nullable - as ExpandedPlayerSettings, - preferredDefaultVolume: null == preferredDefaultVolume - ? _value.preferredDefaultVolume - : preferredDefaultVolume // ignore: cast_nullable_to_non_nullable - as double, - preferredDefaultSpeed: null == preferredDefaultSpeed - ? _value.preferredDefaultSpeed - : preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable - as double, - speedOptions: null == speedOptions - ? _value.speedOptions - : speedOptions // ignore: cast_nullable_to_non_nullable - as List, - sleepTimerSettings: null == sleepTimerSettings - ? _value.sleepTimerSettings - : sleepTimerSettings // ignore: cast_nullable_to_non_nullable - as SleepTimerSettings, - playbackReportInterval: null == playbackReportInterval - ? _value.playbackReportInterval - : playbackReportInterval // ignore: cast_nullable_to_non_nullable - as Duration, - configurePlayerForEveryBook: null == configurePlayerForEveryBook - ? _value.configurePlayerForEveryBook - : configurePlayerForEveryBook // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } - - /// Create a copy of PlayerSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings { - return $MinimizedPlayerSettingsCopyWith<$Res>(_value.miniPlayerSettings, - (value) { - return _then(_value.copyWith(miniPlayerSettings: value) as $Val); - }); - } - - /// Create a copy of PlayerSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings { - return $ExpandedPlayerSettingsCopyWith<$Res>(_value.expandedPlayerSettings, - (value) { - return _then(_value.copyWith(expandedPlayerSettings: value) as $Val); - }); - } - - /// Create a copy of PlayerSettings - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings { - return $SleepTimerSettingsCopyWith<$Res>(_value.sleepTimerSettings, - (value) { - return _then(_value.copyWith(sleepTimerSettings: value) as $Val); - }); - } +/// Create a copy of PlayerSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? miniPlayerSettings = null,Object? expandedPlayerSettings = null,Object? preferredDefaultVolume = null,Object? preferredDefaultSpeed = null,Object? speedOptions = null,Object? speedIncrement = null,Object? minSpeed = null,Object? maxSpeed = null,Object? minimumPositionForReporting = null,Object? playbackReportInterval = null,Object? markCompleteWhenTimeLeft = null,Object? configurePlayerForEveryBook = null,}) { + return _then(_self.copyWith( +miniPlayerSettings: null == miniPlayerSettings ? _self.miniPlayerSettings : miniPlayerSettings // ignore: cast_nullable_to_non_nullable +as MinimizedPlayerSettings,expandedPlayerSettings: null == expandedPlayerSettings ? _self.expandedPlayerSettings : expandedPlayerSettings // ignore: cast_nullable_to_non_nullable +as ExpandedPlayerSettings,preferredDefaultVolume: null == preferredDefaultVolume ? _self.preferredDefaultVolume : preferredDefaultVolume // ignore: cast_nullable_to_non_nullable +as double,preferredDefaultSpeed: null == preferredDefaultSpeed ? _self.preferredDefaultSpeed : preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable +as double,speedOptions: null == speedOptions ? _self.speedOptions : speedOptions // ignore: cast_nullable_to_non_nullable +as List,speedIncrement: null == speedIncrement ? _self.speedIncrement : speedIncrement // ignore: cast_nullable_to_non_nullable +as double,minSpeed: null == minSpeed ? _self.minSpeed : minSpeed // ignore: cast_nullable_to_non_nullable +as double,maxSpeed: null == maxSpeed ? _self.maxSpeed : maxSpeed // ignore: cast_nullable_to_non_nullable +as double,minimumPositionForReporting: null == minimumPositionForReporting ? _self.minimumPositionForReporting : minimumPositionForReporting // ignore: cast_nullable_to_non_nullable +as Duration,playbackReportInterval: null == playbackReportInterval ? _self.playbackReportInterval : playbackReportInterval // ignore: cast_nullable_to_non_nullable +as Duration,markCompleteWhenTimeLeft: null == markCompleteWhenTimeLeft ? _self.markCompleteWhenTimeLeft : markCompleteWhenTimeLeft // ignore: cast_nullable_to_non_nullable +as Duration,configurePlayerForEveryBook: null == configurePlayerForEveryBook ? _self.configurePlayerForEveryBook : configurePlayerForEveryBook // ignore: cast_nullable_to_non_nullable +as bool, + )); +} +/// Create a copy of PlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings { + + return $MinimizedPlayerSettingsCopyWith<$Res>(_self.miniPlayerSettings, (value) { + return _then(_self.copyWith(miniPlayerSettings: value)); + }); +}/// Create a copy of PlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings { + + return $ExpandedPlayerSettingsCopyWith<$Res>(_self.expandedPlayerSettings, (value) { + return _then(_self.copyWith(expandedPlayerSettings: value)); + }); +} } -/// @nodoc -abstract class _$$PlayerSettingsImplCopyWith<$Res> - implements $PlayerSettingsCopyWith<$Res> { - factory _$$PlayerSettingsImplCopyWith(_$PlayerSettingsImpl value, - $Res Function(_$PlayerSettingsImpl) then) = - __$$PlayerSettingsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {MinimizedPlayerSettings miniPlayerSettings, - ExpandedPlayerSettings expandedPlayerSettings, - double preferredDefaultVolume, - double preferredDefaultSpeed, - List speedOptions, - SleepTimerSettings sleepTimerSettings, - Duration playbackReportInterval, - bool configurePlayerForEveryBook}); - @override - $MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings; - @override - $ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings; - @override - $SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings; +/// Adds pattern-matching-related methods to [PlayerSettings]. +extension PlayerSettingsPatterns on PlayerSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _PlayerSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _PlayerSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _PlayerSettings value) $default,){ +final _that = this; +switch (_that) { +case _PlayerSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _PlayerSettings value)? $default,){ +final _that = this; +switch (_that) { +case _PlayerSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( MinimizedPlayerSettings miniPlayerSettings, ExpandedPlayerSettings expandedPlayerSettings, double preferredDefaultVolume, double preferredDefaultSpeed, List speedOptions, double speedIncrement, double minSpeed, double maxSpeed, Duration minimumPositionForReporting, Duration playbackReportInterval, Duration markCompleteWhenTimeLeft, bool configurePlayerForEveryBook)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _PlayerSettings() when $default != null: +return $default(_that.miniPlayerSettings,_that.expandedPlayerSettings,_that.preferredDefaultVolume,_that.preferredDefaultSpeed,_that.speedOptions,_that.speedIncrement,_that.minSpeed,_that.maxSpeed,_that.minimumPositionForReporting,_that.playbackReportInterval,_that.markCompleteWhenTimeLeft,_that.configurePlayerForEveryBook);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( MinimizedPlayerSettings miniPlayerSettings, ExpandedPlayerSettings expandedPlayerSettings, double preferredDefaultVolume, double preferredDefaultSpeed, List speedOptions, double speedIncrement, double minSpeed, double maxSpeed, Duration minimumPositionForReporting, Duration playbackReportInterval, Duration markCompleteWhenTimeLeft, bool configurePlayerForEveryBook) $default,) {final _that = this; +switch (_that) { +case _PlayerSettings(): +return $default(_that.miniPlayerSettings,_that.expandedPlayerSettings,_that.preferredDefaultVolume,_that.preferredDefaultSpeed,_that.speedOptions,_that.speedIncrement,_that.minSpeed,_that.maxSpeed,_that.minimumPositionForReporting,_that.playbackReportInterval,_that.markCompleteWhenTimeLeft,_that.configurePlayerForEveryBook);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( MinimizedPlayerSettings miniPlayerSettings, ExpandedPlayerSettings expandedPlayerSettings, double preferredDefaultVolume, double preferredDefaultSpeed, List speedOptions, double speedIncrement, double minSpeed, double maxSpeed, Duration minimumPositionForReporting, Duration playbackReportInterval, Duration markCompleteWhenTimeLeft, bool configurePlayerForEveryBook)? $default,) {final _that = this; +switch (_that) { +case _PlayerSettings() when $default != null: +return $default(_that.miniPlayerSettings,_that.expandedPlayerSettings,_that.preferredDefaultVolume,_that.preferredDefaultSpeed,_that.speedOptions,_that.speedIncrement,_that.minSpeed,_that.maxSpeed,_that.minimumPositionForReporting,_that.playbackReportInterval,_that.markCompleteWhenTimeLeft,_that.configurePlayerForEveryBook);case _: + return null; + +} } -/// @nodoc -class __$$PlayerSettingsImplCopyWithImpl<$Res> - extends _$PlayerSettingsCopyWithImpl<$Res, _$PlayerSettingsImpl> - implements _$$PlayerSettingsImplCopyWith<$Res> { - __$$PlayerSettingsImplCopyWithImpl( - _$PlayerSettingsImpl _value, $Res Function(_$PlayerSettingsImpl) _then) - : super(_value, _then); - - /// Create a copy of PlayerSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? miniPlayerSettings = null, - Object? expandedPlayerSettings = null, - Object? preferredDefaultVolume = null, - Object? preferredDefaultSpeed = null, - Object? speedOptions = null, - Object? sleepTimerSettings = null, - Object? playbackReportInterval = null, - Object? configurePlayerForEveryBook = null, - }) { - return _then(_$PlayerSettingsImpl( - miniPlayerSettings: null == miniPlayerSettings - ? _value.miniPlayerSettings - : miniPlayerSettings // ignore: cast_nullable_to_non_nullable - as MinimizedPlayerSettings, - expandedPlayerSettings: null == expandedPlayerSettings - ? _value.expandedPlayerSettings - : expandedPlayerSettings // ignore: cast_nullable_to_non_nullable - as ExpandedPlayerSettings, - preferredDefaultVolume: null == preferredDefaultVolume - ? _value.preferredDefaultVolume - : preferredDefaultVolume // ignore: cast_nullable_to_non_nullable - as double, - preferredDefaultSpeed: null == preferredDefaultSpeed - ? _value.preferredDefaultSpeed - : preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable - as double, - speedOptions: null == speedOptions - ? _value._speedOptions - : speedOptions // ignore: cast_nullable_to_non_nullable - as List, - sleepTimerSettings: null == sleepTimerSettings - ? _value.sleepTimerSettings - : sleepTimerSettings // ignore: cast_nullable_to_non_nullable - as SleepTimerSettings, - playbackReportInterval: null == playbackReportInterval - ? _value.playbackReportInterval - : playbackReportInterval // ignore: cast_nullable_to_non_nullable - as Duration, - configurePlayerForEveryBook: null == configurePlayerForEveryBook - ? _value.configurePlayerForEveryBook - : configurePlayerForEveryBook // ignore: cast_nullable_to_non_nullable - as bool, - )); - } } /// @nodoc @JsonSerializable() -class _$PlayerSettingsImpl implements _PlayerSettings { - const _$PlayerSettingsImpl( - {this.miniPlayerSettings = const MinimizedPlayerSettings(), - this.expandedPlayerSettings = const ExpandedPlayerSettings(), - this.preferredDefaultVolume = 1, - this.preferredDefaultSpeed = 1, - final List speedOptions = const [0.75, 1, 1.25, 1.5, 1.75, 2], - this.sleepTimerSettings = const SleepTimerSettings(), - this.playbackReportInterval = const Duration(seconds: 10), - this.configurePlayerForEveryBook = true}) - : _speedOptions = speedOptions; - factory _$PlayerSettingsImpl.fromJson(Map json) => - _$$PlayerSettingsImplFromJson(json); +class _PlayerSettings implements PlayerSettings { + const _PlayerSettings({this.miniPlayerSettings = const MinimizedPlayerSettings(), this.expandedPlayerSettings = const ExpandedPlayerSettings(), this.preferredDefaultVolume = 1, this.preferredDefaultSpeed = 1, final List speedOptions = const [1, 1.25, 1.5, 1.75, 2], this.speedIncrement = 0.05, this.minSpeed = 0.1, this.maxSpeed = 4, this.minimumPositionForReporting = const Duration(seconds: 10), this.playbackReportInterval = const Duration(seconds: 10), this.markCompleteWhenTimeLeft = const Duration(seconds: 15), this.configurePlayerForEveryBook = true}): _speedOptions = speedOptions; + factory _PlayerSettings.fromJson(Map json) => _$PlayerSettingsFromJson(json); - @override - @JsonKey() - final MinimizedPlayerSettings miniPlayerSettings; - @override - @JsonKey() - final ExpandedPlayerSettings expandedPlayerSettings; - @override - @JsonKey() - final double preferredDefaultVolume; - @override - @JsonKey() - final double preferredDefaultSpeed; - final List _speedOptions; - @override - @JsonKey() - List get speedOptions { - if (_speedOptions is EqualUnmodifiableListView) return _speedOptions; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_speedOptions); - } - - @override - @JsonKey() - final SleepTimerSettings sleepTimerSettings; - @override - @JsonKey() - final Duration playbackReportInterval; - @override - @JsonKey() - final bool configurePlayerForEveryBook; - - @override - String toString() { - return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, playbackReportInterval: $playbackReportInterval, configurePlayerForEveryBook: $configurePlayerForEveryBook)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$PlayerSettingsImpl && - (identical(other.miniPlayerSettings, miniPlayerSettings) || - other.miniPlayerSettings == miniPlayerSettings) && - (identical(other.expandedPlayerSettings, expandedPlayerSettings) || - other.expandedPlayerSettings == expandedPlayerSettings) && - (identical(other.preferredDefaultVolume, preferredDefaultVolume) || - other.preferredDefaultVolume == preferredDefaultVolume) && - (identical(other.preferredDefaultSpeed, preferredDefaultSpeed) || - other.preferredDefaultSpeed == preferredDefaultSpeed) && - const DeepCollectionEquality() - .equals(other._speedOptions, _speedOptions) && - (identical(other.sleepTimerSettings, sleepTimerSettings) || - other.sleepTimerSettings == sleepTimerSettings) && - (identical(other.playbackReportInterval, playbackReportInterval) || - other.playbackReportInterval == playbackReportInterval) && - (identical(other.configurePlayerForEveryBook, - configurePlayerForEveryBook) || - other.configurePlayerForEveryBook == - configurePlayerForEveryBook)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - miniPlayerSettings, - expandedPlayerSettings, - preferredDefaultVolume, - preferredDefaultSpeed, - const DeepCollectionEquality().hash(_speedOptions), - sleepTimerSettings, - playbackReportInterval, - configurePlayerForEveryBook); - - /// Create a copy of PlayerSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$PlayerSettingsImplCopyWith<_$PlayerSettingsImpl> get copyWith => - __$$PlayerSettingsImplCopyWithImpl<_$PlayerSettingsImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$PlayerSettingsImplToJson( - this, - ); - } +@override@JsonKey() final MinimizedPlayerSettings miniPlayerSettings; +@override@JsonKey() final ExpandedPlayerSettings expandedPlayerSettings; +@override@JsonKey() final double preferredDefaultVolume; +@override@JsonKey() final double preferredDefaultSpeed; + final List _speedOptions; +@override@JsonKey() List get speedOptions { + if (_speedOptions is EqualUnmodifiableListView) return _speedOptions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_speedOptions); } -abstract class _PlayerSettings implements PlayerSettings { - const factory _PlayerSettings( - {final MinimizedPlayerSettings miniPlayerSettings, - final ExpandedPlayerSettings expandedPlayerSettings, - final double preferredDefaultVolume, - final double preferredDefaultSpeed, - final List speedOptions, - final SleepTimerSettings sleepTimerSettings, - final Duration playbackReportInterval, - final bool configurePlayerForEveryBook}) = _$PlayerSettingsImpl; +@override@JsonKey() final double speedIncrement; +@override@JsonKey() final double minSpeed; +@override@JsonKey() final double maxSpeed; +@override@JsonKey() final Duration minimumPositionForReporting; +@override@JsonKey() final Duration playbackReportInterval; +@override@JsonKey() final Duration markCompleteWhenTimeLeft; +@override@JsonKey() final bool configurePlayerForEveryBook; - factory _PlayerSettings.fromJson(Map json) = - _$PlayerSettingsImpl.fromJson; +/// Create a copy of PlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$PlayerSettingsCopyWith<_PlayerSettings> get copyWith => __$PlayerSettingsCopyWithImpl<_PlayerSettings>(this, _$identity); - @override - MinimizedPlayerSettings get miniPlayerSettings; - @override - ExpandedPlayerSettings get expandedPlayerSettings; - @override - double get preferredDefaultVolume; - @override - double get preferredDefaultSpeed; - @override - List get speedOptions; - @override - SleepTimerSettings get sleepTimerSettings; - @override - Duration get playbackReportInterval; - @override - bool get configurePlayerForEveryBook; - - /// Create a copy of PlayerSettings - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$PlayerSettingsImplCopyWith<_$PlayerSettingsImpl> get copyWith => - throw _privateConstructorUsedError; +@override +Map toJson() { + return _$PlayerSettingsToJson(this, ); } -ExpandedPlayerSettings _$ExpandedPlayerSettingsFromJson( - Map json) { - return _ExpandedPlayerSettings.fromJson(json); +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _PlayerSettings&&(identical(other.miniPlayerSettings, miniPlayerSettings) || other.miniPlayerSettings == miniPlayerSettings)&&(identical(other.expandedPlayerSettings, expandedPlayerSettings) || other.expandedPlayerSettings == expandedPlayerSettings)&&(identical(other.preferredDefaultVolume, preferredDefaultVolume) || other.preferredDefaultVolume == preferredDefaultVolume)&&(identical(other.preferredDefaultSpeed, preferredDefaultSpeed) || other.preferredDefaultSpeed == preferredDefaultSpeed)&&const DeepCollectionEquality().equals(other._speedOptions, _speedOptions)&&(identical(other.speedIncrement, speedIncrement) || other.speedIncrement == speedIncrement)&&(identical(other.minSpeed, minSpeed) || other.minSpeed == minSpeed)&&(identical(other.maxSpeed, maxSpeed) || other.maxSpeed == maxSpeed)&&(identical(other.minimumPositionForReporting, minimumPositionForReporting) || other.minimumPositionForReporting == minimumPositionForReporting)&&(identical(other.playbackReportInterval, playbackReportInterval) || other.playbackReportInterval == playbackReportInterval)&&(identical(other.markCompleteWhenTimeLeft, markCompleteWhenTimeLeft) || other.markCompleteWhenTimeLeft == markCompleteWhenTimeLeft)&&(identical(other.configurePlayerForEveryBook, configurePlayerForEveryBook) || other.configurePlayerForEveryBook == configurePlayerForEveryBook)); } +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,miniPlayerSettings,expandedPlayerSettings,preferredDefaultVolume,preferredDefaultSpeed,const DeepCollectionEquality().hash(_speedOptions),speedIncrement,minSpeed,maxSpeed,minimumPositionForReporting,playbackReportInterval,markCompleteWhenTimeLeft,configurePlayerForEveryBook); + +@override +String toString() { + return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, speedIncrement: $speedIncrement, minSpeed: $minSpeed, maxSpeed: $maxSpeed, minimumPositionForReporting: $minimumPositionForReporting, playbackReportInterval: $playbackReportInterval, markCompleteWhenTimeLeft: $markCompleteWhenTimeLeft, configurePlayerForEveryBook: $configurePlayerForEveryBook)'; +} + + +} + +/// @nodoc +abstract mixin class _$PlayerSettingsCopyWith<$Res> implements $PlayerSettingsCopyWith<$Res> { + factory _$PlayerSettingsCopyWith(_PlayerSettings value, $Res Function(_PlayerSettings) _then) = __$PlayerSettingsCopyWithImpl; +@override @useResult +$Res call({ + MinimizedPlayerSettings miniPlayerSettings, ExpandedPlayerSettings expandedPlayerSettings, double preferredDefaultVolume, double preferredDefaultSpeed, List speedOptions, double speedIncrement, double minSpeed, double maxSpeed, Duration minimumPositionForReporting, Duration playbackReportInterval, Duration markCompleteWhenTimeLeft, bool configurePlayerForEveryBook +}); + + +@override $MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings;@override $ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings; + +} +/// @nodoc +class __$PlayerSettingsCopyWithImpl<$Res> + implements _$PlayerSettingsCopyWith<$Res> { + __$PlayerSettingsCopyWithImpl(this._self, this._then); + + final _PlayerSettings _self; + final $Res Function(_PlayerSettings) _then; + +/// Create a copy of PlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? miniPlayerSettings = null,Object? expandedPlayerSettings = null,Object? preferredDefaultVolume = null,Object? preferredDefaultSpeed = null,Object? speedOptions = null,Object? speedIncrement = null,Object? minSpeed = null,Object? maxSpeed = null,Object? minimumPositionForReporting = null,Object? playbackReportInterval = null,Object? markCompleteWhenTimeLeft = null,Object? configurePlayerForEveryBook = null,}) { + return _then(_PlayerSettings( +miniPlayerSettings: null == miniPlayerSettings ? _self.miniPlayerSettings : miniPlayerSettings // ignore: cast_nullable_to_non_nullable +as MinimizedPlayerSettings,expandedPlayerSettings: null == expandedPlayerSettings ? _self.expandedPlayerSettings : expandedPlayerSettings // ignore: cast_nullable_to_non_nullable +as ExpandedPlayerSettings,preferredDefaultVolume: null == preferredDefaultVolume ? _self.preferredDefaultVolume : preferredDefaultVolume // ignore: cast_nullable_to_non_nullable +as double,preferredDefaultSpeed: null == preferredDefaultSpeed ? _self.preferredDefaultSpeed : preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable +as double,speedOptions: null == speedOptions ? _self._speedOptions : speedOptions // ignore: cast_nullable_to_non_nullable +as List,speedIncrement: null == speedIncrement ? _self.speedIncrement : speedIncrement // ignore: cast_nullable_to_non_nullable +as double,minSpeed: null == minSpeed ? _self.minSpeed : minSpeed // ignore: cast_nullable_to_non_nullable +as double,maxSpeed: null == maxSpeed ? _self.maxSpeed : maxSpeed // ignore: cast_nullable_to_non_nullable +as double,minimumPositionForReporting: null == minimumPositionForReporting ? _self.minimumPositionForReporting : minimumPositionForReporting // ignore: cast_nullable_to_non_nullable +as Duration,playbackReportInterval: null == playbackReportInterval ? _self.playbackReportInterval : playbackReportInterval // ignore: cast_nullable_to_non_nullable +as Duration,markCompleteWhenTimeLeft: null == markCompleteWhenTimeLeft ? _self.markCompleteWhenTimeLeft : markCompleteWhenTimeLeft // ignore: cast_nullable_to_non_nullable +as Duration,configurePlayerForEveryBook: null == configurePlayerForEveryBook ? _self.configurePlayerForEveryBook : configurePlayerForEveryBook // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +/// Create a copy of PlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings { + + return $MinimizedPlayerSettingsCopyWith<$Res>(_self.miniPlayerSettings, (value) { + return _then(_self.copyWith(miniPlayerSettings: value)); + }); +}/// Create a copy of PlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings { + + return $ExpandedPlayerSettingsCopyWith<$Res>(_self.expandedPlayerSettings, (value) { + return _then(_self.copyWith(expandedPlayerSettings: value)); + }); +} +} + + /// @nodoc mixin _$ExpandedPlayerSettings { - bool get showTotalProgress => throw _privateConstructorUsedError; - bool get showChapterProgress => throw _privateConstructorUsedError; + + bool get showTotalProgress; bool get showChapterProgress; +/// Create a copy of ExpandedPlayerSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ExpandedPlayerSettingsCopyWith get copyWith => _$ExpandedPlayerSettingsCopyWithImpl(this as ExpandedPlayerSettings, _$identity); /// Serializes this ExpandedPlayerSettings to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ExpandedPlayerSettings&&(identical(other.showTotalProgress, showTotalProgress) || other.showTotalProgress == showTotalProgress)&&(identical(other.showChapterProgress, showChapterProgress) || other.showChapterProgress == showChapterProgress)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,showTotalProgress,showChapterProgress); + +@override +String toString() { + return 'ExpandedPlayerSettings(showTotalProgress: $showTotalProgress, showChapterProgress: $showChapterProgress)'; +} + - /// Create a copy of ExpandedPlayerSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $ExpandedPlayerSettingsCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $ExpandedPlayerSettingsCopyWith<$Res> { - factory $ExpandedPlayerSettingsCopyWith(ExpandedPlayerSettings value, - $Res Function(ExpandedPlayerSettings) then) = - _$ExpandedPlayerSettingsCopyWithImpl<$Res, ExpandedPlayerSettings>; - @useResult - $Res call({bool showTotalProgress, bool showChapterProgress}); -} +abstract mixin class $ExpandedPlayerSettingsCopyWith<$Res> { + factory $ExpandedPlayerSettingsCopyWith(ExpandedPlayerSettings value, $Res Function(ExpandedPlayerSettings) _then) = _$ExpandedPlayerSettingsCopyWithImpl; +@useResult +$Res call({ + bool showTotalProgress, bool showChapterProgress +}); + + + +} /// @nodoc -class _$ExpandedPlayerSettingsCopyWithImpl<$Res, - $Val extends ExpandedPlayerSettings> +class _$ExpandedPlayerSettingsCopyWithImpl<$Res> implements $ExpandedPlayerSettingsCopyWith<$Res> { - _$ExpandedPlayerSettingsCopyWithImpl(this._value, this._then); + _$ExpandedPlayerSettingsCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final ExpandedPlayerSettings _self; + final $Res Function(ExpandedPlayerSettings) _then; - /// Create a copy of ExpandedPlayerSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? showTotalProgress = null, - Object? showChapterProgress = null, - }) { - return _then(_value.copyWith( - showTotalProgress: null == showTotalProgress - ? _value.showTotalProgress - : showTotalProgress // ignore: cast_nullable_to_non_nullable - as bool, - showChapterProgress: null == showChapterProgress - ? _value.showChapterProgress - : showChapterProgress // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } +/// Create a copy of ExpandedPlayerSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? showTotalProgress = null,Object? showChapterProgress = null,}) { + return _then(_self.copyWith( +showTotalProgress: null == showTotalProgress ? _self.showTotalProgress : showTotalProgress // ignore: cast_nullable_to_non_nullable +as bool,showChapterProgress: null == showChapterProgress ? _self.showChapterProgress : showChapterProgress // ignore: cast_nullable_to_non_nullable +as bool, + )); } -/// @nodoc -abstract class _$$ExpandedPlayerSettingsImplCopyWith<$Res> - implements $ExpandedPlayerSettingsCopyWith<$Res> { - factory _$$ExpandedPlayerSettingsImplCopyWith( - _$ExpandedPlayerSettingsImpl value, - $Res Function(_$ExpandedPlayerSettingsImpl) then) = - __$$ExpandedPlayerSettingsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({bool showTotalProgress, bool showChapterProgress}); } -/// @nodoc -class __$$ExpandedPlayerSettingsImplCopyWithImpl<$Res> - extends _$ExpandedPlayerSettingsCopyWithImpl<$Res, - _$ExpandedPlayerSettingsImpl> - implements _$$ExpandedPlayerSettingsImplCopyWith<$Res> { - __$$ExpandedPlayerSettingsImplCopyWithImpl( - _$ExpandedPlayerSettingsImpl _value, - $Res Function(_$ExpandedPlayerSettingsImpl) _then) - : super(_value, _then); - /// Create a copy of ExpandedPlayerSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? showTotalProgress = null, - Object? showChapterProgress = null, - }) { - return _then(_$ExpandedPlayerSettingsImpl( - showTotalProgress: null == showTotalProgress - ? _value.showTotalProgress - : showTotalProgress // ignore: cast_nullable_to_non_nullable - as bool, - showChapterProgress: null == showChapterProgress - ? _value.showChapterProgress - : showChapterProgress // ignore: cast_nullable_to_non_nullable - as bool, - )); - } +/// Adds pattern-matching-related methods to [ExpandedPlayerSettings]. +extension ExpandedPlayerSettingsPatterns on ExpandedPlayerSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ExpandedPlayerSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ExpandedPlayerSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ExpandedPlayerSettings value) $default,){ +final _that = this; +switch (_that) { +case _ExpandedPlayerSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ExpandedPlayerSettings value)? $default,){ +final _that = this; +switch (_that) { +case _ExpandedPlayerSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool showTotalProgress, bool showChapterProgress)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ExpandedPlayerSettings() when $default != null: +return $default(_that.showTotalProgress,_that.showChapterProgress);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( bool showTotalProgress, bool showChapterProgress) $default,) {final _that = this; +switch (_that) { +case _ExpandedPlayerSettings(): +return $default(_that.showTotalProgress,_that.showChapterProgress);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool showTotalProgress, bool showChapterProgress)? $default,) {final _that = this; +switch (_that) { +case _ExpandedPlayerSettings() when $default != null: +return $default(_that.showTotalProgress,_that.showChapterProgress);case _: + return null; + +} +} + } /// @nodoc @JsonSerializable() -class _$ExpandedPlayerSettingsImpl implements _ExpandedPlayerSettings { - const _$ExpandedPlayerSettingsImpl( - {this.showTotalProgress = false, this.showChapterProgress = true}); - factory _$ExpandedPlayerSettingsImpl.fromJson(Map json) => - _$$ExpandedPlayerSettingsImplFromJson(json); +class _ExpandedPlayerSettings implements ExpandedPlayerSettings { + const _ExpandedPlayerSettings({this.showTotalProgress = false, this.showChapterProgress = true}); + factory _ExpandedPlayerSettings.fromJson(Map json) => _$ExpandedPlayerSettingsFromJson(json); - @override - @JsonKey() - final bool showTotalProgress; - @override - @JsonKey() - final bool showChapterProgress; +@override@JsonKey() final bool showTotalProgress; +@override@JsonKey() final bool showChapterProgress; - @override - String toString() { - return 'ExpandedPlayerSettings(showTotalProgress: $showTotalProgress, showChapterProgress: $showChapterProgress)'; - } +/// Create a copy of ExpandedPlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ExpandedPlayerSettingsCopyWith<_ExpandedPlayerSettings> get copyWith => __$ExpandedPlayerSettingsCopyWithImpl<_ExpandedPlayerSettings>(this, _$identity); - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$ExpandedPlayerSettingsImpl && - (identical(other.showTotalProgress, showTotalProgress) || - other.showTotalProgress == showTotalProgress) && - (identical(other.showChapterProgress, showChapterProgress) || - other.showChapterProgress == showChapterProgress)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => - Object.hash(runtimeType, showTotalProgress, showChapterProgress); - - /// Create a copy of ExpandedPlayerSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$ExpandedPlayerSettingsImplCopyWith<_$ExpandedPlayerSettingsImpl> - get copyWith => __$$ExpandedPlayerSettingsImplCopyWithImpl< - _$ExpandedPlayerSettingsImpl>(this, _$identity); - - @override - Map toJson() { - return _$$ExpandedPlayerSettingsImplToJson( - this, - ); - } +@override +Map toJson() { + return _$ExpandedPlayerSettingsToJson(this, ); } -abstract class _ExpandedPlayerSettings implements ExpandedPlayerSettings { - const factory _ExpandedPlayerSettings( - {final bool showTotalProgress, - final bool showChapterProgress}) = _$ExpandedPlayerSettingsImpl; - - factory _ExpandedPlayerSettings.fromJson(Map json) = - _$ExpandedPlayerSettingsImpl.fromJson; - - @override - bool get showTotalProgress; - @override - bool get showChapterProgress; - - /// Create a copy of ExpandedPlayerSettings - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$ExpandedPlayerSettingsImplCopyWith<_$ExpandedPlayerSettingsImpl> - get copyWith => throw _privateConstructorUsedError; +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ExpandedPlayerSettings&&(identical(other.showTotalProgress, showTotalProgress) || other.showTotalProgress == showTotalProgress)&&(identical(other.showChapterProgress, showChapterProgress) || other.showChapterProgress == showChapterProgress)); } -MinimizedPlayerSettings _$MinimizedPlayerSettingsFromJson( - Map json) { - return _MinimizedPlayerSettings.fromJson(json); +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,showTotalProgress,showChapterProgress); + +@override +String toString() { + return 'ExpandedPlayerSettings(showTotalProgress: $showTotalProgress, showChapterProgress: $showChapterProgress)'; } + +} + +/// @nodoc +abstract mixin class _$ExpandedPlayerSettingsCopyWith<$Res> implements $ExpandedPlayerSettingsCopyWith<$Res> { + factory _$ExpandedPlayerSettingsCopyWith(_ExpandedPlayerSettings value, $Res Function(_ExpandedPlayerSettings) _then) = __$ExpandedPlayerSettingsCopyWithImpl; +@override @useResult +$Res call({ + bool showTotalProgress, bool showChapterProgress +}); + + + + +} +/// @nodoc +class __$ExpandedPlayerSettingsCopyWithImpl<$Res> + implements _$ExpandedPlayerSettingsCopyWith<$Res> { + __$ExpandedPlayerSettingsCopyWithImpl(this._self, this._then); + + final _ExpandedPlayerSettings _self; + final $Res Function(_ExpandedPlayerSettings) _then; + +/// Create a copy of ExpandedPlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? showTotalProgress = null,Object? showChapterProgress = null,}) { + return _then(_ExpandedPlayerSettings( +showTotalProgress: null == showTotalProgress ? _self.showTotalProgress : showTotalProgress // ignore: cast_nullable_to_non_nullable +as bool,showChapterProgress: null == showChapterProgress ? _self.showChapterProgress : showChapterProgress // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + + /// @nodoc mixin _$MinimizedPlayerSettings { - bool get useChapterInfo => throw _privateConstructorUsedError; + + bool get useChapterInfo; +/// Create a copy of MinimizedPlayerSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$MinimizedPlayerSettingsCopyWith get copyWith => _$MinimizedPlayerSettingsCopyWithImpl(this as MinimizedPlayerSettings, _$identity); /// Serializes this MinimizedPlayerSettings to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is MinimizedPlayerSettings&&(identical(other.useChapterInfo, useChapterInfo) || other.useChapterInfo == useChapterInfo)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,useChapterInfo); + +@override +String toString() { + return 'MinimizedPlayerSettings(useChapterInfo: $useChapterInfo)'; +} + - /// Create a copy of MinimizedPlayerSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $MinimizedPlayerSettingsCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $MinimizedPlayerSettingsCopyWith<$Res> { - factory $MinimizedPlayerSettingsCopyWith(MinimizedPlayerSettings value, - $Res Function(MinimizedPlayerSettings) then) = - _$MinimizedPlayerSettingsCopyWithImpl<$Res, MinimizedPlayerSettings>; - @useResult - $Res call({bool useChapterInfo}); -} +abstract mixin class $MinimizedPlayerSettingsCopyWith<$Res> { + factory $MinimizedPlayerSettingsCopyWith(MinimizedPlayerSettings value, $Res Function(MinimizedPlayerSettings) _then) = _$MinimizedPlayerSettingsCopyWithImpl; +@useResult +$Res call({ + bool useChapterInfo +}); + + + +} /// @nodoc -class _$MinimizedPlayerSettingsCopyWithImpl<$Res, - $Val extends MinimizedPlayerSettings> +class _$MinimizedPlayerSettingsCopyWithImpl<$Res> implements $MinimizedPlayerSettingsCopyWith<$Res> { - _$MinimizedPlayerSettingsCopyWithImpl(this._value, this._then); + _$MinimizedPlayerSettingsCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final MinimizedPlayerSettings _self; + final $Res Function(MinimizedPlayerSettings) _then; - /// Create a copy of MinimizedPlayerSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? useChapterInfo = null, - }) { - return _then(_value.copyWith( - useChapterInfo: null == useChapterInfo - ? _value.useChapterInfo - : useChapterInfo // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } +/// Create a copy of MinimizedPlayerSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? useChapterInfo = null,}) { + return _then(_self.copyWith( +useChapterInfo: null == useChapterInfo ? _self.useChapterInfo : useChapterInfo // ignore: cast_nullable_to_non_nullable +as bool, + )); } -/// @nodoc -abstract class _$$MinimizedPlayerSettingsImplCopyWith<$Res> - implements $MinimizedPlayerSettingsCopyWith<$Res> { - factory _$$MinimizedPlayerSettingsImplCopyWith( - _$MinimizedPlayerSettingsImpl value, - $Res Function(_$MinimizedPlayerSettingsImpl) then) = - __$$MinimizedPlayerSettingsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({bool useChapterInfo}); } -/// @nodoc -class __$$MinimizedPlayerSettingsImplCopyWithImpl<$Res> - extends _$MinimizedPlayerSettingsCopyWithImpl<$Res, - _$MinimizedPlayerSettingsImpl> - implements _$$MinimizedPlayerSettingsImplCopyWith<$Res> { - __$$MinimizedPlayerSettingsImplCopyWithImpl( - _$MinimizedPlayerSettingsImpl _value, - $Res Function(_$MinimizedPlayerSettingsImpl) _then) - : super(_value, _then); - /// Create a copy of MinimizedPlayerSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? useChapterInfo = null, - }) { - return _then(_$MinimizedPlayerSettingsImpl( - useChapterInfo: null == useChapterInfo - ? _value.useChapterInfo - : useChapterInfo // ignore: cast_nullable_to_non_nullable - as bool, - )); - } +/// Adds pattern-matching-related methods to [MinimizedPlayerSettings]. +extension MinimizedPlayerSettingsPatterns on MinimizedPlayerSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _MinimizedPlayerSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _MinimizedPlayerSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _MinimizedPlayerSettings value) $default,){ +final _that = this; +switch (_that) { +case _MinimizedPlayerSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _MinimizedPlayerSettings value)? $default,){ +final _that = this; +switch (_that) { +case _MinimizedPlayerSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool useChapterInfo)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _MinimizedPlayerSettings() when $default != null: +return $default(_that.useChapterInfo);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( bool useChapterInfo) $default,) {final _that = this; +switch (_that) { +case _MinimizedPlayerSettings(): +return $default(_that.useChapterInfo);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool useChapterInfo)? $default,) {final _that = this; +switch (_that) { +case _MinimizedPlayerSettings() when $default != null: +return $default(_that.useChapterInfo);case _: + return null; + +} +} + } /// @nodoc @JsonSerializable() -class _$MinimizedPlayerSettingsImpl implements _MinimizedPlayerSettings { - const _$MinimizedPlayerSettingsImpl({this.useChapterInfo = false}); - factory _$MinimizedPlayerSettingsImpl.fromJson(Map json) => - _$$MinimizedPlayerSettingsImplFromJson(json); +class _MinimizedPlayerSettings implements MinimizedPlayerSettings { + const _MinimizedPlayerSettings({this.useChapterInfo = false}); + factory _MinimizedPlayerSettings.fromJson(Map json) => _$MinimizedPlayerSettingsFromJson(json); - @override - @JsonKey() - final bool useChapterInfo; +@override@JsonKey() final bool useChapterInfo; - @override - String toString() { - return 'MinimizedPlayerSettings(useChapterInfo: $useChapterInfo)'; - } +/// Create a copy of MinimizedPlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$MinimizedPlayerSettingsCopyWith<_MinimizedPlayerSettings> get copyWith => __$MinimizedPlayerSettingsCopyWithImpl<_MinimizedPlayerSettings>(this, _$identity); - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$MinimizedPlayerSettingsImpl && - (identical(other.useChapterInfo, useChapterInfo) || - other.useChapterInfo == useChapterInfo)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, useChapterInfo); - - /// Create a copy of MinimizedPlayerSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$MinimizedPlayerSettingsImplCopyWith<_$MinimizedPlayerSettingsImpl> - get copyWith => __$$MinimizedPlayerSettingsImplCopyWithImpl< - _$MinimizedPlayerSettingsImpl>(this, _$identity); - - @override - Map toJson() { - return _$$MinimizedPlayerSettingsImplToJson( - this, - ); - } +@override +Map toJson() { + return _$MinimizedPlayerSettingsToJson(this, ); } -abstract class _MinimizedPlayerSettings implements MinimizedPlayerSettings { - const factory _MinimizedPlayerSettings({final bool useChapterInfo}) = - _$MinimizedPlayerSettingsImpl; - - factory _MinimizedPlayerSettings.fromJson(Map json) = - _$MinimizedPlayerSettingsImpl.fromJson; - - @override - bool get useChapterInfo; - - /// Create a copy of MinimizedPlayerSettings - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$MinimizedPlayerSettingsImplCopyWith<_$MinimizedPlayerSettingsImpl> - get copyWith => throw _privateConstructorUsedError; +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MinimizedPlayerSettings&&(identical(other.useChapterInfo, useChapterInfo) || other.useChapterInfo == useChapterInfo)); } -SleepTimerSettings _$SleepTimerSettingsFromJson(Map json) { - return _SleepTimerSettings.fromJson(json); +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,useChapterInfo); + +@override +String toString() { + return 'MinimizedPlayerSettings(useChapterInfo: $useChapterInfo)'; } + +} + +/// @nodoc +abstract mixin class _$MinimizedPlayerSettingsCopyWith<$Res> implements $MinimizedPlayerSettingsCopyWith<$Res> { + factory _$MinimizedPlayerSettingsCopyWith(_MinimizedPlayerSettings value, $Res Function(_MinimizedPlayerSettings) _then) = __$MinimizedPlayerSettingsCopyWithImpl; +@override @useResult +$Res call({ + bool useChapterInfo +}); + + + + +} +/// @nodoc +class __$MinimizedPlayerSettingsCopyWithImpl<$Res> + implements _$MinimizedPlayerSettingsCopyWith<$Res> { + __$MinimizedPlayerSettingsCopyWithImpl(this._self, this._then); + + final _MinimizedPlayerSettings _self; + final $Res Function(_MinimizedPlayerSettings) _then; + +/// Create a copy of MinimizedPlayerSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? useChapterInfo = null,}) { + return _then(_MinimizedPlayerSettings( +useChapterInfo: null == useChapterInfo ? _self.useChapterInfo : useChapterInfo // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + + /// @nodoc mixin _$SleepTimerSettings { - Duration get defaultDuration => throw _privateConstructorUsedError; - SleepTimerShakeSenseMode get shakeSenseMode => - throw _privateConstructorUsedError; - /// the duration in which the shake is detected before the end of the timer and after the timer ends - /// only used if [shakeSenseMode] is [SleepTimerShakeSenseMode.nearEnds] - Duration get shakeSenseDuration => throw _privateConstructorUsedError; - bool get vibrateWhenReset => throw _privateConstructorUsedError; - bool get beepWhenReset => throw _privateConstructorUsedError; - bool get fadeOutAudio => throw _privateConstructorUsedError; - double get shakeDetectThreshold => throw _privateConstructorUsedError; - - /// if true, the player will automatically rewind the audio when the sleep timer is stopped - bool get autoRewindWhenStopped => throw _privateConstructorUsedError; - - /// the key is the duration in minutes - Map get autoRewindDurations => - throw _privateConstructorUsedError; - - /// auto turn on timer settings - bool get autoTurnOnTimer => throw _privateConstructorUsedError; - - /// always auto turn on timer settings or during specific times - bool get alwaysAutoTurnOnTimer => throw _privateConstructorUsedError; - - /// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false - /// - /// duration is the time from 00:00 - Duration get autoTurnOnTime => throw _privateConstructorUsedError; - Duration get autoTurnOffTime => throw _privateConstructorUsedError; + Duration get defaultDuration; List get presetDurations; Duration get maxDuration; bool get fadeOutAudio; Duration get fadeOutDuration;/// if true, the player will automatically rewind the audio when the sleep timer is stopped + bool get autoRewindWhenStopped;/// the key is the duration in minutes + Map get autoRewindDurations;/// auto turn on timer settings + bool get autoTurnOnTimer;/// always auto turn on timer settings or during specific times + bool get alwaysAutoTurnOnTimer;/// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false +/// +/// duration is the time from 00:00 + Duration get autoTurnOnTime; Duration get autoTurnOffTime; +/// Create a copy of SleepTimerSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$SleepTimerSettingsCopyWith get copyWith => _$SleepTimerSettingsCopyWithImpl(this as SleepTimerSettings, _$identity); /// Serializes this SleepTimerSettings to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is SleepTimerSettings&&(identical(other.defaultDuration, defaultDuration) || other.defaultDuration == defaultDuration)&&const DeepCollectionEquality().equals(other.presetDurations, presetDurations)&&(identical(other.maxDuration, maxDuration) || other.maxDuration == maxDuration)&&(identical(other.fadeOutAudio, fadeOutAudio) || other.fadeOutAudio == fadeOutAudio)&&(identical(other.fadeOutDuration, fadeOutDuration) || other.fadeOutDuration == fadeOutDuration)&&(identical(other.autoRewindWhenStopped, autoRewindWhenStopped) || other.autoRewindWhenStopped == autoRewindWhenStopped)&&const DeepCollectionEquality().equals(other.autoRewindDurations, autoRewindDurations)&&(identical(other.autoTurnOnTimer, autoTurnOnTimer) || other.autoTurnOnTimer == autoTurnOnTimer)&&(identical(other.alwaysAutoTurnOnTimer, alwaysAutoTurnOnTimer) || other.alwaysAutoTurnOnTimer == alwaysAutoTurnOnTimer)&&(identical(other.autoTurnOnTime, autoTurnOnTime) || other.autoTurnOnTime == autoTurnOnTime)&&(identical(other.autoTurnOffTime, autoTurnOffTime) || other.autoTurnOffTime == autoTurnOffTime)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,defaultDuration,const DeepCollectionEquality().hash(presetDurations),maxDuration,fadeOutAudio,fadeOutDuration,autoRewindWhenStopped,const DeepCollectionEquality().hash(autoRewindDurations),autoTurnOnTimer,alwaysAutoTurnOnTimer,autoTurnOnTime,autoTurnOffTime); + +@override +String toString() { + return 'SleepTimerSettings(defaultDuration: $defaultDuration, presetDurations: $presetDurations, maxDuration: $maxDuration, fadeOutAudio: $fadeOutAudio, fadeOutDuration: $fadeOutDuration, autoRewindWhenStopped: $autoRewindWhenStopped, autoRewindDurations: $autoRewindDurations, autoTurnOnTimer: $autoTurnOnTimer, alwaysAutoTurnOnTimer: $alwaysAutoTurnOnTimer, autoTurnOnTime: $autoTurnOnTime, autoTurnOffTime: $autoTurnOffTime)'; +} + - /// Create a copy of SleepTimerSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $SleepTimerSettingsCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $SleepTimerSettingsCopyWith<$Res> { - factory $SleepTimerSettingsCopyWith( - SleepTimerSettings value, $Res Function(SleepTimerSettings) then) = - _$SleepTimerSettingsCopyWithImpl<$Res, SleepTimerSettings>; - @useResult - $Res call( - {Duration defaultDuration, - SleepTimerShakeSenseMode shakeSenseMode, - Duration shakeSenseDuration, - bool vibrateWhenReset, - bool beepWhenReset, - bool fadeOutAudio, - double shakeDetectThreshold, - bool autoRewindWhenStopped, - Map autoRewindDurations, - bool autoTurnOnTimer, - bool alwaysAutoTurnOnTimer, - Duration autoTurnOnTime, - Duration autoTurnOffTime}); -} +abstract mixin class $SleepTimerSettingsCopyWith<$Res> { + factory $SleepTimerSettingsCopyWith(SleepTimerSettings value, $Res Function(SleepTimerSettings) _then) = _$SleepTimerSettingsCopyWithImpl; +@useResult +$Res call({ + Duration defaultDuration, List presetDurations, Duration maxDuration, bool fadeOutAudio, Duration fadeOutDuration, bool autoRewindWhenStopped, Map autoRewindDurations, bool autoTurnOnTimer, bool alwaysAutoTurnOnTimer, Duration autoTurnOnTime, Duration autoTurnOffTime +}); + + + +} /// @nodoc -class _$SleepTimerSettingsCopyWithImpl<$Res, $Val extends SleepTimerSettings> +class _$SleepTimerSettingsCopyWithImpl<$Res> implements $SleepTimerSettingsCopyWith<$Res> { - _$SleepTimerSettingsCopyWithImpl(this._value, this._then); + _$SleepTimerSettingsCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final SleepTimerSettings _self; + final $Res Function(SleepTimerSettings) _then; - /// Create a copy of SleepTimerSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? defaultDuration = null, - Object? shakeSenseMode = null, - Object? shakeSenseDuration = null, - Object? vibrateWhenReset = null, - Object? beepWhenReset = null, - Object? fadeOutAudio = null, - Object? shakeDetectThreshold = null, - Object? autoRewindWhenStopped = null, - Object? autoRewindDurations = null, - Object? autoTurnOnTimer = null, - Object? alwaysAutoTurnOnTimer = null, - Object? autoTurnOnTime = null, - Object? autoTurnOffTime = null, - }) { - return _then(_value.copyWith( - defaultDuration: null == defaultDuration - ? _value.defaultDuration - : defaultDuration // ignore: cast_nullable_to_non_nullable - as Duration, - shakeSenseMode: null == shakeSenseMode - ? _value.shakeSenseMode - : shakeSenseMode // ignore: cast_nullable_to_non_nullable - as SleepTimerShakeSenseMode, - shakeSenseDuration: null == shakeSenseDuration - ? _value.shakeSenseDuration - : shakeSenseDuration // ignore: cast_nullable_to_non_nullable - as Duration, - vibrateWhenReset: null == vibrateWhenReset - ? _value.vibrateWhenReset - : vibrateWhenReset // ignore: cast_nullable_to_non_nullable - as bool, - beepWhenReset: null == beepWhenReset - ? _value.beepWhenReset - : beepWhenReset // ignore: cast_nullable_to_non_nullable - as bool, - fadeOutAudio: null == fadeOutAudio - ? _value.fadeOutAudio - : fadeOutAudio // ignore: cast_nullable_to_non_nullable - as bool, - shakeDetectThreshold: null == shakeDetectThreshold - ? _value.shakeDetectThreshold - : shakeDetectThreshold // ignore: cast_nullable_to_non_nullable - as double, - autoRewindWhenStopped: null == autoRewindWhenStopped - ? _value.autoRewindWhenStopped - : autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable - as bool, - autoRewindDurations: null == autoRewindDurations - ? _value.autoRewindDurations - : autoRewindDurations // ignore: cast_nullable_to_non_nullable - as Map, - autoTurnOnTimer: null == autoTurnOnTimer - ? _value.autoTurnOnTimer - : autoTurnOnTimer // ignore: cast_nullable_to_non_nullable - as bool, - alwaysAutoTurnOnTimer: null == alwaysAutoTurnOnTimer - ? _value.alwaysAutoTurnOnTimer - : alwaysAutoTurnOnTimer // ignore: cast_nullable_to_non_nullable - as bool, - autoTurnOnTime: null == autoTurnOnTime - ? _value.autoTurnOnTime - : autoTurnOnTime // ignore: cast_nullable_to_non_nullable - as Duration, - autoTurnOffTime: null == autoTurnOffTime - ? _value.autoTurnOffTime - : autoTurnOffTime // ignore: cast_nullable_to_non_nullable - as Duration, - ) as $Val); - } +/// Create a copy of SleepTimerSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? defaultDuration = null,Object? presetDurations = null,Object? maxDuration = null,Object? fadeOutAudio = null,Object? fadeOutDuration = null,Object? autoRewindWhenStopped = null,Object? autoRewindDurations = null,Object? autoTurnOnTimer = null,Object? alwaysAutoTurnOnTimer = null,Object? autoTurnOnTime = null,Object? autoTurnOffTime = null,}) { + return _then(_self.copyWith( +defaultDuration: null == defaultDuration ? _self.defaultDuration : defaultDuration // ignore: cast_nullable_to_non_nullable +as Duration,presetDurations: null == presetDurations ? _self.presetDurations : presetDurations // ignore: cast_nullable_to_non_nullable +as List,maxDuration: null == maxDuration ? _self.maxDuration : maxDuration // ignore: cast_nullable_to_non_nullable +as Duration,fadeOutAudio: null == fadeOutAudio ? _self.fadeOutAudio : fadeOutAudio // ignore: cast_nullable_to_non_nullable +as bool,fadeOutDuration: null == fadeOutDuration ? _self.fadeOutDuration : fadeOutDuration // ignore: cast_nullable_to_non_nullable +as Duration,autoRewindWhenStopped: null == autoRewindWhenStopped ? _self.autoRewindWhenStopped : autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable +as bool,autoRewindDurations: null == autoRewindDurations ? _self.autoRewindDurations : autoRewindDurations // ignore: cast_nullable_to_non_nullable +as Map,autoTurnOnTimer: null == autoTurnOnTimer ? _self.autoTurnOnTimer : autoTurnOnTimer // ignore: cast_nullable_to_non_nullable +as bool,alwaysAutoTurnOnTimer: null == alwaysAutoTurnOnTimer ? _self.alwaysAutoTurnOnTimer : alwaysAutoTurnOnTimer // ignore: cast_nullable_to_non_nullable +as bool,autoTurnOnTime: null == autoTurnOnTime ? _self.autoTurnOnTime : autoTurnOnTime // ignore: cast_nullable_to_non_nullable +as Duration,autoTurnOffTime: null == autoTurnOffTime ? _self.autoTurnOffTime : autoTurnOffTime // ignore: cast_nullable_to_non_nullable +as Duration, + )); } -/// @nodoc -abstract class _$$SleepTimerSettingsImplCopyWith<$Res> - implements $SleepTimerSettingsCopyWith<$Res> { - factory _$$SleepTimerSettingsImplCopyWith(_$SleepTimerSettingsImpl value, - $Res Function(_$SleepTimerSettingsImpl) then) = - __$$SleepTimerSettingsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {Duration defaultDuration, - SleepTimerShakeSenseMode shakeSenseMode, - Duration shakeSenseDuration, - bool vibrateWhenReset, - bool beepWhenReset, - bool fadeOutAudio, - double shakeDetectThreshold, - bool autoRewindWhenStopped, - Map autoRewindDurations, - bool autoTurnOnTimer, - bool alwaysAutoTurnOnTimer, - Duration autoTurnOnTime, - Duration autoTurnOffTime}); } -/// @nodoc -class __$$SleepTimerSettingsImplCopyWithImpl<$Res> - extends _$SleepTimerSettingsCopyWithImpl<$Res, _$SleepTimerSettingsImpl> - implements _$$SleepTimerSettingsImplCopyWith<$Res> { - __$$SleepTimerSettingsImplCopyWithImpl(_$SleepTimerSettingsImpl _value, - $Res Function(_$SleepTimerSettingsImpl) _then) - : super(_value, _then); - /// Create a copy of SleepTimerSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? defaultDuration = null, - Object? shakeSenseMode = null, - Object? shakeSenseDuration = null, - Object? vibrateWhenReset = null, - Object? beepWhenReset = null, - Object? fadeOutAudio = null, - Object? shakeDetectThreshold = null, - Object? autoRewindWhenStopped = null, - Object? autoRewindDurations = null, - Object? autoTurnOnTimer = null, - Object? alwaysAutoTurnOnTimer = null, - Object? autoTurnOnTime = null, - Object? autoTurnOffTime = null, - }) { - return _then(_$SleepTimerSettingsImpl( - defaultDuration: null == defaultDuration - ? _value.defaultDuration - : defaultDuration // ignore: cast_nullable_to_non_nullable - as Duration, - shakeSenseMode: null == shakeSenseMode - ? _value.shakeSenseMode - : shakeSenseMode // ignore: cast_nullable_to_non_nullable - as SleepTimerShakeSenseMode, - shakeSenseDuration: null == shakeSenseDuration - ? _value.shakeSenseDuration - : shakeSenseDuration // ignore: cast_nullable_to_non_nullable - as Duration, - vibrateWhenReset: null == vibrateWhenReset - ? _value.vibrateWhenReset - : vibrateWhenReset // ignore: cast_nullable_to_non_nullable - as bool, - beepWhenReset: null == beepWhenReset - ? _value.beepWhenReset - : beepWhenReset // ignore: cast_nullable_to_non_nullable - as bool, - fadeOutAudio: null == fadeOutAudio - ? _value.fadeOutAudio - : fadeOutAudio // ignore: cast_nullable_to_non_nullable - as bool, - shakeDetectThreshold: null == shakeDetectThreshold - ? _value.shakeDetectThreshold - : shakeDetectThreshold // ignore: cast_nullable_to_non_nullable - as double, - autoRewindWhenStopped: null == autoRewindWhenStopped - ? _value.autoRewindWhenStopped - : autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable - as bool, - autoRewindDurations: null == autoRewindDurations - ? _value._autoRewindDurations - : autoRewindDurations // ignore: cast_nullable_to_non_nullable - as Map, - autoTurnOnTimer: null == autoTurnOnTimer - ? _value.autoTurnOnTimer - : autoTurnOnTimer // ignore: cast_nullable_to_non_nullable - as bool, - alwaysAutoTurnOnTimer: null == alwaysAutoTurnOnTimer - ? _value.alwaysAutoTurnOnTimer - : alwaysAutoTurnOnTimer // ignore: cast_nullable_to_non_nullable - as bool, - autoTurnOnTime: null == autoTurnOnTime - ? _value.autoTurnOnTime - : autoTurnOnTime // ignore: cast_nullable_to_non_nullable - as Duration, - autoTurnOffTime: null == autoTurnOffTime - ? _value.autoTurnOffTime - : autoTurnOffTime // ignore: cast_nullable_to_non_nullable - as Duration, - )); - } +/// Adds pattern-matching-related methods to [SleepTimerSettings]. +extension SleepTimerSettingsPatterns on SleepTimerSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _SleepTimerSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _SleepTimerSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _SleepTimerSettings value) $default,){ +final _that = this; +switch (_that) { +case _SleepTimerSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _SleepTimerSettings value)? $default,){ +final _that = this; +switch (_that) { +case _SleepTimerSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( Duration defaultDuration, List presetDurations, Duration maxDuration, bool fadeOutAudio, Duration fadeOutDuration, bool autoRewindWhenStopped, Map autoRewindDurations, bool autoTurnOnTimer, bool alwaysAutoTurnOnTimer, Duration autoTurnOnTime, Duration autoTurnOffTime)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _SleepTimerSettings() when $default != null: +return $default(_that.defaultDuration,_that.presetDurations,_that.maxDuration,_that.fadeOutAudio,_that.fadeOutDuration,_that.autoRewindWhenStopped,_that.autoRewindDurations,_that.autoTurnOnTimer,_that.alwaysAutoTurnOnTimer,_that.autoTurnOnTime,_that.autoTurnOffTime);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( Duration defaultDuration, List presetDurations, Duration maxDuration, bool fadeOutAudio, Duration fadeOutDuration, bool autoRewindWhenStopped, Map autoRewindDurations, bool autoTurnOnTimer, bool alwaysAutoTurnOnTimer, Duration autoTurnOnTime, Duration autoTurnOffTime) $default,) {final _that = this; +switch (_that) { +case _SleepTimerSettings(): +return $default(_that.defaultDuration,_that.presetDurations,_that.maxDuration,_that.fadeOutAudio,_that.fadeOutDuration,_that.autoRewindWhenStopped,_that.autoRewindDurations,_that.autoTurnOnTimer,_that.alwaysAutoTurnOnTimer,_that.autoTurnOnTime,_that.autoTurnOffTime);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( Duration defaultDuration, List presetDurations, Duration maxDuration, bool fadeOutAudio, Duration fadeOutDuration, bool autoRewindWhenStopped, Map autoRewindDurations, bool autoTurnOnTimer, bool alwaysAutoTurnOnTimer, Duration autoTurnOnTime, Duration autoTurnOffTime)? $default,) {final _that = this; +switch (_that) { +case _SleepTimerSettings() when $default != null: +return $default(_that.defaultDuration,_that.presetDurations,_that.maxDuration,_that.fadeOutAudio,_that.fadeOutDuration,_that.autoRewindWhenStopped,_that.autoRewindDurations,_that.autoTurnOnTimer,_that.alwaysAutoTurnOnTimer,_that.autoTurnOnTime,_that.autoTurnOffTime);case _: + return null; + +} +} + } /// @nodoc @JsonSerializable() -class _$SleepTimerSettingsImpl implements _SleepTimerSettings { - const _$SleepTimerSettingsImpl( - {this.defaultDuration = const Duration(minutes: 15), - this.shakeSenseMode = SleepTimerShakeSenseMode.always, - this.shakeSenseDuration = const Duration(seconds: 30), - this.vibrateWhenReset = true, - this.beepWhenReset = false, - this.fadeOutAudio = false, - this.shakeDetectThreshold = 0.5, - this.autoRewindWhenStopped = false, - final Map autoRewindDurations = const { - 5: Duration(seconds: 10), - 15: Duration(seconds: 30), - 45: Duration(seconds: 45), - 60: Duration(minutes: 1), - 120: Duration(minutes: 2) - }, - this.autoTurnOnTimer = false, - this.alwaysAutoTurnOnTimer = true, - this.autoTurnOnTime = const Duration(hours: 22, minutes: 0), - this.autoTurnOffTime = const Duration(hours: 6, minutes: 0)}) - : _autoRewindDurations = autoRewindDurations; - factory _$SleepTimerSettingsImpl.fromJson(Map json) => - _$$SleepTimerSettingsImplFromJson(json); +class _SleepTimerSettings implements SleepTimerSettings { + const _SleepTimerSettings({this.defaultDuration = const Duration(minutes: 15), final List presetDurations = const [Duration(minutes: 5), Duration(minutes: 10), Duration(minutes: 15), Duration(minutes: 20), Duration(minutes: 30)], this.maxDuration = const Duration(minutes: 100), this.fadeOutAudio = false, this.fadeOutDuration = const Duration(seconds: 20), this.autoRewindWhenStopped = false, final Map autoRewindDurations = const {5 : Duration(seconds: 10), 15 : Duration(seconds: 30), 45 : Duration(seconds: 45), 60 : Duration(minutes: 1), 120 : Duration(minutes: 2)}, this.autoTurnOnTimer = false, this.alwaysAutoTurnOnTimer = false, this.autoTurnOnTime = const Duration(hours: 22, minutes: 0), this.autoTurnOffTime = const Duration(hours: 6, minutes: 0)}): _presetDurations = presetDurations,_autoRewindDurations = autoRewindDurations; + factory _SleepTimerSettings.fromJson(Map json) => _$SleepTimerSettingsFromJson(json); - @override - @JsonKey() - final Duration defaultDuration; - @override - @JsonKey() - final SleepTimerShakeSenseMode shakeSenseMode; - - /// the duration in which the shake is detected before the end of the timer and after the timer ends - /// only used if [shakeSenseMode] is [SleepTimerShakeSenseMode.nearEnds] - @override - @JsonKey() - final Duration shakeSenseDuration; - @override - @JsonKey() - final bool vibrateWhenReset; - @override - @JsonKey() - final bool beepWhenReset; - @override - @JsonKey() - final bool fadeOutAudio; - @override - @JsonKey() - final double shakeDetectThreshold; - - /// if true, the player will automatically rewind the audio when the sleep timer is stopped - @override - @JsonKey() - final bool autoRewindWhenStopped; - - /// the key is the duration in minutes - final Map _autoRewindDurations; - - /// the key is the duration in minutes - @override - @JsonKey() - Map get autoRewindDurations { - if (_autoRewindDurations is EqualUnmodifiableMapView) - return _autoRewindDurations; - // ignore: implicit_dynamic_type - return EqualUnmodifiableMapView(_autoRewindDurations); - } - - /// auto turn on timer settings - @override - @JsonKey() - final bool autoTurnOnTimer; - - /// always auto turn on timer settings or during specific times - @override - @JsonKey() - final bool alwaysAutoTurnOnTimer; - - /// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false - /// - /// duration is the time from 00:00 - @override - @JsonKey() - final Duration autoTurnOnTime; - @override - @JsonKey() - final Duration autoTurnOffTime; - - @override - String toString() { - return 'SleepTimerSettings(defaultDuration: $defaultDuration, shakeSenseMode: $shakeSenseMode, shakeSenseDuration: $shakeSenseDuration, vibrateWhenReset: $vibrateWhenReset, beepWhenReset: $beepWhenReset, fadeOutAudio: $fadeOutAudio, shakeDetectThreshold: $shakeDetectThreshold, autoRewindWhenStopped: $autoRewindWhenStopped, autoRewindDurations: $autoRewindDurations, autoTurnOnTimer: $autoTurnOnTimer, alwaysAutoTurnOnTimer: $alwaysAutoTurnOnTimer, autoTurnOnTime: $autoTurnOnTime, autoTurnOffTime: $autoTurnOffTime)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$SleepTimerSettingsImpl && - (identical(other.defaultDuration, defaultDuration) || - other.defaultDuration == defaultDuration) && - (identical(other.shakeSenseMode, shakeSenseMode) || - other.shakeSenseMode == shakeSenseMode) && - (identical(other.shakeSenseDuration, shakeSenseDuration) || - other.shakeSenseDuration == shakeSenseDuration) && - (identical(other.vibrateWhenReset, vibrateWhenReset) || - other.vibrateWhenReset == vibrateWhenReset) && - (identical(other.beepWhenReset, beepWhenReset) || - other.beepWhenReset == beepWhenReset) && - (identical(other.fadeOutAudio, fadeOutAudio) || - other.fadeOutAudio == fadeOutAudio) && - (identical(other.shakeDetectThreshold, shakeDetectThreshold) || - other.shakeDetectThreshold == shakeDetectThreshold) && - (identical(other.autoRewindWhenStopped, autoRewindWhenStopped) || - other.autoRewindWhenStopped == autoRewindWhenStopped) && - const DeepCollectionEquality() - .equals(other._autoRewindDurations, _autoRewindDurations) && - (identical(other.autoTurnOnTimer, autoTurnOnTimer) || - other.autoTurnOnTimer == autoTurnOnTimer) && - (identical(other.alwaysAutoTurnOnTimer, alwaysAutoTurnOnTimer) || - other.alwaysAutoTurnOnTimer == alwaysAutoTurnOnTimer) && - (identical(other.autoTurnOnTime, autoTurnOnTime) || - other.autoTurnOnTime == autoTurnOnTime) && - (identical(other.autoTurnOffTime, autoTurnOffTime) || - other.autoTurnOffTime == autoTurnOffTime)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash( - runtimeType, - defaultDuration, - shakeSenseMode, - shakeSenseDuration, - vibrateWhenReset, - beepWhenReset, - fadeOutAudio, - shakeDetectThreshold, - autoRewindWhenStopped, - const DeepCollectionEquality().hash(_autoRewindDurations), - autoTurnOnTimer, - alwaysAutoTurnOnTimer, - autoTurnOnTime, - autoTurnOffTime); - - /// Create a copy of SleepTimerSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$SleepTimerSettingsImplCopyWith<_$SleepTimerSettingsImpl> get copyWith => - __$$SleepTimerSettingsImplCopyWithImpl<_$SleepTimerSettingsImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$SleepTimerSettingsImplToJson( - this, - ); - } +@override@JsonKey() final Duration defaultDuration; + final List _presetDurations; +@override@JsonKey() List get presetDurations { + if (_presetDurations is EqualUnmodifiableListView) return _presetDurations; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_presetDurations); } -abstract class _SleepTimerSettings implements SleepTimerSettings { - const factory _SleepTimerSettings( - {final Duration defaultDuration, - final SleepTimerShakeSenseMode shakeSenseMode, - final Duration shakeSenseDuration, - final bool vibrateWhenReset, - final bool beepWhenReset, - final bool fadeOutAudio, - final double shakeDetectThreshold, - final bool autoRewindWhenStopped, - final Map autoRewindDurations, - final bool autoTurnOnTimer, - final bool alwaysAutoTurnOnTimer, - final Duration autoTurnOnTime, - final Duration autoTurnOffTime}) = _$SleepTimerSettingsImpl; - - factory _SleepTimerSettings.fromJson(Map json) = - _$SleepTimerSettingsImpl.fromJson; - - @override - Duration get defaultDuration; - @override - SleepTimerShakeSenseMode get shakeSenseMode; - - /// the duration in which the shake is detected before the end of the timer and after the timer ends - /// only used if [shakeSenseMode] is [SleepTimerShakeSenseMode.nearEnds] - @override - Duration get shakeSenseDuration; - @override - bool get vibrateWhenReset; - @override - bool get beepWhenReset; - @override - bool get fadeOutAudio; - @override - double get shakeDetectThreshold; - - /// if true, the player will automatically rewind the audio when the sleep timer is stopped - @override - bool get autoRewindWhenStopped; - - /// the key is the duration in minutes - @override - Map get autoRewindDurations; - - /// auto turn on timer settings - @override - bool get autoTurnOnTimer; - - /// always auto turn on timer settings or during specific times - @override - bool get alwaysAutoTurnOnTimer; - - /// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false - /// - /// duration is the time from 00:00 - @override - Duration get autoTurnOnTime; - @override - Duration get autoTurnOffTime; - - /// Create a copy of SleepTimerSettings - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$SleepTimerSettingsImplCopyWith<_$SleepTimerSettingsImpl> get copyWith => - throw _privateConstructorUsedError; +@override@JsonKey() final Duration maxDuration; +@override@JsonKey() final bool fadeOutAudio; +@override@JsonKey() final Duration fadeOutDuration; +/// if true, the player will automatically rewind the audio when the sleep timer is stopped +@override@JsonKey() final bool autoRewindWhenStopped; +/// the key is the duration in minutes + final Map _autoRewindDurations; +/// the key is the duration in minutes +@override@JsonKey() Map get autoRewindDurations { + if (_autoRewindDurations is EqualUnmodifiableMapView) return _autoRewindDurations; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_autoRewindDurations); } -DownloadSettings _$DownloadSettingsFromJson(Map json) { - return _DownloadSettings.fromJson(json); +/// auto turn on timer settings +@override@JsonKey() final bool autoTurnOnTimer; +/// always auto turn on timer settings or during specific times +@override@JsonKey() final bool alwaysAutoTurnOnTimer; +/// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false +/// +/// duration is the time from 00:00 +@override@JsonKey() final Duration autoTurnOnTime; +@override@JsonKey() final Duration autoTurnOffTime; + +/// Create a copy of SleepTimerSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$SleepTimerSettingsCopyWith<_SleepTimerSettings> get copyWith => __$SleepTimerSettingsCopyWithImpl<_SleepTimerSettings>(this, _$identity); + +@override +Map toJson() { + return _$SleepTimerSettingsToJson(this, ); } +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _SleepTimerSettings&&(identical(other.defaultDuration, defaultDuration) || other.defaultDuration == defaultDuration)&&const DeepCollectionEquality().equals(other._presetDurations, _presetDurations)&&(identical(other.maxDuration, maxDuration) || other.maxDuration == maxDuration)&&(identical(other.fadeOutAudio, fadeOutAudio) || other.fadeOutAudio == fadeOutAudio)&&(identical(other.fadeOutDuration, fadeOutDuration) || other.fadeOutDuration == fadeOutDuration)&&(identical(other.autoRewindWhenStopped, autoRewindWhenStopped) || other.autoRewindWhenStopped == autoRewindWhenStopped)&&const DeepCollectionEquality().equals(other._autoRewindDurations, _autoRewindDurations)&&(identical(other.autoTurnOnTimer, autoTurnOnTimer) || other.autoTurnOnTimer == autoTurnOnTimer)&&(identical(other.alwaysAutoTurnOnTimer, alwaysAutoTurnOnTimer) || other.alwaysAutoTurnOnTimer == alwaysAutoTurnOnTimer)&&(identical(other.autoTurnOnTime, autoTurnOnTime) || other.autoTurnOnTime == autoTurnOnTime)&&(identical(other.autoTurnOffTime, autoTurnOffTime) || other.autoTurnOffTime == autoTurnOffTime)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,defaultDuration,const DeepCollectionEquality().hash(_presetDurations),maxDuration,fadeOutAudio,fadeOutDuration,autoRewindWhenStopped,const DeepCollectionEquality().hash(_autoRewindDurations),autoTurnOnTimer,alwaysAutoTurnOnTimer,autoTurnOnTime,autoTurnOffTime); + +@override +String toString() { + return 'SleepTimerSettings(defaultDuration: $defaultDuration, presetDurations: $presetDurations, maxDuration: $maxDuration, fadeOutAudio: $fadeOutAudio, fadeOutDuration: $fadeOutDuration, autoRewindWhenStopped: $autoRewindWhenStopped, autoRewindDurations: $autoRewindDurations, autoTurnOnTimer: $autoTurnOnTimer, alwaysAutoTurnOnTimer: $alwaysAutoTurnOnTimer, autoTurnOnTime: $autoTurnOnTime, autoTurnOffTime: $autoTurnOffTime)'; +} + + +} + +/// @nodoc +abstract mixin class _$SleepTimerSettingsCopyWith<$Res> implements $SleepTimerSettingsCopyWith<$Res> { + factory _$SleepTimerSettingsCopyWith(_SleepTimerSettings value, $Res Function(_SleepTimerSettings) _then) = __$SleepTimerSettingsCopyWithImpl; +@override @useResult +$Res call({ + Duration defaultDuration, List presetDurations, Duration maxDuration, bool fadeOutAudio, Duration fadeOutDuration, bool autoRewindWhenStopped, Map autoRewindDurations, bool autoTurnOnTimer, bool alwaysAutoTurnOnTimer, Duration autoTurnOnTime, Duration autoTurnOffTime +}); + + + + +} +/// @nodoc +class __$SleepTimerSettingsCopyWithImpl<$Res> + implements _$SleepTimerSettingsCopyWith<$Res> { + __$SleepTimerSettingsCopyWithImpl(this._self, this._then); + + final _SleepTimerSettings _self; + final $Res Function(_SleepTimerSettings) _then; + +/// Create a copy of SleepTimerSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? defaultDuration = null,Object? presetDurations = null,Object? maxDuration = null,Object? fadeOutAudio = null,Object? fadeOutDuration = null,Object? autoRewindWhenStopped = null,Object? autoRewindDurations = null,Object? autoTurnOnTimer = null,Object? alwaysAutoTurnOnTimer = null,Object? autoTurnOnTime = null,Object? autoTurnOffTime = null,}) { + return _then(_SleepTimerSettings( +defaultDuration: null == defaultDuration ? _self.defaultDuration : defaultDuration // ignore: cast_nullable_to_non_nullable +as Duration,presetDurations: null == presetDurations ? _self._presetDurations : presetDurations // ignore: cast_nullable_to_non_nullable +as List,maxDuration: null == maxDuration ? _self.maxDuration : maxDuration // ignore: cast_nullable_to_non_nullable +as Duration,fadeOutAudio: null == fadeOutAudio ? _self.fadeOutAudio : fadeOutAudio // ignore: cast_nullable_to_non_nullable +as bool,fadeOutDuration: null == fadeOutDuration ? _self.fadeOutDuration : fadeOutDuration // ignore: cast_nullable_to_non_nullable +as Duration,autoRewindWhenStopped: null == autoRewindWhenStopped ? _self.autoRewindWhenStopped : autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable +as bool,autoRewindDurations: null == autoRewindDurations ? _self._autoRewindDurations : autoRewindDurations // ignore: cast_nullable_to_non_nullable +as Map,autoTurnOnTimer: null == autoTurnOnTimer ? _self.autoTurnOnTimer : autoTurnOnTimer // ignore: cast_nullable_to_non_nullable +as bool,alwaysAutoTurnOnTimer: null == alwaysAutoTurnOnTimer ? _self.alwaysAutoTurnOnTimer : alwaysAutoTurnOnTimer // ignore: cast_nullable_to_non_nullable +as bool,autoTurnOnTime: null == autoTurnOnTime ? _self.autoTurnOnTime : autoTurnOnTime // ignore: cast_nullable_to_non_nullable +as Duration,autoTurnOffTime: null == autoTurnOffTime ? _self.autoTurnOffTime : autoTurnOffTime // ignore: cast_nullable_to_non_nullable +as Duration, + )); +} + + +} + + /// @nodoc mixin _$DownloadSettings { - bool get requiresWiFi => throw _privateConstructorUsedError; - int get retries => throw _privateConstructorUsedError; - bool get allowPause => throw _privateConstructorUsedError; - int get maxConcurrent => throw _privateConstructorUsedError; - int get maxConcurrentByHost => throw _privateConstructorUsedError; - int get maxConcurrentByGroup => throw _privateConstructorUsedError; + + bool get requiresWiFi; int get retries; bool get allowPause; int get maxConcurrent; int get maxConcurrentByHost; int get maxConcurrentByGroup; +/// Create a copy of DownloadSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DownloadSettingsCopyWith get copyWith => _$DownloadSettingsCopyWithImpl(this as DownloadSettings, _$identity); /// Serializes this DownloadSettings to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DownloadSettings&&(identical(other.requiresWiFi, requiresWiFi) || other.requiresWiFi == requiresWiFi)&&(identical(other.retries, retries) || other.retries == retries)&&(identical(other.allowPause, allowPause) || other.allowPause == allowPause)&&(identical(other.maxConcurrent, maxConcurrent) || other.maxConcurrent == maxConcurrent)&&(identical(other.maxConcurrentByHost, maxConcurrentByHost) || other.maxConcurrentByHost == maxConcurrentByHost)&&(identical(other.maxConcurrentByGroup, maxConcurrentByGroup) || other.maxConcurrentByGroup == maxConcurrentByGroup)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,requiresWiFi,retries,allowPause,maxConcurrent,maxConcurrentByHost,maxConcurrentByGroup); + +@override +String toString() { + return 'DownloadSettings(requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause, maxConcurrent: $maxConcurrent, maxConcurrentByHost: $maxConcurrentByHost, maxConcurrentByGroup: $maxConcurrentByGroup)'; +} + - /// Create a copy of DownloadSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $DownloadSettingsCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $DownloadSettingsCopyWith<$Res> { - factory $DownloadSettingsCopyWith( - DownloadSettings value, $Res Function(DownloadSettings) then) = - _$DownloadSettingsCopyWithImpl<$Res, DownloadSettings>; - @useResult - $Res call( - {bool requiresWiFi, - int retries, - bool allowPause, - int maxConcurrent, - int maxConcurrentByHost, - int maxConcurrentByGroup}); -} +abstract mixin class $DownloadSettingsCopyWith<$Res> { + factory $DownloadSettingsCopyWith(DownloadSettings value, $Res Function(DownloadSettings) _then) = _$DownloadSettingsCopyWithImpl; +@useResult +$Res call({ + bool requiresWiFi, int retries, bool allowPause, int maxConcurrent, int maxConcurrentByHost, int maxConcurrentByGroup +}); + + + +} /// @nodoc -class _$DownloadSettingsCopyWithImpl<$Res, $Val extends DownloadSettings> +class _$DownloadSettingsCopyWithImpl<$Res> implements $DownloadSettingsCopyWith<$Res> { - _$DownloadSettingsCopyWithImpl(this._value, this._then); + _$DownloadSettingsCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final DownloadSettings _self; + final $Res Function(DownloadSettings) _then; - /// Create a copy of DownloadSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? requiresWiFi = null, - Object? retries = null, - Object? allowPause = null, - Object? maxConcurrent = null, - Object? maxConcurrentByHost = null, - Object? maxConcurrentByGroup = null, - }) { - return _then(_value.copyWith( - requiresWiFi: null == requiresWiFi - ? _value.requiresWiFi - : requiresWiFi // ignore: cast_nullable_to_non_nullable - as bool, - retries: null == retries - ? _value.retries - : retries // ignore: cast_nullable_to_non_nullable - as int, - allowPause: null == allowPause - ? _value.allowPause - : allowPause // ignore: cast_nullable_to_non_nullable - as bool, - maxConcurrent: null == maxConcurrent - ? _value.maxConcurrent - : maxConcurrent // ignore: cast_nullable_to_non_nullable - as int, - maxConcurrentByHost: null == maxConcurrentByHost - ? _value.maxConcurrentByHost - : maxConcurrentByHost // ignore: cast_nullable_to_non_nullable - as int, - maxConcurrentByGroup: null == maxConcurrentByGroup - ? _value.maxConcurrentByGroup - : maxConcurrentByGroup // ignore: cast_nullable_to_non_nullable - as int, - ) as $Val); - } +/// Create a copy of DownloadSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? requiresWiFi = null,Object? retries = null,Object? allowPause = null,Object? maxConcurrent = null,Object? maxConcurrentByHost = null,Object? maxConcurrentByGroup = null,}) { + return _then(_self.copyWith( +requiresWiFi: null == requiresWiFi ? _self.requiresWiFi : requiresWiFi // ignore: cast_nullable_to_non_nullable +as bool,retries: null == retries ? _self.retries : retries // ignore: cast_nullable_to_non_nullable +as int,allowPause: null == allowPause ? _self.allowPause : allowPause // ignore: cast_nullable_to_non_nullable +as bool,maxConcurrent: null == maxConcurrent ? _self.maxConcurrent : maxConcurrent // ignore: cast_nullable_to_non_nullable +as int,maxConcurrentByHost: null == maxConcurrentByHost ? _self.maxConcurrentByHost : maxConcurrentByHost // ignore: cast_nullable_to_non_nullable +as int,maxConcurrentByGroup: null == maxConcurrentByGroup ? _self.maxConcurrentByGroup : maxConcurrentByGroup // ignore: cast_nullable_to_non_nullable +as int, + )); } -/// @nodoc -abstract class _$$DownloadSettingsImplCopyWith<$Res> - implements $DownloadSettingsCopyWith<$Res> { - factory _$$DownloadSettingsImplCopyWith(_$DownloadSettingsImpl value, - $Res Function(_$DownloadSettingsImpl) then) = - __$$DownloadSettingsImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {bool requiresWiFi, - int retries, - bool allowPause, - int maxConcurrent, - int maxConcurrentByHost, - int maxConcurrentByGroup}); } -/// @nodoc -class __$$DownloadSettingsImplCopyWithImpl<$Res> - extends _$DownloadSettingsCopyWithImpl<$Res, _$DownloadSettingsImpl> - implements _$$DownloadSettingsImplCopyWith<$Res> { - __$$DownloadSettingsImplCopyWithImpl(_$DownloadSettingsImpl _value, - $Res Function(_$DownloadSettingsImpl) _then) - : super(_value, _then); - /// Create a copy of DownloadSettings - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? requiresWiFi = null, - Object? retries = null, - Object? allowPause = null, - Object? maxConcurrent = null, - Object? maxConcurrentByHost = null, - Object? maxConcurrentByGroup = null, - }) { - return _then(_$DownloadSettingsImpl( - requiresWiFi: null == requiresWiFi - ? _value.requiresWiFi - : requiresWiFi // ignore: cast_nullable_to_non_nullable - as bool, - retries: null == retries - ? _value.retries - : retries // ignore: cast_nullable_to_non_nullable - as int, - allowPause: null == allowPause - ? _value.allowPause - : allowPause // ignore: cast_nullable_to_non_nullable - as bool, - maxConcurrent: null == maxConcurrent - ? _value.maxConcurrent - : maxConcurrent // ignore: cast_nullable_to_non_nullable - as int, - maxConcurrentByHost: null == maxConcurrentByHost - ? _value.maxConcurrentByHost - : maxConcurrentByHost // ignore: cast_nullable_to_non_nullable - as int, - maxConcurrentByGroup: null == maxConcurrentByGroup - ? _value.maxConcurrentByGroup - : maxConcurrentByGroup // ignore: cast_nullable_to_non_nullable - as int, - )); - } +/// Adds pattern-matching-related methods to [DownloadSettings]. +extension DownloadSettingsPatterns on DownloadSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _DownloadSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _DownloadSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _DownloadSettings value) $default,){ +final _that = this; +switch (_that) { +case _DownloadSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _DownloadSettings value)? $default,){ +final _that = this; +switch (_that) { +case _DownloadSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool requiresWiFi, int retries, bool allowPause, int maxConcurrent, int maxConcurrentByHost, int maxConcurrentByGroup)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _DownloadSettings() when $default != null: +return $default(_that.requiresWiFi,_that.retries,_that.allowPause,_that.maxConcurrent,_that.maxConcurrentByHost,_that.maxConcurrentByGroup);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( bool requiresWiFi, int retries, bool allowPause, int maxConcurrent, int maxConcurrentByHost, int maxConcurrentByGroup) $default,) {final _that = this; +switch (_that) { +case _DownloadSettings(): +return $default(_that.requiresWiFi,_that.retries,_that.allowPause,_that.maxConcurrent,_that.maxConcurrentByHost,_that.maxConcurrentByGroup);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool requiresWiFi, int retries, bool allowPause, int maxConcurrent, int maxConcurrentByHost, int maxConcurrentByGroup)? $default,) {final _that = this; +switch (_that) { +case _DownloadSettings() when $default != null: +return $default(_that.requiresWiFi,_that.retries,_that.allowPause,_that.maxConcurrent,_that.maxConcurrentByHost,_that.maxConcurrentByGroup);case _: + return null; + +} +} + } /// @nodoc @JsonSerializable() -class _$DownloadSettingsImpl implements _DownloadSettings { - const _$DownloadSettingsImpl( - {this.requiresWiFi = true, - this.retries = 3, - this.allowPause = true, - this.maxConcurrent = 3, - this.maxConcurrentByHost = 3, - this.maxConcurrentByGroup = 3}); - factory _$DownloadSettingsImpl.fromJson(Map json) => - _$$DownloadSettingsImplFromJson(json); +class _DownloadSettings implements DownloadSettings { + const _DownloadSettings({this.requiresWiFi = true, this.retries = 3, this.allowPause = true, this.maxConcurrent = 3, this.maxConcurrentByHost = 3, this.maxConcurrentByGroup = 3}); + factory _DownloadSettings.fromJson(Map json) => _$DownloadSettingsFromJson(json); - @override - @JsonKey() - final bool requiresWiFi; - @override - @JsonKey() - final int retries; - @override - @JsonKey() - final bool allowPause; - @override - @JsonKey() - final int maxConcurrent; - @override - @JsonKey() - final int maxConcurrentByHost; - @override - @JsonKey() - final int maxConcurrentByGroup; +@override@JsonKey() final bool requiresWiFi; +@override@JsonKey() final int retries; +@override@JsonKey() final bool allowPause; +@override@JsonKey() final int maxConcurrent; +@override@JsonKey() final int maxConcurrentByHost; +@override@JsonKey() final int maxConcurrentByGroup; - @override - String toString() { - return 'DownloadSettings(requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause, maxConcurrent: $maxConcurrent, maxConcurrentByHost: $maxConcurrentByHost, maxConcurrentByGroup: $maxConcurrentByGroup)'; - } +/// Create a copy of DownloadSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$DownloadSettingsCopyWith<_DownloadSettings> get copyWith => __$DownloadSettingsCopyWithImpl<_DownloadSettings>(this, _$identity); - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$DownloadSettingsImpl && - (identical(other.requiresWiFi, requiresWiFi) || - other.requiresWiFi == requiresWiFi) && - (identical(other.retries, retries) || other.retries == retries) && - (identical(other.allowPause, allowPause) || - other.allowPause == allowPause) && - (identical(other.maxConcurrent, maxConcurrent) || - other.maxConcurrent == maxConcurrent) && - (identical(other.maxConcurrentByHost, maxConcurrentByHost) || - other.maxConcurrentByHost == maxConcurrentByHost) && - (identical(other.maxConcurrentByGroup, maxConcurrentByGroup) || - other.maxConcurrentByGroup == maxConcurrentByGroup)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, requiresWiFi, retries, - allowPause, maxConcurrent, maxConcurrentByHost, maxConcurrentByGroup); - - /// Create a copy of DownloadSettings - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$DownloadSettingsImplCopyWith<_$DownloadSettingsImpl> get copyWith => - __$$DownloadSettingsImplCopyWithImpl<_$DownloadSettingsImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$DownloadSettingsImplToJson( - this, - ); - } +@override +Map toJson() { + return _$DownloadSettingsToJson(this, ); } -abstract class _DownloadSettings implements DownloadSettings { - const factory _DownloadSettings( - {final bool requiresWiFi, - final int retries, - final bool allowPause, - final int maxConcurrent, - final int maxConcurrentByHost, - final int maxConcurrentByGroup}) = _$DownloadSettingsImpl; - - factory _DownloadSettings.fromJson(Map json) = - _$DownloadSettingsImpl.fromJson; - - @override - bool get requiresWiFi; - @override - int get retries; - @override - bool get allowPause; - @override - int get maxConcurrent; - @override - int get maxConcurrentByHost; - @override - int get maxConcurrentByGroup; - - /// Create a copy of DownloadSettings - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$DownloadSettingsImplCopyWith<_$DownloadSettingsImpl> get copyWith => - throw _privateConstructorUsedError; +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _DownloadSettings&&(identical(other.requiresWiFi, requiresWiFi) || other.requiresWiFi == requiresWiFi)&&(identical(other.retries, retries) || other.retries == retries)&&(identical(other.allowPause, allowPause) || other.allowPause == allowPause)&&(identical(other.maxConcurrent, maxConcurrent) || other.maxConcurrent == maxConcurrent)&&(identical(other.maxConcurrentByHost, maxConcurrentByHost) || other.maxConcurrentByHost == maxConcurrentByHost)&&(identical(other.maxConcurrentByGroup, maxConcurrentByGroup) || other.maxConcurrentByGroup == maxConcurrentByGroup)); } + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,requiresWiFi,retries,allowPause,maxConcurrent,maxConcurrentByHost,maxConcurrentByGroup); + +@override +String toString() { + return 'DownloadSettings(requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause, maxConcurrent: $maxConcurrent, maxConcurrentByHost: $maxConcurrentByHost, maxConcurrentByGroup: $maxConcurrentByGroup)'; +} + + +} + +/// @nodoc +abstract mixin class _$DownloadSettingsCopyWith<$Res> implements $DownloadSettingsCopyWith<$Res> { + factory _$DownloadSettingsCopyWith(_DownloadSettings value, $Res Function(_DownloadSettings) _then) = __$DownloadSettingsCopyWithImpl; +@override @useResult +$Res call({ + bool requiresWiFi, int retries, bool allowPause, int maxConcurrent, int maxConcurrentByHost, int maxConcurrentByGroup +}); + + + + +} +/// @nodoc +class __$DownloadSettingsCopyWithImpl<$Res> + implements _$DownloadSettingsCopyWith<$Res> { + __$DownloadSettingsCopyWithImpl(this._self, this._then); + + final _DownloadSettings _self; + final $Res Function(_DownloadSettings) _then; + +/// Create a copy of DownloadSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? requiresWiFi = null,Object? retries = null,Object? allowPause = null,Object? maxConcurrent = null,Object? maxConcurrentByHost = null,Object? maxConcurrentByGroup = null,}) { + return _then(_DownloadSettings( +requiresWiFi: null == requiresWiFi ? _self.requiresWiFi : requiresWiFi // ignore: cast_nullable_to_non_nullable +as bool,retries: null == retries ? _self.retries : retries // ignore: cast_nullable_to_non_nullable +as int,allowPause: null == allowPause ? _self.allowPause : allowPause // ignore: cast_nullable_to_non_nullable +as bool,maxConcurrent: null == maxConcurrent ? _self.maxConcurrent : maxConcurrent // ignore: cast_nullable_to_non_nullable +as int,maxConcurrentByHost: null == maxConcurrentByHost ? _self.maxConcurrentByHost : maxConcurrentByHost // ignore: cast_nullable_to_non_nullable +as int,maxConcurrentByGroup: null == maxConcurrentByGroup ? _self.maxConcurrentByGroup : maxConcurrentByGroup // ignore: cast_nullable_to_non_nullable +as int, + )); +} + + +} + + +/// @nodoc +mixin _$NotificationSettings { + + Duration get fastForwardInterval; Duration get rewindInterval; bool get progressBarIsChapterProgress; String get primaryTitle; String get secondaryTitle; List get mediaControls; +/// Create a copy of NotificationSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$NotificationSettingsCopyWith get copyWith => _$NotificationSettingsCopyWithImpl(this as NotificationSettings, _$identity); + + /// Serializes this NotificationSettings to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is NotificationSettings&&(identical(other.fastForwardInterval, fastForwardInterval) || other.fastForwardInterval == fastForwardInterval)&&(identical(other.rewindInterval, rewindInterval) || other.rewindInterval == rewindInterval)&&(identical(other.progressBarIsChapterProgress, progressBarIsChapterProgress) || other.progressBarIsChapterProgress == progressBarIsChapterProgress)&&(identical(other.primaryTitle, primaryTitle) || other.primaryTitle == primaryTitle)&&(identical(other.secondaryTitle, secondaryTitle) || other.secondaryTitle == secondaryTitle)&&const DeepCollectionEquality().equals(other.mediaControls, mediaControls)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,fastForwardInterval,rewindInterval,progressBarIsChapterProgress,primaryTitle,secondaryTitle,const DeepCollectionEquality().hash(mediaControls)); + +@override +String toString() { + return 'NotificationSettings(fastForwardInterval: $fastForwardInterval, rewindInterval: $rewindInterval, progressBarIsChapterProgress: $progressBarIsChapterProgress, primaryTitle: $primaryTitle, secondaryTitle: $secondaryTitle, mediaControls: $mediaControls)'; +} + + +} + +/// @nodoc +abstract mixin class $NotificationSettingsCopyWith<$Res> { + factory $NotificationSettingsCopyWith(NotificationSettings value, $Res Function(NotificationSettings) _then) = _$NotificationSettingsCopyWithImpl; +@useResult +$Res call({ + Duration fastForwardInterval, Duration rewindInterval, bool progressBarIsChapterProgress, String primaryTitle, String secondaryTitle, List mediaControls +}); + + + + +} +/// @nodoc +class _$NotificationSettingsCopyWithImpl<$Res> + implements $NotificationSettingsCopyWith<$Res> { + _$NotificationSettingsCopyWithImpl(this._self, this._then); + + final NotificationSettings _self; + final $Res Function(NotificationSettings) _then; + +/// Create a copy of NotificationSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? fastForwardInterval = null,Object? rewindInterval = null,Object? progressBarIsChapterProgress = null,Object? primaryTitle = null,Object? secondaryTitle = null,Object? mediaControls = null,}) { + return _then(_self.copyWith( +fastForwardInterval: null == fastForwardInterval ? _self.fastForwardInterval : fastForwardInterval // ignore: cast_nullable_to_non_nullable +as Duration,rewindInterval: null == rewindInterval ? _self.rewindInterval : rewindInterval // ignore: cast_nullable_to_non_nullable +as Duration,progressBarIsChapterProgress: null == progressBarIsChapterProgress ? _self.progressBarIsChapterProgress : progressBarIsChapterProgress // ignore: cast_nullable_to_non_nullable +as bool,primaryTitle: null == primaryTitle ? _self.primaryTitle : primaryTitle // ignore: cast_nullable_to_non_nullable +as String,secondaryTitle: null == secondaryTitle ? _self.secondaryTitle : secondaryTitle // ignore: cast_nullable_to_non_nullable +as String,mediaControls: null == mediaControls ? _self.mediaControls : mediaControls // ignore: cast_nullable_to_non_nullable +as List, + )); +} + +} + + +/// Adds pattern-matching-related methods to [NotificationSettings]. +extension NotificationSettingsPatterns on NotificationSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _NotificationSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _NotificationSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _NotificationSettings value) $default,){ +final _that = this; +switch (_that) { +case _NotificationSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _NotificationSettings value)? $default,){ +final _that = this; +switch (_that) { +case _NotificationSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( Duration fastForwardInterval, Duration rewindInterval, bool progressBarIsChapterProgress, String primaryTitle, String secondaryTitle, List mediaControls)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _NotificationSettings() when $default != null: +return $default(_that.fastForwardInterval,_that.rewindInterval,_that.progressBarIsChapterProgress,_that.primaryTitle,_that.secondaryTitle,_that.mediaControls);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( Duration fastForwardInterval, Duration rewindInterval, bool progressBarIsChapterProgress, String primaryTitle, String secondaryTitle, List mediaControls) $default,) {final _that = this; +switch (_that) { +case _NotificationSettings(): +return $default(_that.fastForwardInterval,_that.rewindInterval,_that.progressBarIsChapterProgress,_that.primaryTitle,_that.secondaryTitle,_that.mediaControls);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( Duration fastForwardInterval, Duration rewindInterval, bool progressBarIsChapterProgress, String primaryTitle, String secondaryTitle, List mediaControls)? $default,) {final _that = this; +switch (_that) { +case _NotificationSettings() when $default != null: +return $default(_that.fastForwardInterval,_that.rewindInterval,_that.progressBarIsChapterProgress,_that.primaryTitle,_that.secondaryTitle,_that.mediaControls);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _NotificationSettings implements NotificationSettings { + const _NotificationSettings({this.fastForwardInterval = const Duration(seconds: 30), this.rewindInterval = const Duration(seconds: 10), this.progressBarIsChapterProgress = true, this.primaryTitle = '\$bookTitle', this.secondaryTitle = '\$author', final List mediaControls = const [NotificationMediaControl.rewind, NotificationMediaControl.fastForward, NotificationMediaControl.skipToPreviousChapter, NotificationMediaControl.skipToNextChapter]}): _mediaControls = mediaControls; + factory _NotificationSettings.fromJson(Map json) => _$NotificationSettingsFromJson(json); + +@override@JsonKey() final Duration fastForwardInterval; +@override@JsonKey() final Duration rewindInterval; +@override@JsonKey() final bool progressBarIsChapterProgress; +@override@JsonKey() final String primaryTitle; +@override@JsonKey() final String secondaryTitle; + final List _mediaControls; +@override@JsonKey() List get mediaControls { + if (_mediaControls is EqualUnmodifiableListView) return _mediaControls; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_mediaControls); +} + + +/// Create a copy of NotificationSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$NotificationSettingsCopyWith<_NotificationSettings> get copyWith => __$NotificationSettingsCopyWithImpl<_NotificationSettings>(this, _$identity); + +@override +Map toJson() { + return _$NotificationSettingsToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _NotificationSettings&&(identical(other.fastForwardInterval, fastForwardInterval) || other.fastForwardInterval == fastForwardInterval)&&(identical(other.rewindInterval, rewindInterval) || other.rewindInterval == rewindInterval)&&(identical(other.progressBarIsChapterProgress, progressBarIsChapterProgress) || other.progressBarIsChapterProgress == progressBarIsChapterProgress)&&(identical(other.primaryTitle, primaryTitle) || other.primaryTitle == primaryTitle)&&(identical(other.secondaryTitle, secondaryTitle) || other.secondaryTitle == secondaryTitle)&&const DeepCollectionEquality().equals(other._mediaControls, _mediaControls)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,fastForwardInterval,rewindInterval,progressBarIsChapterProgress,primaryTitle,secondaryTitle,const DeepCollectionEquality().hash(_mediaControls)); + +@override +String toString() { + return 'NotificationSettings(fastForwardInterval: $fastForwardInterval, rewindInterval: $rewindInterval, progressBarIsChapterProgress: $progressBarIsChapterProgress, primaryTitle: $primaryTitle, secondaryTitle: $secondaryTitle, mediaControls: $mediaControls)'; +} + + +} + +/// @nodoc +abstract mixin class _$NotificationSettingsCopyWith<$Res> implements $NotificationSettingsCopyWith<$Res> { + factory _$NotificationSettingsCopyWith(_NotificationSettings value, $Res Function(_NotificationSettings) _then) = __$NotificationSettingsCopyWithImpl; +@override @useResult +$Res call({ + Duration fastForwardInterval, Duration rewindInterval, bool progressBarIsChapterProgress, String primaryTitle, String secondaryTitle, List mediaControls +}); + + + + +} +/// @nodoc +class __$NotificationSettingsCopyWithImpl<$Res> + implements _$NotificationSettingsCopyWith<$Res> { + __$NotificationSettingsCopyWithImpl(this._self, this._then); + + final _NotificationSettings _self; + final $Res Function(_NotificationSettings) _then; + +/// Create a copy of NotificationSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? fastForwardInterval = null,Object? rewindInterval = null,Object? progressBarIsChapterProgress = null,Object? primaryTitle = null,Object? secondaryTitle = null,Object? mediaControls = null,}) { + return _then(_NotificationSettings( +fastForwardInterval: null == fastForwardInterval ? _self.fastForwardInterval : fastForwardInterval // ignore: cast_nullable_to_non_nullable +as Duration,rewindInterval: null == rewindInterval ? _self.rewindInterval : rewindInterval // ignore: cast_nullable_to_non_nullable +as Duration,progressBarIsChapterProgress: null == progressBarIsChapterProgress ? _self.progressBarIsChapterProgress : progressBarIsChapterProgress // ignore: cast_nullable_to_non_nullable +as bool,primaryTitle: null == primaryTitle ? _self.primaryTitle : primaryTitle // ignore: cast_nullable_to_non_nullable +as String,secondaryTitle: null == secondaryTitle ? _self.secondaryTitle : secondaryTitle // ignore: cast_nullable_to_non_nullable +as String,mediaControls: null == mediaControls ? _self._mediaControls : mediaControls // ignore: cast_nullable_to_non_nullable +as List, + )); +} + + +} + + +/// @nodoc +mixin _$ShakeDetectionSettings { + + bool get isEnabled; ShakeDirection get direction; double get threshold; ShakeAction get shakeAction; Set get feedback; double get beepVolume;/// the duration to wait before the shake detection is enabled again + Duration get shakeTriggerCoolDown;/// the number of shakes required to trigger the action + int get shakeTriggerCount;/// acceleration sampling interval + Duration get samplingPeriod; +/// Create a copy of ShakeDetectionSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$ShakeDetectionSettingsCopyWith get copyWith => _$ShakeDetectionSettingsCopyWithImpl(this as ShakeDetectionSettings, _$identity); + + /// Serializes this ShakeDetectionSettings to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is ShakeDetectionSettings&&(identical(other.isEnabled, isEnabled) || other.isEnabled == isEnabled)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.threshold, threshold) || other.threshold == threshold)&&(identical(other.shakeAction, shakeAction) || other.shakeAction == shakeAction)&&const DeepCollectionEquality().equals(other.feedback, feedback)&&(identical(other.beepVolume, beepVolume) || other.beepVolume == beepVolume)&&(identical(other.shakeTriggerCoolDown, shakeTriggerCoolDown) || other.shakeTriggerCoolDown == shakeTriggerCoolDown)&&(identical(other.shakeTriggerCount, shakeTriggerCount) || other.shakeTriggerCount == shakeTriggerCount)&&(identical(other.samplingPeriod, samplingPeriod) || other.samplingPeriod == samplingPeriod)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,isEnabled,direction,threshold,shakeAction,const DeepCollectionEquality().hash(feedback),beepVolume,shakeTriggerCoolDown,shakeTriggerCount,samplingPeriod); + +@override +String toString() { + return 'ShakeDetectionSettings(isEnabled: $isEnabled, direction: $direction, threshold: $threshold, shakeAction: $shakeAction, feedback: $feedback, beepVolume: $beepVolume, shakeTriggerCoolDown: $shakeTriggerCoolDown, shakeTriggerCount: $shakeTriggerCount, samplingPeriod: $samplingPeriod)'; +} + + +} + +/// @nodoc +abstract mixin class $ShakeDetectionSettingsCopyWith<$Res> { + factory $ShakeDetectionSettingsCopyWith(ShakeDetectionSettings value, $Res Function(ShakeDetectionSettings) _then) = _$ShakeDetectionSettingsCopyWithImpl; +@useResult +$Res call({ + bool isEnabled, ShakeDirection direction, double threshold, ShakeAction shakeAction, Set feedback, double beepVolume, Duration shakeTriggerCoolDown, int shakeTriggerCount, Duration samplingPeriod +}); + + + + +} +/// @nodoc +class _$ShakeDetectionSettingsCopyWithImpl<$Res> + implements $ShakeDetectionSettingsCopyWith<$Res> { + _$ShakeDetectionSettingsCopyWithImpl(this._self, this._then); + + final ShakeDetectionSettings _self; + final $Res Function(ShakeDetectionSettings) _then; + +/// Create a copy of ShakeDetectionSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? isEnabled = null,Object? direction = null,Object? threshold = null,Object? shakeAction = null,Object? feedback = null,Object? beepVolume = null,Object? shakeTriggerCoolDown = null,Object? shakeTriggerCount = null,Object? samplingPeriod = null,}) { + return _then(_self.copyWith( +isEnabled: null == isEnabled ? _self.isEnabled : isEnabled // ignore: cast_nullable_to_non_nullable +as bool,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as ShakeDirection,threshold: null == threshold ? _self.threshold : threshold // ignore: cast_nullable_to_non_nullable +as double,shakeAction: null == shakeAction ? _self.shakeAction : shakeAction // ignore: cast_nullable_to_non_nullable +as ShakeAction,feedback: null == feedback ? _self.feedback : feedback // ignore: cast_nullable_to_non_nullable +as Set,beepVolume: null == beepVolume ? _self.beepVolume : beepVolume // ignore: cast_nullable_to_non_nullable +as double,shakeTriggerCoolDown: null == shakeTriggerCoolDown ? _self.shakeTriggerCoolDown : shakeTriggerCoolDown // ignore: cast_nullable_to_non_nullable +as Duration,shakeTriggerCount: null == shakeTriggerCount ? _self.shakeTriggerCount : shakeTriggerCount // ignore: cast_nullable_to_non_nullable +as int,samplingPeriod: null == samplingPeriod ? _self.samplingPeriod : samplingPeriod // ignore: cast_nullable_to_non_nullable +as Duration, + )); +} + +} + + +/// Adds pattern-matching-related methods to [ShakeDetectionSettings]. +extension ShakeDetectionSettingsPatterns on ShakeDetectionSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _ShakeDetectionSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _ShakeDetectionSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _ShakeDetectionSettings value) $default,){ +final _that = this; +switch (_that) { +case _ShakeDetectionSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _ShakeDetectionSettings value)? $default,){ +final _that = this; +switch (_that) { +case _ShakeDetectionSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool isEnabled, ShakeDirection direction, double threshold, ShakeAction shakeAction, Set feedback, double beepVolume, Duration shakeTriggerCoolDown, int shakeTriggerCount, Duration samplingPeriod)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _ShakeDetectionSettings() when $default != null: +return $default(_that.isEnabled,_that.direction,_that.threshold,_that.shakeAction,_that.feedback,_that.beepVolume,_that.shakeTriggerCoolDown,_that.shakeTriggerCount,_that.samplingPeriod);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( bool isEnabled, ShakeDirection direction, double threshold, ShakeAction shakeAction, Set feedback, double beepVolume, Duration shakeTriggerCoolDown, int shakeTriggerCount, Duration samplingPeriod) $default,) {final _that = this; +switch (_that) { +case _ShakeDetectionSettings(): +return $default(_that.isEnabled,_that.direction,_that.threshold,_that.shakeAction,_that.feedback,_that.beepVolume,_that.shakeTriggerCoolDown,_that.shakeTriggerCount,_that.samplingPeriod);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool isEnabled, ShakeDirection direction, double threshold, ShakeAction shakeAction, Set feedback, double beepVolume, Duration shakeTriggerCoolDown, int shakeTriggerCount, Duration samplingPeriod)? $default,) {final _that = this; +switch (_that) { +case _ShakeDetectionSettings() when $default != null: +return $default(_that.isEnabled,_that.direction,_that.threshold,_that.shakeAction,_that.feedback,_that.beepVolume,_that.shakeTriggerCoolDown,_that.shakeTriggerCount,_that.samplingPeriod);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _ShakeDetectionSettings implements ShakeDetectionSettings { + const _ShakeDetectionSettings({this.isEnabled = true, this.direction = ShakeDirection.horizontal, this.threshold = 5, this.shakeAction = ShakeAction.resetSleepTimer, final Set feedback = const {ShakeDetectedFeedback.vibrate}, this.beepVolume = 0.5, this.shakeTriggerCoolDown = const Duration(seconds: 2), this.shakeTriggerCount = 2, this.samplingPeriod = const Duration(milliseconds: 100)}): _feedback = feedback; + factory _ShakeDetectionSettings.fromJson(Map json) => _$ShakeDetectionSettingsFromJson(json); + +@override@JsonKey() final bool isEnabled; +@override@JsonKey() final ShakeDirection direction; +@override@JsonKey() final double threshold; +@override@JsonKey() final ShakeAction shakeAction; + final Set _feedback; +@override@JsonKey() Set get feedback { + if (_feedback is EqualUnmodifiableSetView) return _feedback; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_feedback); +} + +@override@JsonKey() final double beepVolume; +/// the duration to wait before the shake detection is enabled again +@override@JsonKey() final Duration shakeTriggerCoolDown; +/// the number of shakes required to trigger the action +@override@JsonKey() final int shakeTriggerCount; +/// acceleration sampling interval +@override@JsonKey() final Duration samplingPeriod; + +/// Create a copy of ShakeDetectionSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$ShakeDetectionSettingsCopyWith<_ShakeDetectionSettings> get copyWith => __$ShakeDetectionSettingsCopyWithImpl<_ShakeDetectionSettings>(this, _$identity); + +@override +Map toJson() { + return _$ShakeDetectionSettingsToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ShakeDetectionSettings&&(identical(other.isEnabled, isEnabled) || other.isEnabled == isEnabled)&&(identical(other.direction, direction) || other.direction == direction)&&(identical(other.threshold, threshold) || other.threshold == threshold)&&(identical(other.shakeAction, shakeAction) || other.shakeAction == shakeAction)&&const DeepCollectionEquality().equals(other._feedback, _feedback)&&(identical(other.beepVolume, beepVolume) || other.beepVolume == beepVolume)&&(identical(other.shakeTriggerCoolDown, shakeTriggerCoolDown) || other.shakeTriggerCoolDown == shakeTriggerCoolDown)&&(identical(other.shakeTriggerCount, shakeTriggerCount) || other.shakeTriggerCount == shakeTriggerCount)&&(identical(other.samplingPeriod, samplingPeriod) || other.samplingPeriod == samplingPeriod)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,isEnabled,direction,threshold,shakeAction,const DeepCollectionEquality().hash(_feedback),beepVolume,shakeTriggerCoolDown,shakeTriggerCount,samplingPeriod); + +@override +String toString() { + return 'ShakeDetectionSettings(isEnabled: $isEnabled, direction: $direction, threshold: $threshold, shakeAction: $shakeAction, feedback: $feedback, beepVolume: $beepVolume, shakeTriggerCoolDown: $shakeTriggerCoolDown, shakeTriggerCount: $shakeTriggerCount, samplingPeriod: $samplingPeriod)'; +} + + +} + +/// @nodoc +abstract mixin class _$ShakeDetectionSettingsCopyWith<$Res> implements $ShakeDetectionSettingsCopyWith<$Res> { + factory _$ShakeDetectionSettingsCopyWith(_ShakeDetectionSettings value, $Res Function(_ShakeDetectionSettings) _then) = __$ShakeDetectionSettingsCopyWithImpl; +@override @useResult +$Res call({ + bool isEnabled, ShakeDirection direction, double threshold, ShakeAction shakeAction, Set feedback, double beepVolume, Duration shakeTriggerCoolDown, int shakeTriggerCount, Duration samplingPeriod +}); + + + + +} +/// @nodoc +class __$ShakeDetectionSettingsCopyWithImpl<$Res> + implements _$ShakeDetectionSettingsCopyWith<$Res> { + __$ShakeDetectionSettingsCopyWithImpl(this._self, this._then); + + final _ShakeDetectionSettings _self; + final $Res Function(_ShakeDetectionSettings) _then; + +/// Create a copy of ShakeDetectionSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? isEnabled = null,Object? direction = null,Object? threshold = null,Object? shakeAction = null,Object? feedback = null,Object? beepVolume = null,Object? shakeTriggerCoolDown = null,Object? shakeTriggerCount = null,Object? samplingPeriod = null,}) { + return _then(_ShakeDetectionSettings( +isEnabled: null == isEnabled ? _self.isEnabled : isEnabled // ignore: cast_nullable_to_non_nullable +as bool,direction: null == direction ? _self.direction : direction // ignore: cast_nullable_to_non_nullable +as ShakeDirection,threshold: null == threshold ? _self.threshold : threshold // ignore: cast_nullable_to_non_nullable +as double,shakeAction: null == shakeAction ? _self.shakeAction : shakeAction // ignore: cast_nullable_to_non_nullable +as ShakeAction,feedback: null == feedback ? _self._feedback : feedback // ignore: cast_nullable_to_non_nullable +as Set,beepVolume: null == beepVolume ? _self.beepVolume : beepVolume // ignore: cast_nullable_to_non_nullable +as double,shakeTriggerCoolDown: null == shakeTriggerCoolDown ? _self.shakeTriggerCoolDown : shakeTriggerCoolDown // ignore: cast_nullable_to_non_nullable +as Duration,shakeTriggerCount: null == shakeTriggerCount ? _self.shakeTriggerCount : shakeTriggerCount // ignore: cast_nullable_to_non_nullable +as int,samplingPeriod: null == samplingPeriod ? _self.samplingPeriod : samplingPeriod // ignore: cast_nullable_to_non_nullable +as Duration, + )); +} + + +} + + +/// @nodoc +mixin _$HomePageSettings { + + bool get showPlayButtonOnContinueListeningShelf; bool get showPlayButtonOnContinueSeriesShelf; bool get showPlayButtonOnAllRemainingShelves; bool get showPlayButtonOnListenAgainShelf; +/// Create a copy of HomePageSettings +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$HomePageSettingsCopyWith get copyWith => _$HomePageSettingsCopyWithImpl(this as HomePageSettings, _$identity); + + /// Serializes this HomePageSettings to a JSON map. + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is HomePageSettings&&(identical(other.showPlayButtonOnContinueListeningShelf, showPlayButtonOnContinueListeningShelf) || other.showPlayButtonOnContinueListeningShelf == showPlayButtonOnContinueListeningShelf)&&(identical(other.showPlayButtonOnContinueSeriesShelf, showPlayButtonOnContinueSeriesShelf) || other.showPlayButtonOnContinueSeriesShelf == showPlayButtonOnContinueSeriesShelf)&&(identical(other.showPlayButtonOnAllRemainingShelves, showPlayButtonOnAllRemainingShelves) || other.showPlayButtonOnAllRemainingShelves == showPlayButtonOnAllRemainingShelves)&&(identical(other.showPlayButtonOnListenAgainShelf, showPlayButtonOnListenAgainShelf) || other.showPlayButtonOnListenAgainShelf == showPlayButtonOnListenAgainShelf)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,showPlayButtonOnContinueListeningShelf,showPlayButtonOnContinueSeriesShelf,showPlayButtonOnAllRemainingShelves,showPlayButtonOnListenAgainShelf); + +@override +String toString() { + return 'HomePageSettings(showPlayButtonOnContinueListeningShelf: $showPlayButtonOnContinueListeningShelf, showPlayButtonOnContinueSeriesShelf: $showPlayButtonOnContinueSeriesShelf, showPlayButtonOnAllRemainingShelves: $showPlayButtonOnAllRemainingShelves, showPlayButtonOnListenAgainShelf: $showPlayButtonOnListenAgainShelf)'; +} + + +} + +/// @nodoc +abstract mixin class $HomePageSettingsCopyWith<$Res> { + factory $HomePageSettingsCopyWith(HomePageSettings value, $Res Function(HomePageSettings) _then) = _$HomePageSettingsCopyWithImpl; +@useResult +$Res call({ + bool showPlayButtonOnContinueListeningShelf, bool showPlayButtonOnContinueSeriesShelf, bool showPlayButtonOnAllRemainingShelves, bool showPlayButtonOnListenAgainShelf +}); + + + + +} +/// @nodoc +class _$HomePageSettingsCopyWithImpl<$Res> + implements $HomePageSettingsCopyWith<$Res> { + _$HomePageSettingsCopyWithImpl(this._self, this._then); + + final HomePageSettings _self; + final $Res Function(HomePageSettings) _then; + +/// Create a copy of HomePageSettings +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? showPlayButtonOnContinueListeningShelf = null,Object? showPlayButtonOnContinueSeriesShelf = null,Object? showPlayButtonOnAllRemainingShelves = null,Object? showPlayButtonOnListenAgainShelf = null,}) { + return _then(_self.copyWith( +showPlayButtonOnContinueListeningShelf: null == showPlayButtonOnContinueListeningShelf ? _self.showPlayButtonOnContinueListeningShelf : showPlayButtonOnContinueListeningShelf // ignore: cast_nullable_to_non_nullable +as bool,showPlayButtonOnContinueSeriesShelf: null == showPlayButtonOnContinueSeriesShelf ? _self.showPlayButtonOnContinueSeriesShelf : showPlayButtonOnContinueSeriesShelf // ignore: cast_nullable_to_non_nullable +as bool,showPlayButtonOnAllRemainingShelves: null == showPlayButtonOnAllRemainingShelves ? _self.showPlayButtonOnAllRemainingShelves : showPlayButtonOnAllRemainingShelves // ignore: cast_nullable_to_non_nullable +as bool,showPlayButtonOnListenAgainShelf: null == showPlayButtonOnListenAgainShelf ? _self.showPlayButtonOnListenAgainShelf : showPlayButtonOnListenAgainShelf // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + +} + + +/// Adds pattern-matching-related methods to [HomePageSettings]. +extension HomePageSettingsPatterns on HomePageSettings { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _HomePageSettings value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _HomePageSettings() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _HomePageSettings value) $default,){ +final _that = this; +switch (_that) { +case _HomePageSettings(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _HomePageSettings value)? $default,){ +final _that = this; +switch (_that) { +case _HomePageSettings() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( bool showPlayButtonOnContinueListeningShelf, bool showPlayButtonOnContinueSeriesShelf, bool showPlayButtonOnAllRemainingShelves, bool showPlayButtonOnListenAgainShelf)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _HomePageSettings() when $default != null: +return $default(_that.showPlayButtonOnContinueListeningShelf,_that.showPlayButtonOnContinueSeriesShelf,_that.showPlayButtonOnAllRemainingShelves,_that.showPlayButtonOnListenAgainShelf);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( bool showPlayButtonOnContinueListeningShelf, bool showPlayButtonOnContinueSeriesShelf, bool showPlayButtonOnAllRemainingShelves, bool showPlayButtonOnListenAgainShelf) $default,) {final _that = this; +switch (_that) { +case _HomePageSettings(): +return $default(_that.showPlayButtonOnContinueListeningShelf,_that.showPlayButtonOnContinueSeriesShelf,_that.showPlayButtonOnAllRemainingShelves,_that.showPlayButtonOnListenAgainShelf);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool showPlayButtonOnContinueListeningShelf, bool showPlayButtonOnContinueSeriesShelf, bool showPlayButtonOnAllRemainingShelves, bool showPlayButtonOnListenAgainShelf)? $default,) {final _that = this; +switch (_that) { +case _HomePageSettings() when $default != null: +return $default(_that.showPlayButtonOnContinueListeningShelf,_that.showPlayButtonOnContinueSeriesShelf,_that.showPlayButtonOnAllRemainingShelves,_that.showPlayButtonOnListenAgainShelf);case _: + return null; + +} +} + +} + +/// @nodoc +@JsonSerializable() + +class _HomePageSettings implements HomePageSettings { + const _HomePageSettings({this.showPlayButtonOnContinueListeningShelf = true, this.showPlayButtonOnContinueSeriesShelf = false, this.showPlayButtonOnAllRemainingShelves = false, this.showPlayButtonOnListenAgainShelf = false}); + factory _HomePageSettings.fromJson(Map json) => _$HomePageSettingsFromJson(json); + +@override@JsonKey() final bool showPlayButtonOnContinueListeningShelf; +@override@JsonKey() final bool showPlayButtonOnContinueSeriesShelf; +@override@JsonKey() final bool showPlayButtonOnAllRemainingShelves; +@override@JsonKey() final bool showPlayButtonOnListenAgainShelf; + +/// Create a copy of HomePageSettings +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$HomePageSettingsCopyWith<_HomePageSettings> get copyWith => __$HomePageSettingsCopyWithImpl<_HomePageSettings>(this, _$identity); + +@override +Map toJson() { + return _$HomePageSettingsToJson(this, ); +} + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _HomePageSettings&&(identical(other.showPlayButtonOnContinueListeningShelf, showPlayButtonOnContinueListeningShelf) || other.showPlayButtonOnContinueListeningShelf == showPlayButtonOnContinueListeningShelf)&&(identical(other.showPlayButtonOnContinueSeriesShelf, showPlayButtonOnContinueSeriesShelf) || other.showPlayButtonOnContinueSeriesShelf == showPlayButtonOnContinueSeriesShelf)&&(identical(other.showPlayButtonOnAllRemainingShelves, showPlayButtonOnAllRemainingShelves) || other.showPlayButtonOnAllRemainingShelves == showPlayButtonOnAllRemainingShelves)&&(identical(other.showPlayButtonOnListenAgainShelf, showPlayButtonOnListenAgainShelf) || other.showPlayButtonOnListenAgainShelf == showPlayButtonOnListenAgainShelf)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,showPlayButtonOnContinueListeningShelf,showPlayButtonOnContinueSeriesShelf,showPlayButtonOnAllRemainingShelves,showPlayButtonOnListenAgainShelf); + +@override +String toString() { + return 'HomePageSettings(showPlayButtonOnContinueListeningShelf: $showPlayButtonOnContinueListeningShelf, showPlayButtonOnContinueSeriesShelf: $showPlayButtonOnContinueSeriesShelf, showPlayButtonOnAllRemainingShelves: $showPlayButtonOnAllRemainingShelves, showPlayButtonOnListenAgainShelf: $showPlayButtonOnListenAgainShelf)'; +} + + +} + +/// @nodoc +abstract mixin class _$HomePageSettingsCopyWith<$Res> implements $HomePageSettingsCopyWith<$Res> { + factory _$HomePageSettingsCopyWith(_HomePageSettings value, $Res Function(_HomePageSettings) _then) = __$HomePageSettingsCopyWithImpl; +@override @useResult +$Res call({ + bool showPlayButtonOnContinueListeningShelf, bool showPlayButtonOnContinueSeriesShelf, bool showPlayButtonOnAllRemainingShelves, bool showPlayButtonOnListenAgainShelf +}); + + + + +} +/// @nodoc +class __$HomePageSettingsCopyWithImpl<$Res> + implements _$HomePageSettingsCopyWith<$Res> { + __$HomePageSettingsCopyWithImpl(this._self, this._then); + + final _HomePageSettings _self; + final $Res Function(_HomePageSettings) _then; + +/// Create a copy of HomePageSettings +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? showPlayButtonOnContinueListeningShelf = null,Object? showPlayButtonOnContinueSeriesShelf = null,Object? showPlayButtonOnAllRemainingShelves = null,Object? showPlayButtonOnListenAgainShelf = null,}) { + return _then(_HomePageSettings( +showPlayButtonOnContinueListeningShelf: null == showPlayButtonOnContinueListeningShelf ? _self.showPlayButtonOnContinueListeningShelf : showPlayButtonOnContinueListeningShelf // ignore: cast_nullable_to_non_nullable +as bool,showPlayButtonOnContinueSeriesShelf: null == showPlayButtonOnContinueSeriesShelf ? _self.showPlayButtonOnContinueSeriesShelf : showPlayButtonOnContinueSeriesShelf // ignore: cast_nullable_to_non_nullable +as bool,showPlayButtonOnAllRemainingShelves: null == showPlayButtonOnAllRemainingShelves ? _self.showPlayButtonOnAllRemainingShelves : showPlayButtonOnAllRemainingShelves // ignore: cast_nullable_to_non_nullable +as bool,showPlayButtonOnListenAgainShelf: null == showPlayButtonOnListenAgainShelf ? _self.showPlayButtonOnListenAgainShelf : showPlayButtonOnListenAgainShelf // ignore: cast_nullable_to_non_nullable +as bool, + )); +} + + +} + +// dart format on diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index 8986989..003dac8 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -6,147 +6,207 @@ part of 'app_settings.dart'; // JsonSerializableGenerator // ************************************************************************** -_$AppSettingsImpl _$$AppSettingsImplFromJson(Map json) => - _$AppSettingsImpl( - themeSettings: json['themeSettings'] == null - ? const ThemeSettings() - : ThemeSettings.fromJson( - json['themeSettings'] as Map), - playerSettings: json['playerSettings'] == null - ? const PlayerSettings() - : PlayerSettings.fromJson( - json['playerSettings'] as Map), - downloadSettings: json['downloadSettings'] == null - ? const DownloadSettings() - : DownloadSettings.fromJson( - json['downloadSettings'] as Map), - ); +_AppSettings _$AppSettingsFromJson(Map json) => _AppSettings( + themeSettings: json['themeSettings'] == null + ? const ThemeSettings() + : ThemeSettings.fromJson(json['themeSettings'] as Map), + playerSettings: json['playerSettings'] == null + ? const PlayerSettings() + : PlayerSettings.fromJson(json['playerSettings'] as Map), + sleepTimerSettings: json['sleepTimerSettings'] == null + ? const SleepTimerSettings() + : SleepTimerSettings.fromJson( + json['sleepTimerSettings'] as Map, + ), + downloadSettings: json['downloadSettings'] == null + ? const DownloadSettings() + : DownloadSettings.fromJson( + json['downloadSettings'] as Map, + ), + notificationSettings: json['notificationSettings'] == null + ? const NotificationSettings() + : NotificationSettings.fromJson( + json['notificationSettings'] as Map, + ), + shakeDetectionSettings: json['shakeDetectionSettings'] == null + ? const ShakeDetectionSettings() + : ShakeDetectionSettings.fromJson( + json['shakeDetectionSettings'] as Map, + ), + homePageSettings: json['homePageSettings'] == null + ? const HomePageSettings() + : HomePageSettings.fromJson( + json['homePageSettings'] as Map, + ), +); -Map _$$AppSettingsImplToJson(_$AppSettingsImpl instance) => +Map _$AppSettingsToJson(_AppSettings instance) => { 'themeSettings': instance.themeSettings, 'playerSettings': instance.playerSettings, + 'sleepTimerSettings': instance.sleepTimerSettings, 'downloadSettings': instance.downloadSettings, + 'notificationSettings': instance.notificationSettings, + 'shakeDetectionSettings': instance.shakeDetectionSettings, + 'homePageSettings': instance.homePageSettings, }; -_$ThemeSettingsImpl _$$ThemeSettingsImplFromJson(Map json) => - _$ThemeSettingsImpl( - isDarkMode: json['isDarkMode'] as bool? ?? true, +_ThemeSettings _$ThemeSettingsFromJson(Map json) => + _ThemeSettings( + themeMode: + $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? + ThemeMode.system, + highContrast: json['highContrast'] as bool? ?? false, + useMaterialThemeFromSystem: + json['useMaterialThemeFromSystem'] as bool? ?? false, + customThemeColor: json['customThemeColor'] as String? ?? '#FF311B92', useMaterialThemeOnItemPage: json['useMaterialThemeOnItemPage'] as bool? ?? true, useCurrentPlayerThemeThroughoutApp: json['useCurrentPlayerThemeThroughoutApp'] as bool? ?? true, ); -Map _$$ThemeSettingsImplToJson(_$ThemeSettingsImpl instance) => +Map _$ThemeSettingsToJson(_ThemeSettings instance) => { - 'isDarkMode': instance.isDarkMode, + 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, + 'highContrast': instance.highContrast, + 'useMaterialThemeFromSystem': instance.useMaterialThemeFromSystem, + 'customThemeColor': instance.customThemeColor, 'useMaterialThemeOnItemPage': instance.useMaterialThemeOnItemPage, 'useCurrentPlayerThemeThroughoutApp': instance.useCurrentPlayerThemeThroughoutApp, }; -_$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map json) => - _$PlayerSettingsImpl( - miniPlayerSettings: json['miniPlayerSettings'] == null - ? const MinimizedPlayerSettings() - : MinimizedPlayerSettings.fromJson( - json['miniPlayerSettings'] as Map), - expandedPlayerSettings: json['expandedPlayerSettings'] == null - ? const ExpandedPlayerSettings() - : ExpandedPlayerSettings.fromJson( - json['expandedPlayerSettings'] as Map), - preferredDefaultVolume: - (json['preferredDefaultVolume'] as num?)?.toDouble() ?? 1, - preferredDefaultSpeed: - (json['preferredDefaultSpeed'] as num?)?.toDouble() ?? 1, - speedOptions: (json['speedOptions'] as List?) - ?.map((e) => (e as num).toDouble()) - .toList() ?? - const [0.75, 1, 1.25, 1.5, 1.75, 2], - sleepTimerSettings: json['sleepTimerSettings'] == null - ? const SleepTimerSettings() - : SleepTimerSettings.fromJson( - json['sleepTimerSettings'] as Map), - playbackReportInterval: json['playbackReportInterval'] == null - ? const Duration(seconds: 10) - : Duration( - microseconds: (json['playbackReportInterval'] as num).toInt()), - configurePlayerForEveryBook: - json['configurePlayerForEveryBook'] as bool? ?? true, - ); +const _$ThemeModeEnumMap = { + ThemeMode.system: 'system', + ThemeMode.light: 'light', + ThemeMode.dark: 'dark', +}; -Map _$$PlayerSettingsImplToJson( - _$PlayerSettingsImpl instance) => - { - 'miniPlayerSettings': instance.miniPlayerSettings, - 'expandedPlayerSettings': instance.expandedPlayerSettings, - 'preferredDefaultVolume': instance.preferredDefaultVolume, - 'preferredDefaultSpeed': instance.preferredDefaultSpeed, - 'speedOptions': instance.speedOptions, - 'sleepTimerSettings': instance.sleepTimerSettings, - 'playbackReportInterval': instance.playbackReportInterval.inMicroseconds, - 'configurePlayerForEveryBook': instance.configurePlayerForEveryBook, - }; +_PlayerSettings _$PlayerSettingsFromJson( + Map json, +) => _PlayerSettings( + miniPlayerSettings: json['miniPlayerSettings'] == null + ? const MinimizedPlayerSettings() + : MinimizedPlayerSettings.fromJson( + json['miniPlayerSettings'] as Map, + ), + expandedPlayerSettings: json['expandedPlayerSettings'] == null + ? const ExpandedPlayerSettings() + : ExpandedPlayerSettings.fromJson( + json['expandedPlayerSettings'] as Map, + ), + preferredDefaultVolume: + (json['preferredDefaultVolume'] as num?)?.toDouble() ?? 1, + preferredDefaultSpeed: + (json['preferredDefaultSpeed'] as num?)?.toDouble() ?? 1, + speedOptions: + (json['speedOptions'] as List?) + ?.map((e) => (e as num).toDouble()) + .toList() ?? + const [1, 1.25, 1.5, 1.75, 2], + speedIncrement: (json['speedIncrement'] as num?)?.toDouble() ?? 0.05, + minSpeed: (json['minSpeed'] as num?)?.toDouble() ?? 0.1, + maxSpeed: (json['maxSpeed'] as num?)?.toDouble() ?? 4, + minimumPositionForReporting: json['minimumPositionForReporting'] == null + ? const Duration(seconds: 10) + : Duration( + microseconds: (json['minimumPositionForReporting'] as num).toInt(), + ), + playbackReportInterval: json['playbackReportInterval'] == null + ? const Duration(seconds: 10) + : Duration(microseconds: (json['playbackReportInterval'] as num).toInt()), + markCompleteWhenTimeLeft: json['markCompleteWhenTimeLeft'] == null + ? const Duration(seconds: 15) + : Duration( + microseconds: (json['markCompleteWhenTimeLeft'] as num).toInt(), + ), + configurePlayerForEveryBook: + json['configurePlayerForEveryBook'] as bool? ?? true, +); -_$ExpandedPlayerSettingsImpl _$$ExpandedPlayerSettingsImplFromJson( - Map json) => - _$ExpandedPlayerSettingsImpl( - showTotalProgress: json['showTotalProgress'] as bool? ?? false, - showChapterProgress: json['showChapterProgress'] as bool? ?? true, - ); +Map _$PlayerSettingsToJson( + _PlayerSettings instance, +) => { + 'miniPlayerSettings': instance.miniPlayerSettings, + 'expandedPlayerSettings': instance.expandedPlayerSettings, + 'preferredDefaultVolume': instance.preferredDefaultVolume, + 'preferredDefaultSpeed': instance.preferredDefaultSpeed, + 'speedOptions': instance.speedOptions, + 'speedIncrement': instance.speedIncrement, + 'minSpeed': instance.minSpeed, + 'maxSpeed': instance.maxSpeed, + 'minimumPositionForReporting': + instance.minimumPositionForReporting.inMicroseconds, + 'playbackReportInterval': instance.playbackReportInterval.inMicroseconds, + 'markCompleteWhenTimeLeft': instance.markCompleteWhenTimeLeft.inMicroseconds, + 'configurePlayerForEveryBook': instance.configurePlayerForEveryBook, +}; -Map _$$ExpandedPlayerSettingsImplToJson( - _$ExpandedPlayerSettingsImpl instance) => - { - 'showTotalProgress': instance.showTotalProgress, - 'showChapterProgress': instance.showChapterProgress, - }; +_ExpandedPlayerSettings _$ExpandedPlayerSettingsFromJson( + Map json, +) => _ExpandedPlayerSettings( + showTotalProgress: json['showTotalProgress'] as bool? ?? false, + showChapterProgress: json['showChapterProgress'] as bool? ?? true, +); -_$MinimizedPlayerSettingsImpl _$$MinimizedPlayerSettingsImplFromJson( - Map json) => - _$MinimizedPlayerSettingsImpl( - useChapterInfo: json['useChapterInfo'] as bool? ?? false, - ); +Map _$ExpandedPlayerSettingsToJson( + _ExpandedPlayerSettings instance, +) => { + 'showTotalProgress': instance.showTotalProgress, + 'showChapterProgress': instance.showChapterProgress, +}; -Map _$$MinimizedPlayerSettingsImplToJson( - _$MinimizedPlayerSettingsImpl instance) => - { - 'useChapterInfo': instance.useChapterInfo, - }; +_MinimizedPlayerSettings _$MinimizedPlayerSettingsFromJson( + Map json, +) => _MinimizedPlayerSettings( + useChapterInfo: json['useChapterInfo'] as bool? ?? false, +); -_$SleepTimerSettingsImpl _$$SleepTimerSettingsImplFromJson( - Map json) => - _$SleepTimerSettingsImpl( +Map _$MinimizedPlayerSettingsToJson( + _MinimizedPlayerSettings instance, +) => {'useChapterInfo': instance.useChapterInfo}; + +_SleepTimerSettings _$SleepTimerSettingsFromJson(Map json) => + _SleepTimerSettings( defaultDuration: json['defaultDuration'] == null ? const Duration(minutes: 15) : Duration(microseconds: (json['defaultDuration'] as num).toInt()), - shakeSenseMode: $enumDecodeNullable( - _$SleepTimerShakeSenseModeEnumMap, json['shakeSenseMode']) ?? - SleepTimerShakeSenseMode.always, - shakeSenseDuration: json['shakeSenseDuration'] == null - ? const Duration(seconds: 30) - : Duration(microseconds: (json['shakeSenseDuration'] as num).toInt()), - vibrateWhenReset: json['vibrateWhenReset'] as bool? ?? true, - beepWhenReset: json['beepWhenReset'] as bool? ?? false, + presetDurations: + (json['presetDurations'] as List?) + ?.map((e) => Duration(microseconds: (e as num).toInt())) + .toList() ?? + const [ + Duration(minutes: 5), + Duration(minutes: 10), + Duration(minutes: 15), + Duration(minutes: 20), + Duration(minutes: 30), + ], + maxDuration: json['maxDuration'] == null + ? const Duration(minutes: 100) + : Duration(microseconds: (json['maxDuration'] as num).toInt()), fadeOutAudio: json['fadeOutAudio'] as bool? ?? false, - shakeDetectThreshold: - (json['shakeDetectThreshold'] as num?)?.toDouble() ?? 0.5, + fadeOutDuration: json['fadeOutDuration'] == null + ? const Duration(seconds: 20) + : Duration(microseconds: (json['fadeOutDuration'] as num).toInt()), autoRewindWhenStopped: json['autoRewindWhenStopped'] as bool? ?? false, autoRewindDurations: (json['autoRewindDurations'] as Map?)?.map( - (k, e) => MapEntry( - int.parse(k), Duration(microseconds: (e as num).toInt())), - ) ?? - const { - 5: Duration(seconds: 10), - 15: Duration(seconds: 30), - 45: Duration(seconds: 45), - 60: Duration(minutes: 1), - 120: Duration(minutes: 2) - }, + (k, e) => MapEntry( + int.parse(k), + Duration(microseconds: (e as num).toInt()), + ), + ) ?? + const { + 5: Duration(seconds: 10), + 15: Duration(seconds: 30), + 45: Duration(seconds: 45), + 60: Duration(minutes: 1), + 120: Duration(minutes: 2), + }, autoTurnOnTimer: json['autoTurnOnTimer'] as bool? ?? false, - alwaysAutoTurnOnTimer: json['alwaysAutoTurnOnTimer'] as bool? ?? true, + alwaysAutoTurnOnTimer: json['alwaysAutoTurnOnTimer'] as bool? ?? false, autoTurnOnTime: json['autoTurnOnTime'] == null ? const Duration(hours: 22, minutes: 0) : Duration(microseconds: (json['autoTurnOnTime'] as num).toInt()), @@ -155,35 +215,27 @@ _$SleepTimerSettingsImpl _$$SleepTimerSettingsImplFromJson( : Duration(microseconds: (json['autoTurnOffTime'] as num).toInt()), ); -Map _$$SleepTimerSettingsImplToJson( - _$SleepTimerSettingsImpl instance) => +Map _$SleepTimerSettingsToJson(_SleepTimerSettings instance) => { 'defaultDuration': instance.defaultDuration.inMicroseconds, - 'shakeSenseMode': - _$SleepTimerShakeSenseModeEnumMap[instance.shakeSenseMode]!, - 'shakeSenseDuration': instance.shakeSenseDuration.inMicroseconds, - 'vibrateWhenReset': instance.vibrateWhenReset, - 'beepWhenReset': instance.beepWhenReset, + 'presetDurations': instance.presetDurations + .map((e) => e.inMicroseconds) + .toList(), + 'maxDuration': instance.maxDuration.inMicroseconds, 'fadeOutAudio': instance.fadeOutAudio, - 'shakeDetectThreshold': instance.shakeDetectThreshold, + 'fadeOutDuration': instance.fadeOutDuration.inMicroseconds, 'autoRewindWhenStopped': instance.autoRewindWhenStopped, - 'autoRewindDurations': instance.autoRewindDurations - .map((k, e) => MapEntry(k.toString(), e.inMicroseconds)), + 'autoRewindDurations': instance.autoRewindDurations.map( + (k, e) => MapEntry(k.toString(), e.inMicroseconds), + ), 'autoTurnOnTimer': instance.autoTurnOnTimer, 'alwaysAutoTurnOnTimer': instance.alwaysAutoTurnOnTimer, 'autoTurnOnTime': instance.autoTurnOnTime.inMicroseconds, 'autoTurnOffTime': instance.autoTurnOffTime.inMicroseconds, }; -const _$SleepTimerShakeSenseModeEnumMap = { - SleepTimerShakeSenseMode.never: 'never', - SleepTimerShakeSenseMode.always: 'always', - SleepTimerShakeSenseMode.nearEnds: 'nearEnds', -}; - -_$DownloadSettingsImpl _$$DownloadSettingsImplFromJson( - Map json) => - _$DownloadSettingsImpl( +_DownloadSettings _$DownloadSettingsFromJson(Map json) => + _DownloadSettings( requiresWiFi: json['requiresWiFi'] as bool? ?? true, retries: (json['retries'] as num?)?.toInt() ?? 3, allowPause: json['allowPause'] as bool? ?? true, @@ -193,8 +245,7 @@ _$DownloadSettingsImpl _$$DownloadSettingsImplFromJson( (json['maxConcurrentByGroup'] as num?)?.toInt() ?? 3, ); -Map _$$DownloadSettingsImplToJson( - _$DownloadSettingsImpl instance) => +Map _$DownloadSettingsToJson(_DownloadSettings instance) => { 'requiresWiFi': instance.requiresWiFi, 'retries': instance.retries, @@ -203,3 +254,134 @@ Map _$$DownloadSettingsImplToJson( 'maxConcurrentByHost': instance.maxConcurrentByHost, 'maxConcurrentByGroup': instance.maxConcurrentByGroup, }; + +_NotificationSettings _$NotificationSettingsFromJson( + Map json, +) => _NotificationSettings( + fastForwardInterval: json['fastForwardInterval'] == null + ? const Duration(seconds: 30) + : Duration(microseconds: (json['fastForwardInterval'] as num).toInt()), + rewindInterval: json['rewindInterval'] == null + ? const Duration(seconds: 10) + : Duration(microseconds: (json['rewindInterval'] as num).toInt()), + progressBarIsChapterProgress: + json['progressBarIsChapterProgress'] as bool? ?? true, + primaryTitle: json['primaryTitle'] as String? ?? '\$bookTitle', + secondaryTitle: json['secondaryTitle'] as String? ?? '\$author', + mediaControls: + (json['mediaControls'] as List?) + ?.map((e) => $enumDecode(_$NotificationMediaControlEnumMap, e)) + .toList() ?? + const [ + NotificationMediaControl.rewind, + NotificationMediaControl.fastForward, + NotificationMediaControl.skipToPreviousChapter, + NotificationMediaControl.skipToNextChapter, + ], +); + +Map _$NotificationSettingsToJson( + _NotificationSettings instance, +) => { + 'fastForwardInterval': instance.fastForwardInterval.inMicroseconds, + 'rewindInterval': instance.rewindInterval.inMicroseconds, + 'progressBarIsChapterProgress': instance.progressBarIsChapterProgress, + 'primaryTitle': instance.primaryTitle, + 'secondaryTitle': instance.secondaryTitle, + 'mediaControls': instance.mediaControls + .map((e) => _$NotificationMediaControlEnumMap[e]!) + .toList(), +}; + +const _$NotificationMediaControlEnumMap = { + NotificationMediaControl.fastForward: 'fastForward', + NotificationMediaControl.rewind: 'rewind', + NotificationMediaControl.speedToggle: 'speedToggle', + NotificationMediaControl.stop: 'stop', + NotificationMediaControl.skipToNextChapter: 'skipToNextChapter', + NotificationMediaControl.skipToPreviousChapter: 'skipToPreviousChapter', +}; + +_ShakeDetectionSettings _$ShakeDetectionSettingsFromJson( + Map json, +) => _ShakeDetectionSettings( + isEnabled: json['isEnabled'] as bool? ?? true, + direction: + $enumDecodeNullable(_$ShakeDirectionEnumMap, json['direction']) ?? + ShakeDirection.horizontal, + threshold: (json['threshold'] as num?)?.toDouble() ?? 5, + shakeAction: + $enumDecodeNullable(_$ShakeActionEnumMap, json['shakeAction']) ?? + ShakeAction.resetSleepTimer, + feedback: + (json['feedback'] as List?) + ?.map((e) => $enumDecode(_$ShakeDetectedFeedbackEnumMap, e)) + .toSet() ?? + const {ShakeDetectedFeedback.vibrate}, + beepVolume: (json['beepVolume'] as num?)?.toDouble() ?? 0.5, + shakeTriggerCoolDown: json['shakeTriggerCoolDown'] == null + ? const Duration(seconds: 2) + : Duration(microseconds: (json['shakeTriggerCoolDown'] as num).toInt()), + shakeTriggerCount: (json['shakeTriggerCount'] as num?)?.toInt() ?? 2, + samplingPeriod: json['samplingPeriod'] == null + ? const Duration(milliseconds: 100) + : Duration(microseconds: (json['samplingPeriod'] as num).toInt()), +); + +Map _$ShakeDetectionSettingsToJson( + _ShakeDetectionSettings instance, +) => { + 'isEnabled': instance.isEnabled, + 'direction': _$ShakeDirectionEnumMap[instance.direction]!, + 'threshold': instance.threshold, + 'shakeAction': _$ShakeActionEnumMap[instance.shakeAction]!, + 'feedback': instance.feedback + .map((e) => _$ShakeDetectedFeedbackEnumMap[e]!) + .toList(), + 'beepVolume': instance.beepVolume, + 'shakeTriggerCoolDown': instance.shakeTriggerCoolDown.inMicroseconds, + 'shakeTriggerCount': instance.shakeTriggerCount, + 'samplingPeriod': instance.samplingPeriod.inMicroseconds, +}; + +const _$ShakeDirectionEnumMap = { + ShakeDirection.horizontal: 'horizontal', + ShakeDirection.vertical: 'vertical', +}; + +const _$ShakeActionEnumMap = { + ShakeAction.none: 'none', + ShakeAction.playPause: 'playPause', + ShakeAction.resetSleepTimer: 'resetSleepTimer', + ShakeAction.fastForward: 'fastForward', + ShakeAction.rewind: 'rewind', +}; + +const _$ShakeDetectedFeedbackEnumMap = { + ShakeDetectedFeedback.vibrate: 'vibrate', + ShakeDetectedFeedback.beep: 'beep', +}; + +_HomePageSettings _$HomePageSettingsFromJson(Map json) => + _HomePageSettings( + showPlayButtonOnContinueListeningShelf: + json['showPlayButtonOnContinueListeningShelf'] as bool? ?? true, + showPlayButtonOnContinueSeriesShelf: + json['showPlayButtonOnContinueSeriesShelf'] as bool? ?? false, + showPlayButtonOnAllRemainingShelves: + json['showPlayButtonOnAllRemainingShelves'] as bool? ?? false, + showPlayButtonOnListenAgainShelf: + json['showPlayButtonOnListenAgainShelf'] as bool? ?? false, + ); + +Map _$HomePageSettingsToJson( + _HomePageSettings instance, +) => { + 'showPlayButtonOnContinueListeningShelf': + instance.showPlayButtonOnContinueListeningShelf, + 'showPlayButtonOnContinueSeriesShelf': + instance.showPlayButtonOnContinueSeriesShelf, + 'showPlayButtonOnAllRemainingShelves': + instance.showPlayButtonOnAllRemainingShelves, + 'showPlayButtonOnListenAgainShelf': instance.showPlayButtonOnListenAgainShelf, +}; diff --git a/lib/settings/models/audiobookshelf_server.dart b/lib/settings/models/audiobookshelf_server.dart index 0c97e41..cbc14a2 100644 --- a/lib/settings/models/audiobookshelf_server.dart +++ b/lib/settings/models/audiobookshelf_server.dart @@ -7,7 +7,7 @@ typedef AudiobookShelfUri = Uri; /// Represents a audiobookshelf server @freezed -class AudiobookShelfServer with _$AudiobookShelfServer { +sealed class AudiobookShelfServer with _$AudiobookShelfServer { const factory AudiobookShelfServer({ required AudiobookShelfUri serverUrl, // String? serverName, diff --git a/lib/settings/models/audiobookshelf_server.freezed.dart b/lib/settings/models/audiobookshelf_server.freezed.dart index 39f095a..26ec5c4 100644 --- a/lib/settings/models/audiobookshelf_server.freezed.dart +++ b/lib/settings/models/audiobookshelf_server.freezed.dart @@ -1,5 +1,5 @@ -// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file // 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 @@ -9,161 +9,263 @@ part of 'audiobookshelf_server.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -AudiobookShelfServer _$AudiobookShelfServerFromJson(Map json) { - return _AudiobookShelfServer.fromJson(json); -} - /// @nodoc mixin _$AudiobookShelfServer { - Uri get serverUrl => throw _privateConstructorUsedError; + + AudiobookShelfUri get serverUrl; +/// Create a copy of AudiobookShelfServer +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AudiobookShelfServerCopyWith get copyWith => _$AudiobookShelfServerCopyWithImpl(this as AudiobookShelfServer, _$identity); /// Serializes this AudiobookShelfServer to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AudiobookShelfServer&&(identical(other.serverUrl, serverUrl) || other.serverUrl == serverUrl)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,serverUrl); + +@override +String toString() { + return 'AudiobookShelfServer(serverUrl: $serverUrl)'; +} + - /// Create a copy of AudiobookShelfServer - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $AudiobookShelfServerCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $AudiobookShelfServerCopyWith<$Res> { - factory $AudiobookShelfServerCopyWith(AudiobookShelfServer value, - $Res Function(AudiobookShelfServer) then) = - _$AudiobookShelfServerCopyWithImpl<$Res, AudiobookShelfServer>; - @useResult - $Res call({Uri serverUrl}); -} +abstract mixin class $AudiobookShelfServerCopyWith<$Res> { + factory $AudiobookShelfServerCopyWith(AudiobookShelfServer value, $Res Function(AudiobookShelfServer) _then) = _$AudiobookShelfServerCopyWithImpl; +@useResult +$Res call({ + AudiobookShelfUri serverUrl +}); + + + +} /// @nodoc -class _$AudiobookShelfServerCopyWithImpl<$Res, - $Val extends AudiobookShelfServer> +class _$AudiobookShelfServerCopyWithImpl<$Res> implements $AudiobookShelfServerCopyWith<$Res> { - _$AudiobookShelfServerCopyWithImpl(this._value, this._then); + _$AudiobookShelfServerCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final AudiobookShelfServer _self; + final $Res Function(AudiobookShelfServer) _then; - /// Create a copy of AudiobookShelfServer - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? serverUrl = null, - }) { - return _then(_value.copyWith( - serverUrl: null == serverUrl - ? _value.serverUrl - : serverUrl // ignore: cast_nullable_to_non_nullable - as Uri, - ) as $Val); - } +/// Create a copy of AudiobookShelfServer +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? serverUrl = null,}) { + return _then(_self.copyWith( +serverUrl: null == serverUrl ? _self.serverUrl : serverUrl // ignore: cast_nullable_to_non_nullable +as AudiobookShelfUri, + )); } -/// @nodoc -abstract class _$$AudiobookShelfServerImplCopyWith<$Res> - implements $AudiobookShelfServerCopyWith<$Res> { - factory _$$AudiobookShelfServerImplCopyWith(_$AudiobookShelfServerImpl value, - $Res Function(_$AudiobookShelfServerImpl) then) = - __$$AudiobookShelfServerImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({Uri serverUrl}); } -/// @nodoc -class __$$AudiobookShelfServerImplCopyWithImpl<$Res> - extends _$AudiobookShelfServerCopyWithImpl<$Res, _$AudiobookShelfServerImpl> - implements _$$AudiobookShelfServerImplCopyWith<$Res> { - __$$AudiobookShelfServerImplCopyWithImpl(_$AudiobookShelfServerImpl _value, - $Res Function(_$AudiobookShelfServerImpl) _then) - : super(_value, _then); - /// Create a copy of AudiobookShelfServer - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? serverUrl = null, - }) { - return _then(_$AudiobookShelfServerImpl( - serverUrl: null == serverUrl - ? _value.serverUrl - : serverUrl // ignore: cast_nullable_to_non_nullable - as Uri, - )); - } +/// Adds pattern-matching-related methods to [AudiobookShelfServer]. +extension AudiobookShelfServerPatterns on AudiobookShelfServer { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _AudiobookShelfServer value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AudiobookShelfServer() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _AudiobookShelfServer value) $default,){ +final _that = this; +switch (_that) { +case _AudiobookShelfServer(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AudiobookShelfServer value)? $default,){ +final _that = this; +switch (_that) { +case _AudiobookShelfServer() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( AudiobookShelfUri serverUrl)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AudiobookShelfServer() when $default != null: +return $default(_that.serverUrl);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( AudiobookShelfUri serverUrl) $default,) {final _that = this; +switch (_that) { +case _AudiobookShelfServer(): +return $default(_that.serverUrl);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( AudiobookShelfUri serverUrl)? $default,) {final _that = this; +switch (_that) { +case _AudiobookShelfServer() when $default != null: +return $default(_that.serverUrl);case _: + return null; + +} +} + } /// @nodoc @JsonSerializable() -class _$AudiobookShelfServerImpl implements _AudiobookShelfServer { - const _$AudiobookShelfServerImpl({required this.serverUrl}); - factory _$AudiobookShelfServerImpl.fromJson(Map json) => - _$$AudiobookShelfServerImplFromJson(json); +class _AudiobookShelfServer implements AudiobookShelfServer { + const _AudiobookShelfServer({required this.serverUrl}); + factory _AudiobookShelfServer.fromJson(Map json) => _$AudiobookShelfServerFromJson(json); - @override - final Uri serverUrl; +@override final AudiobookShelfUri serverUrl; - @override - String toString() { - return 'AudiobookShelfServer(serverUrl: $serverUrl)'; - } +/// Create a copy of AudiobookShelfServer +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AudiobookShelfServerCopyWith<_AudiobookShelfServer> get copyWith => __$AudiobookShelfServerCopyWithImpl<_AudiobookShelfServer>(this, _$identity); - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$AudiobookShelfServerImpl && - (identical(other.serverUrl, serverUrl) || - other.serverUrl == serverUrl)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => Object.hash(runtimeType, serverUrl); - - /// Create a copy of AudiobookShelfServer - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl> - get copyWith => - __$$AudiobookShelfServerImplCopyWithImpl<_$AudiobookShelfServerImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$AudiobookShelfServerImplToJson( - this, - ); - } +@override +Map toJson() { + return _$AudiobookShelfServerToJson(this, ); } -abstract class _AudiobookShelfServer implements AudiobookShelfServer { - const factory _AudiobookShelfServer({required final Uri serverUrl}) = - _$AudiobookShelfServerImpl; - - factory _AudiobookShelfServer.fromJson(Map json) = - _$AudiobookShelfServerImpl.fromJson; - - @override - Uri get serverUrl; - - /// Create a copy of AudiobookShelfServer - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$AudiobookShelfServerImplCopyWith<_$AudiobookShelfServerImpl> - get copyWith => throw _privateConstructorUsedError; +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AudiobookShelfServer&&(identical(other.serverUrl, serverUrl) || other.serverUrl == serverUrl)); } + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,serverUrl); + +@override +String toString() { + return 'AudiobookShelfServer(serverUrl: $serverUrl)'; +} + + +} + +/// @nodoc +abstract mixin class _$AudiobookShelfServerCopyWith<$Res> implements $AudiobookShelfServerCopyWith<$Res> { + factory _$AudiobookShelfServerCopyWith(_AudiobookShelfServer value, $Res Function(_AudiobookShelfServer) _then) = __$AudiobookShelfServerCopyWithImpl; +@override @useResult +$Res call({ + AudiobookShelfUri serverUrl +}); + + + + +} +/// @nodoc +class __$AudiobookShelfServerCopyWithImpl<$Res> + implements _$AudiobookShelfServerCopyWith<$Res> { + __$AudiobookShelfServerCopyWithImpl(this._self, this._then); + + final _AudiobookShelfServer _self; + final $Res Function(_AudiobookShelfServer) _then; + +/// Create a copy of AudiobookShelfServer +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? serverUrl = null,}) { + return _then(_AudiobookShelfServer( +serverUrl: null == serverUrl ? _self.serverUrl : serverUrl // ignore: cast_nullable_to_non_nullable +as AudiobookShelfUri, + )); +} + + +} + +// dart format on diff --git a/lib/settings/models/audiobookshelf_server.g.dart b/lib/settings/models/audiobookshelf_server.g.dart index a876683..e8504ec 100644 --- a/lib/settings/models/audiobookshelf_server.g.dart +++ b/lib/settings/models/audiobookshelf_server.g.dart @@ -6,14 +6,10 @@ part of 'audiobookshelf_server.dart'; // JsonSerializableGenerator // ************************************************************************** -_$AudiobookShelfServerImpl _$$AudiobookShelfServerImplFromJson( - Map json) => - _$AudiobookShelfServerImpl( - serverUrl: Uri.parse(json['serverUrl'] as String), - ); +_AudiobookShelfServer _$AudiobookShelfServerFromJson( + Map json, +) => _AudiobookShelfServer(serverUrl: Uri.parse(json['serverUrl'] as String)); -Map _$$AudiobookShelfServerImplToJson( - _$AudiobookShelfServerImpl instance) => - { - 'serverUrl': instance.serverUrl.toString(), - }; +Map _$AudiobookShelfServerToJson( + _AudiobookShelfServer instance, +) => {'serverUrl': instance.serverUrl.toString()}; diff --git a/lib/settings/models/authenticated_user.dart b/lib/settings/models/authenticated_user.dart index 04f2bd0..97613ab 100644 --- a/lib/settings/models/authenticated_user.dart +++ b/lib/settings/models/authenticated_user.dart @@ -6,13 +6,12 @@ part 'authenticated_user.g.dart'; /// authenticated user with server and credentials @freezed -class AuthenticatedUser with _$AuthenticatedUser { +sealed class AuthenticatedUser with _$AuthenticatedUser { const factory AuthenticatedUser({ required AudiobookShelfServer server, required String authToken, - String? id, + required String id, String? username, - String? password, }) = _AuthenticatedUser; factory AuthenticatedUser.fromJson(Map json) => diff --git a/lib/settings/models/authenticated_user.freezed.dart b/lib/settings/models/authenticated_user.freezed.dart index e582928..3010150 100644 --- a/lib/settings/models/authenticated_user.freezed.dart +++ b/lib/settings/models/authenticated_user.freezed.dart @@ -1,5 +1,5 @@ -// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file // 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 @@ -9,260 +9,290 @@ part of 'authenticated_user.dart'; // FreezedGenerator // ************************************************************************** +// dart format off 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'); - -AuthenticatedUser _$AuthenticatedUserFromJson(Map json) { - return _AuthenticatedUser.fromJson(json); -} - /// @nodoc mixin _$AuthenticatedUser { - AudiobookShelfServer get server => throw _privateConstructorUsedError; - String get authToken => throw _privateConstructorUsedError; - String? get id => throw _privateConstructorUsedError; - String? get username => throw _privateConstructorUsedError; - String? get password => throw _privateConstructorUsedError; + + AudiobookShelfServer get server; String get authToken; String get id; String? get username; +/// Create a copy of AuthenticatedUser +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$AuthenticatedUserCopyWith get copyWith => _$AuthenticatedUserCopyWithImpl(this as AuthenticatedUser, _$identity); /// Serializes this AuthenticatedUser to a JSON map. - Map toJson() => throw _privateConstructorUsedError; + Map toJson(); + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is AuthenticatedUser&&(identical(other.server, server) || other.server == server)&&(identical(other.authToken, authToken) || other.authToken == authToken)&&(identical(other.id, id) || other.id == id)&&(identical(other.username, username) || other.username == username)); +} + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,server,authToken,id,username); + +@override +String toString() { + return 'AuthenticatedUser(server: $server, authToken: $authToken, id: $id, username: $username)'; +} + - /// Create a copy of AuthenticatedUser - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $AuthenticatedUserCopyWith get copyWith => - throw _privateConstructorUsedError; } /// @nodoc -abstract class $AuthenticatedUserCopyWith<$Res> { - factory $AuthenticatedUserCopyWith( - AuthenticatedUser value, $Res Function(AuthenticatedUser) then) = - _$AuthenticatedUserCopyWithImpl<$Res, AuthenticatedUser>; - @useResult - $Res call( - {AudiobookShelfServer server, - String authToken, - String? id, - String? username, - String? password}); +abstract mixin class $AuthenticatedUserCopyWith<$Res> { + factory $AuthenticatedUserCopyWith(AuthenticatedUser value, $Res Function(AuthenticatedUser) _then) = _$AuthenticatedUserCopyWithImpl; +@useResult +$Res call({ + AudiobookShelfServer server, String authToken, String id, String? username +}); + + +$AudiobookShelfServerCopyWith<$Res> get server; - $AudiobookShelfServerCopyWith<$Res> get server; } - /// @nodoc -class _$AuthenticatedUserCopyWithImpl<$Res, $Val extends AuthenticatedUser> +class _$AuthenticatedUserCopyWithImpl<$Res> implements $AuthenticatedUserCopyWith<$Res> { - _$AuthenticatedUserCopyWithImpl(this._value, this._then); + _$AuthenticatedUserCopyWithImpl(this._self, this._then); - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; + final AuthenticatedUser _self; + final $Res Function(AuthenticatedUser) _then; - /// Create a copy of AuthenticatedUser - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? server = null, - Object? authToken = null, - Object? id = freezed, - Object? username = freezed, - Object? password = freezed, - }) { - return _then(_value.copyWith( - server: null == server - ? _value.server - : server // ignore: cast_nullable_to_non_nullable - as AudiobookShelfServer, - authToken: null == authToken - ? _value.authToken - : authToken // ignore: cast_nullable_to_non_nullable - as String, - id: freezed == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String?, - username: freezed == username - ? _value.username - : username // ignore: cast_nullable_to_non_nullable - as String?, - password: freezed == password - ? _value.password - : password // ignore: cast_nullable_to_non_nullable - as String?, - ) as $Val); - } - - /// Create a copy of AuthenticatedUser - /// with the given fields replaced by the non-null parameter values. - @override - @pragma('vm:prefer-inline') - $AudiobookShelfServerCopyWith<$Res> get server { - return $AudiobookShelfServerCopyWith<$Res>(_value.server, (value) { - return _then(_value.copyWith(server: value) as $Val); - }); - } +/// Create a copy of AuthenticatedUser +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? server = null,Object? authToken = null,Object? id = null,Object? username = freezed,}) { + return _then(_self.copyWith( +server: null == server ? _self.server : server // ignore: cast_nullable_to_non_nullable +as AudiobookShelfServer,authToken: null == authToken ? _self.authToken : authToken // ignore: cast_nullable_to_non_nullable +as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,username: freezed == username ? _self.username : username // ignore: cast_nullable_to_non_nullable +as String?, + )); +} +/// Create a copy of AuthenticatedUser +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$AudiobookShelfServerCopyWith<$Res> get server { + + return $AudiobookShelfServerCopyWith<$Res>(_self.server, (value) { + return _then(_self.copyWith(server: value)); + }); +} } -/// @nodoc -abstract class _$$AuthenticatedUserImplCopyWith<$Res> - implements $AuthenticatedUserCopyWith<$Res> { - factory _$$AuthenticatedUserImplCopyWith(_$AuthenticatedUserImpl value, - $Res Function(_$AuthenticatedUserImpl) then) = - __$$AuthenticatedUserImplCopyWithImpl<$Res>; - @override - @useResult - $Res call( - {AudiobookShelfServer server, - String authToken, - String? id, - String? username, - String? password}); - @override - $AudiobookShelfServerCopyWith<$Res> get server; +/// Adds pattern-matching-related methods to [AuthenticatedUser]. +extension AuthenticatedUserPatterns on AuthenticatedUser { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _AuthenticatedUser value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _AuthenticatedUser() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _AuthenticatedUser value) $default,){ +final _that = this; +switch (_that) { +case _AuthenticatedUser(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _AuthenticatedUser value)? $default,){ +final _that = this; +switch (_that) { +case _AuthenticatedUser() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( AudiobookShelfServer server, String authToken, String id, String? username)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _AuthenticatedUser() when $default != null: +return $default(_that.server,_that.authToken,_that.id,_that.username);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( AudiobookShelfServer server, String authToken, String id, String? username) $default,) {final _that = this; +switch (_that) { +case _AuthenticatedUser(): +return $default(_that.server,_that.authToken,_that.id,_that.username);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( AudiobookShelfServer server, String authToken, String id, String? username)? $default,) {final _that = this; +switch (_that) { +case _AuthenticatedUser() when $default != null: +return $default(_that.server,_that.authToken,_that.id,_that.username);case _: + return null; + +} } -/// @nodoc -class __$$AuthenticatedUserImplCopyWithImpl<$Res> - extends _$AuthenticatedUserCopyWithImpl<$Res, _$AuthenticatedUserImpl> - implements _$$AuthenticatedUserImplCopyWith<$Res> { - __$$AuthenticatedUserImplCopyWithImpl(_$AuthenticatedUserImpl _value, - $Res Function(_$AuthenticatedUserImpl) _then) - : super(_value, _then); - - /// Create a copy of AuthenticatedUser - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? server = null, - Object? authToken = null, - Object? id = freezed, - Object? username = freezed, - Object? password = freezed, - }) { - return _then(_$AuthenticatedUserImpl( - server: null == server - ? _value.server - : server // ignore: cast_nullable_to_non_nullable - as AudiobookShelfServer, - authToken: null == authToken - ? _value.authToken - : authToken // ignore: cast_nullable_to_non_nullable - as String, - id: freezed == id - ? _value.id - : id // ignore: cast_nullable_to_non_nullable - as String?, - username: freezed == username - ? _value.username - : username // ignore: cast_nullable_to_non_nullable - as String?, - password: freezed == password - ? _value.password - : password // ignore: cast_nullable_to_non_nullable - as String?, - )); - } } /// @nodoc @JsonSerializable() -class _$AuthenticatedUserImpl implements _AuthenticatedUser { - const _$AuthenticatedUserImpl( - {required this.server, - required this.authToken, - this.id, - this.username, - this.password}); - factory _$AuthenticatedUserImpl.fromJson(Map json) => - _$$AuthenticatedUserImplFromJson(json); +class _AuthenticatedUser implements AuthenticatedUser { + const _AuthenticatedUser({required this.server, required this.authToken, required this.id, this.username}); + factory _AuthenticatedUser.fromJson(Map json) => _$AuthenticatedUserFromJson(json); - @override - final AudiobookShelfServer server; - @override - final String authToken; - @override - final String? id; - @override - final String? username; - @override - final String? password; +@override final AudiobookShelfServer server; +@override final String authToken; +@override final String id; +@override final String? username; - @override - String toString() { - return 'AuthenticatedUser(server: $server, authToken: $authToken, id: $id, username: $username, password: $password)'; - } +/// Create a copy of AuthenticatedUser +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$AuthenticatedUserCopyWith<_AuthenticatedUser> get copyWith => __$AuthenticatedUserCopyWithImpl<_AuthenticatedUser>(this, _$identity); - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$AuthenticatedUserImpl && - (identical(other.server, server) || other.server == server) && - (identical(other.authToken, authToken) || - other.authToken == authToken) && - (identical(other.id, id) || other.id == id) && - (identical(other.username, username) || - other.username == username) && - (identical(other.password, password) || - other.password == password)); - } - - @JsonKey(includeFromJson: false, includeToJson: false) - @override - int get hashCode => - Object.hash(runtimeType, server, authToken, id, username, password); - - /// Create a copy of AuthenticatedUser - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith => - __$$AuthenticatedUserImplCopyWithImpl<_$AuthenticatedUserImpl>( - this, _$identity); - - @override - Map toJson() { - return _$$AuthenticatedUserImplToJson( - this, - ); - } +@override +Map toJson() { + return _$AuthenticatedUserToJson(this, ); } -abstract class _AuthenticatedUser implements AuthenticatedUser { - const factory _AuthenticatedUser( - {required final AudiobookShelfServer server, - required final String authToken, - final String? id, - final String? username, - final String? password}) = _$AuthenticatedUserImpl; - - factory _AuthenticatedUser.fromJson(Map json) = - _$AuthenticatedUserImpl.fromJson; - - @override - AudiobookShelfServer get server; - @override - String get authToken; - @override - String? get id; - @override - String? get username; - @override - String? get password; - - /// Create a copy of AuthenticatedUser - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$AuthenticatedUserImplCopyWith<_$AuthenticatedUserImpl> get copyWith => - throw _privateConstructorUsedError; +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _AuthenticatedUser&&(identical(other.server, server) || other.server == server)&&(identical(other.authToken, authToken) || other.authToken == authToken)&&(identical(other.id, id) || other.id == id)&&(identical(other.username, username) || other.username == username)); } + +@JsonKey(includeFromJson: false, includeToJson: false) +@override +int get hashCode => Object.hash(runtimeType,server,authToken,id,username); + +@override +String toString() { + return 'AuthenticatedUser(server: $server, authToken: $authToken, id: $id, username: $username)'; +} + + +} + +/// @nodoc +abstract mixin class _$AuthenticatedUserCopyWith<$Res> implements $AuthenticatedUserCopyWith<$Res> { + factory _$AuthenticatedUserCopyWith(_AuthenticatedUser value, $Res Function(_AuthenticatedUser) _then) = __$AuthenticatedUserCopyWithImpl; +@override @useResult +$Res call({ + AudiobookShelfServer server, String authToken, String id, String? username +}); + + +@override $AudiobookShelfServerCopyWith<$Res> get server; + +} +/// @nodoc +class __$AuthenticatedUserCopyWithImpl<$Res> + implements _$AuthenticatedUserCopyWith<$Res> { + __$AuthenticatedUserCopyWithImpl(this._self, this._then); + + final _AuthenticatedUser _self; + final $Res Function(_AuthenticatedUser) _then; + +/// Create a copy of AuthenticatedUser +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? server = null,Object? authToken = null,Object? id = null,Object? username = freezed,}) { + return _then(_AuthenticatedUser( +server: null == server ? _self.server : server // ignore: cast_nullable_to_non_nullable +as AudiobookShelfServer,authToken: null == authToken ? _self.authToken : authToken // ignore: cast_nullable_to_non_nullable +as String,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable +as String,username: freezed == username ? _self.username : username // ignore: cast_nullable_to_non_nullable +as String?, + )); +} + +/// Create a copy of AuthenticatedUser +/// with the given fields replaced by the non-null parameter values. +@override +@pragma('vm:prefer-inline') +$AudiobookShelfServerCopyWith<$Res> get server { + + return $AudiobookShelfServerCopyWith<$Res>(_self.server, (value) { + return _then(_self.copyWith(server: value)); + }); +} +} + +// dart format on diff --git a/lib/settings/models/authenticated_user.g.dart b/lib/settings/models/authenticated_user.g.dart index 0752807..926c7d8 100644 --- a/lib/settings/models/authenticated_user.g.dart +++ b/lib/settings/models/authenticated_user.g.dart @@ -6,23 +6,20 @@ part of 'authenticated_user.dart'; // JsonSerializableGenerator // ************************************************************************** -_$AuthenticatedUserImpl _$$AuthenticatedUserImplFromJson( - Map json) => - _$AuthenticatedUserImpl( - server: - AudiobookShelfServer.fromJson(json['server'] as Map), +_AuthenticatedUser _$AuthenticatedUserFromJson(Map json) => + _AuthenticatedUser( + server: AudiobookShelfServer.fromJson( + json['server'] as Map, + ), authToken: json['authToken'] as String, - id: json['id'] as String?, + id: json['id'] as String, username: json['username'] as String?, - password: json['password'] as String?, ); -Map _$$AuthenticatedUserImplToJson( - _$AuthenticatedUserImpl instance) => +Map _$AuthenticatedUserToJson(_AuthenticatedUser instance) => { 'server': instance.server, 'authToken': instance.authToken, 'id': instance.id, 'username': instance.username, - 'password': instance.password, }; diff --git a/lib/settings/view/app_settings_page.dart b/lib/settings/view/app_settings_page.dart index 6cf2298..a193752 100644 --- a/lib/settings/view/app_settings_page.dart +++ b/lib/settings/view/app_settings_page.dart @@ -6,273 +6,281 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:vaani/api/authenticated_user_provider.dart'; -import 'package:vaani/api/server_provider.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:vaani/router/router.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/models/app_settings.dart' as model; +import 'package:vaani/settings/view/buttons.dart'; +import 'package:vaani/settings/view/simple_settings_page.dart'; +import 'package:vaani/settings/view/widgets/navigation_with_switch_tile.dart'; class AppSettingsPage extends HookConsumerWidget { - const AppSettingsPage({ - super.key, - }); + const AppSettingsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final appSettings = ref.watch(appSettingsProvider); - final registeredServers = ref.watch(audiobookShelfServerProvider); - final registeredServersAsList = registeredServers.toList(); - final availableUsers = ref.watch(authenticatedUserProvider); - final serverURIController = useTextEditingController(); - final sleepTimerSettings = appSettings.playerSettings.sleepTimerSettings; + final sleepTimerSettings = appSettings.sleepTimerSettings; - return Scaffold( - appBar: AppBar( - title: const Text('App Settings'), - ), - body: SettingsList( - sections: [ - // Appearance section - SettingsSection( - margin: const EdgeInsetsDirectional.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - title: Text( - 'Appearance', - style: Theme.of(context).textTheme.titleLarge, - ), - tiles: [ - SettingsTile.switchTile( - initialValue: appSettings.themeSettings.isDarkMode, - title: const Text('Dark Mode'), - description: const Text('we all know dark mode is better'), - leading: appSettings.themeSettings.isDarkMode - ? const Icon(Icons.dark_mode) - : const Icon(Icons.light_mode), - onToggle: (value) { - ref.read(appSettingsProvider.notifier).toggleDarkMode(); - }, - ), - SettingsTile.switchTile( - initialValue: - appSettings.themeSettings.useMaterialThemeOnItemPage, - title: const Text('Adaptive Theme on Item Page'), - description: const Text( - 'get fancy with the colors on the item page at the cost of some performance', - ), - leading: appSettings.themeSettings.useMaterialThemeOnItemPage - ? const Icon(Icons.auto_fix_high) - : const Icon(Icons.auto_fix_off), - onToggle: (value) { - ref.read(appSettingsProvider.notifier).update( - appSettings.copyWith.themeSettings( - useMaterialThemeOnItemPage: value, - ), - ); - }, - ), - ], + return SimpleSettingsPage( + title: const Text('App Settings'), + sections: [ + // General section + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, ), - - // Sleep Timer section - SettingsSection( - margin: const EdgeInsetsDirectional.symmetric( - horizontal: 16.0, - vertical: 8.0, + title: Text('General', style: Theme.of(context).textTheme.titleLarge), + tiles: [ + SettingsTile( + title: const Text('Player Settings'), + leading: const Icon(Icons.play_arrow), + description: const Text('Customize the player settings'), + onPressed: (context) { + context.pushNamed(Routes.playerSettings.name); + }, ), - title: Text( - 'Sleep Timer', - style: Theme.of(context).textTheme.titleLarge, - ), - tiles: [ - SettingsTile.navigation( - // initialValue: sleepTimerSettings.autoTurnOnTimer, - title: const Text('Auto Turn On Timer'), - description: const Text( - 'Automatically turn on the sleep timer based on the time of day', - ), - leading: sleepTimerSettings.autoTurnOnTimer - ? const Icon(Icons.timer) - : const Icon(Icons.timer_off), - onPressed: (context) { - // push the sleep timer settings page - context.pushNamed(Routes.autoSleepTimerSettings.name); - }, - // a switch to enable or disable the auto turn off time - trailing: IntrinsicHeight( - child: Row( - children: [ - VerticalDivider( - color: Theme.of(context).dividerColor.withOpacity(0.5), - indent: 8.0, - endIndent: 8.0, - // width: 8.0, - // thickness: 2.0, - // height: 24.0, + NavigationWithSwitchTile( + title: const Text('Auto Turn On Sleep Timer'), + description: const Text( + 'Automatically turn on the sleep timer based on the time of day', + ), + leading: sleepTimerSettings.autoTurnOnTimer + ? const Icon(Symbols.time_auto, fill: 1) + : const Icon(Symbols.timer_off, fill: 1), + onPressed: (context) { + context.pushNamed(Routes.autoSleepTimerSettings.name); + }, + value: sleepTimerSettings.autoTurnOnTimer, + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.sleepTimerSettings( + autoTurnOnTimer: value, ), - Switch( - value: sleepTimerSettings.autoTurnOnTimer, - onChanged: (value) { - ref.read(appSettingsProvider.notifier).update( - appSettings.copyWith.playerSettings - .sleepTimerSettings( - autoTurnOnTimer: value, - ), - ); - }, + ); + }, + ), + NavigationWithSwitchTile( + title: const Text('Shake Detector'), + leading: const Icon(Icons.vibration), + description: const Text('Customize the shake detector settings'), + value: appSettings.shakeDetectionSettings.isEnabled, + onPressed: (context) { + context.pushNamed(Routes.shakeDetectorSettings.name); + }, + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.shakeDetectionSettings( + isEnabled: value, ), - ], - ), - ), - ), - ], - ), - - // Backup and Restore section - SettingsSection( - margin: const EdgeInsetsDirectional.symmetric( - horizontal: 16.0, - vertical: 8.0, + ); + }, ), - title: Text( - 'Backup and Restore', - style: Theme.of(context).textTheme.titleLarge, - ), - tiles: [ - SettingsTile( - title: const Text('Copy to Clipboard'), - leading: const Icon(Icons.copy), - description: const Text( - 'Copy the app settings to the clipboard', - ), - onPressed: (context) async { - // copy to clipboard - await Clipboard.setData( - ClipboardData( - text: jsonEncode(appSettings.toJson()), - ), - ); - // show toast - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Settings copied to clipboard'), - ), - ); - }, - ), - SettingsTile( - title: const Text('Restore'), - leading: const Icon(Icons.restore), - description: const Text( - 'Restore the app settings from the backup', - ), - onPressed: (context) { - final formKey = GlobalKey(); - // show a dialog to get the backup - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Restore Backup'), - content: Form( - key: formKey, - child: TextFormField( - controller: serverURIController, - decoration: const InputDecoration( - labelText: 'Backup', - hintText: 'Paste the backup here', - ), - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please paste the backup here'; - } - return null; - }, - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - if (formKey.currentState!.validate()) { - final backup = serverURIController.text; - final newSettings = model.AppSettings.fromJson( - // decode the backup as json - jsonDecode(backup), - ); - ref - .read(appSettingsProvider.notifier) - .update(newSettings); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Settings restored'), - ), - ); - // clear the backup - serverURIController.clear(); - } - }, - child: const Text('Restore'), - ), - ], - ); - }, - ); - }, - ), + ], + ), - // a button to reset the app settings - SettingsTile( - title: const Text('Reset App Settings'), - leading: const Icon(Icons.settings_backup_restore), - description: const Text( - 'Reset the app settings to the default values', - ), - onPressed: (context) async { - // confirm the reset - final res = await showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('Reset App Settings'), - content: const Text( - 'Are you sure you want to reset the app settings?', - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(false); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(true); - }, - child: const Text('Reset'), - ), - ], - ); - }, - ); - - // if the user confirms the reset - if (res == true) { - ref.read(appSettingsProvider.notifier).reset(); - } - }, - ), - ], + // Appearance section + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, ), - ], - ), + title: Text( + 'Appearance', + style: Theme.of(context).textTheme.titleLarge, + ), + tiles: [ + SettingsTile.navigation( + leading: const Icon(Icons.color_lens), + title: const Text('Theme Settings'), + description: const Text('Customize the app theme'), + onPressed: (context) { + context.pushNamed(Routes.themeSettings.name); + }, + ), + SettingsTile( + title: const Text('Notification Media Player'), + leading: const Icon(Icons.play_lesson), + description: const Text( + 'Customize the media player in notifications', + ), + onPressed: (context) { + context.pushNamed(Routes.notificationSettings.name); + }, + ), + SettingsTile.navigation( + leading: const Icon(Icons.home_filled), + title: const Text('Home Page Settings'), + description: const Text('Customize the home page'), + onPressed: (context) { + context.pushNamed(Routes.homePageSettings.name); + }, + ), + ], + ), + + // Backup and Restore section + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + title: Text( + 'Backup and Restore', + style: Theme.of(context).textTheme.titleLarge, + ), + tiles: [ + SettingsTile( + title: const Text('Copy to Clipboard'), + leading: const Icon(Icons.copy), + description: const Text('Copy the app settings to the clipboard'), + onPressed: (context) async { + // copy to clipboard + await Clipboard.setData( + ClipboardData(text: jsonEncode(appSettings.toJson())), + ); + // show toast + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Settings copied to clipboard')), + ); + }, + ), + SettingsTile( + title: const Text('Restore'), + leading: const Icon(Icons.restore), + description: const Text( + 'Restore the app settings from the backup', + ), + onPressed: (context) { + // show a dialog to get the backup + showDialog( + context: context, + builder: (context) { + return RestoreDialogue(); + }, + ); + }, + ), + + // a button to reset the app settings + SettingsTile( + title: const Text('Reset App Settings'), + leading: const Icon(Icons.settings_backup_restore), + description: const Text( + 'Reset the app settings to the default values', + ), + onPressed: (context) async { + // confirm the reset + final res = await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('Reset App Settings'), + content: const Text( + 'Are you sure you want to reset the app settings?', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: const Text('Reset'), + ), + ], + ); + }, + ); + + // if the user confirms the reset + if (res == true) { + ref.read(appSettingsProvider.notifier).reset(); + } + }, + ), + ], + ), + ], + ); + } +} + +class RestoreDialogue extends HookConsumerWidget { + const RestoreDialogue({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + final settings = useState(null); + + final settingsInputController = useTextEditingController(); + return AlertDialog( + title: const Text('Restore Backup'), + content: Form( + key: formKey, + child: TextFormField( + autofocus: true, + decoration: InputDecoration( + labelText: 'Backup', + hintText: 'Paste the backup here', + // clear button + suffixIcon: IconButton( + icon: Icon(Icons.clear), + onPressed: () { + settingsInputController.clear(); + }, + ), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please paste the backup here'; + } + try { + // try to decode the backup + settings.value = model.AppSettings.fromJson(jsonDecode(value)); + } catch (e) { + return 'Invalid backup'; + } + return null; + }, + ), + ), + actions: [ + CancelButton(), + TextButton( + onPressed: () { + if (formKey.currentState!.validate()) { + if (settings.value == null) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Invalid backup'))); + return; + } + ref.read(appSettingsProvider.notifier).update(settings.value!); + settingsInputController.clear(); + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Settings restored')), + ); + } else { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Invalid backup'))); + } + }, + child: const Text('Restore'), + ), + ], ); } } diff --git a/lib/settings/view/auto_sleep_timer_settings_page.dart b/lib/settings/view/auto_sleep_timer_settings_page.dart index 2383ba1..58fcf8e 100644 --- a/lib/settings/view/auto_sleep_timer_settings_page.dart +++ b/lib/settings/view/auto_sleep_timer_settings_page.dart @@ -1,110 +1,136 @@ import 'package:flutter/material.dart'; import 'package:flutter_settings_ui/flutter_settings_ui.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/view/simple_settings_page.dart'; import 'package:vaani/shared/extensions/time_of_day.dart'; class AutoSleepTimerSettingsPage extends HookConsumerWidget { - const AutoSleepTimerSettingsPage({ - super.key, - }); + const AutoSleepTimerSettingsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final appSettings = ref.watch(appSettingsProvider); - final sleepTimerSettings = appSettings.playerSettings.sleepTimerSettings; + final sleepTimerSettings = appSettings.sleepTimerSettings; - return Scaffold( - appBar: AppBar( - title: const Text('Auto Sleep Timer Settings'), - ), - body: SettingsList( - sections: [ - SettingsSection( - margin: const EdgeInsetsDirectional.symmetric( - horizontal: 16.0, - vertical: 8.0, + var enabled = + sleepTimerSettings.autoTurnOnTimer && + !sleepTimerSettings.alwaysAutoTurnOnTimer; + final selectedValueColor = enabled + ? Theme.of(context).colorScheme.primary + : Theme.of(context).disabledColor; + return SimpleSettingsPage( + title: const Text('Auto Sleep Timer Settings'), + sections: [ + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + tiles: [ + SettingsTile.switchTile( + // initialValue: sleepTimerSettings.autoTurnOnTimer, + title: const Text('Auto Turn On Timer'), + description: const Text( + 'Automatically turn on the sleep timer based on the time of day', + ), + leading: sleepTimerSettings.autoTurnOnTimer + ? const Icon(Symbols.time_auto) + : const Icon(Symbols.timer_off), + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.sleepTimerSettings( + autoTurnOnTimer: value, + ), + ); + }, + initialValue: sleepTimerSettings.autoTurnOnTimer, ), - tiles: [ - SettingsTile.switchTile( - // initialValue: sleepTimerSettings.autoTurnOnTimer, - title: const Text('Auto Turn On Timer'), - description: const Text( - 'Automatically turn on the sleep timer based on the time of day', - ), - leading: sleepTimerSettings.autoTurnOnTimer - ? const Icon(Icons.timer) - : const Icon(Icons.timer_off), - onToggle: (value) { - ref.read(appSettingsProvider.notifier).update( - appSettings.copyWith.playerSettings.sleepTimerSettings( - autoTurnOnTimer: value, + // auto turn on time settings, enabled only when autoTurnOnTimer is enabled + SettingsTile.navigation( + enabled: enabled, + leading: const Icon(Symbols.timer_play), + title: const Text('From'), + description: const Text( + 'Turn on the sleep timer at the specified time', + ), + onPressed: (context) async { + // navigate to the time picker + final selected = await showTimePicker( + context: context, + initialTime: sleepTimerSettings.autoTurnOnTime.toTimeOfDay(), + ); + if (selected != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.sleepTimerSettings( + autoTurnOnTime: selected.toDuration(), ), ); - }, - initialValue: sleepTimerSettings.autoTurnOnTimer, + } + }, + trailing: Text( + sleepTimerSettings.autoTurnOnTime.toTimeOfDay().format(context), + style: TextStyle(color: selectedValueColor), ), - // auto turn on time settings, enabled only when autoTurnOnTimer is enabled - SettingsTile.navigation( - enabled: sleepTimerSettings.autoTurnOnTimer, - title: const Text('Auto Turn On Time'), - description: const Text( - 'Turn on the sleep timer at the specified time', - ), - onPressed: (context) async { - // navigate to the time picker - final selected = await showTimePicker( - context: context, - initialTime: - sleepTimerSettings.autoTurnOnTime.toTimeOfDay(), - ); - if (selected != null) { - ref.read(appSettingsProvider.notifier).update( - appSettings.copyWith.playerSettings - .sleepTimerSettings( - autoTurnOnTime: selected.toDuration(), - ), - ); - } - }, - value: Text( - sleepTimerSettings.autoTurnOnTime - .toTimeOfDay() - .format(context), - ), + ), + SettingsTile.navigation( + enabled: enabled, + leading: const Icon(Symbols.timer_pause), + title: const Text('Until'), + description: const Text( + 'Turn off the sleep timer at the specified time', ), - SettingsTile.navigation( - title: const Text('Auto Turn Off Time'), - description: const Text( - 'Turn off the sleep timer at the specified time', - ), - enabled: sleepTimerSettings.autoTurnOnTimer, - onPressed: (context) async { - // navigate to the time picker - final selected = await showTimePicker( - context: context, - initialTime: - sleepTimerSettings.autoTurnOffTime.toTimeOfDay(), - ); - if (selected != null) { - ref.read(appSettingsProvider.notifier).update( - appSettings.copyWith.playerSettings - .sleepTimerSettings( - autoTurnOffTime: selected.toDuration(), - ), - ); - } - }, - value: Text( - sleepTimerSettings.autoTurnOffTime - .toTimeOfDay() - .format(context), + onPressed: (context) async { + // navigate to the time picker + final selected = await showTimePicker( + context: context, + initialTime: sleepTimerSettings.autoTurnOffTime.toTimeOfDay(), + ); + if (selected != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.sleepTimerSettings( + autoTurnOffTime: selected.toDuration(), + ), + ); + } + }, + trailing: Text( + sleepTimerSettings.autoTurnOffTime.toTimeOfDay().format( + context, ), + style: TextStyle(color: selectedValueColor), ), - ], - ), - ], - ), + ), + + // switch tile for always auto turn on timer no matter what + SettingsTile.switchTile( + leading: const Icon(Symbols.all_inclusive), + title: const Text('Always Auto Turn On Timer'), + description: const Text( + 'Always turn on the sleep timer, no matter what', + ), + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.sleepTimerSettings( + alwaysAutoTurnOnTimer: value, + ), + ); + }, + enabled: sleepTimerSettings.autoTurnOnTimer, + initialValue: sleepTimerSettings.alwaysAutoTurnOnTimer, + ), + ], + ), + ], ); } } diff --git a/lib/settings/view/buttons.dart b/lib/settings/view/buttons.dart new file mode 100644 index 0000000..209dc21 --- /dev/null +++ b/lib/settings/view/buttons.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class OkButton extends StatelessWidget { + const OkButton({super.key, this.onPressed}); + + final void Function()? onPressed; + + @override + Widget build(BuildContext context) { + return TextButton(onPressed: onPressed, child: const Text('OK')); + } +} + +class CancelButton extends StatelessWidget { + const CancelButton({super.key, this.onPressed}); + + final void Function()? onPressed; + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: () { + onPressed?.call(); + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ); + } +} diff --git a/lib/settings/view/home_page_settings_page.dart b/lib/settings/view/home_page_settings_page.dart new file mode 100644 index 0000000..9b3b3fb --- /dev/null +++ b/lib/settings/view/home_page_settings_page.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/view/simple_settings_page.dart' + show SimpleSettingsPage; + +class HomePageSettingsPage extends HookConsumerWidget { + const HomePageSettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appSettings = ref.watch(appSettingsProvider); + final appSettingsNotifier = ref.read(appSettingsProvider.notifier); + + return SimpleSettingsPage( + title: Text('Home Page Settings'), + sections: [ + SettingsSection( + title: const Text('Quick Play'), + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + tiles: [ + SettingsTile.switchTile( + initialValue: appSettings + .homePageSettings + .showPlayButtonOnContinueListeningShelf, + title: const Text('Continue Listening'), + leading: const Icon(Icons.play_arrow), + description: const Text( + 'Show play button for books in currently listening shelf', + ), + onToggle: (value) { + appSettingsNotifier.update( + appSettings.copyWith( + homePageSettings: appSettings.homePageSettings.copyWith( + showPlayButtonOnContinueListeningShelf: value, + ), + ), + ); + }, + ), + SettingsTile.switchTile( + title: const Text('Continue Series'), + leading: const Icon(Icons.play_arrow), + description: const Text( + 'Show play button for books in continue series shelf', + ), + initialValue: appSettings + .homePageSettings + .showPlayButtonOnContinueSeriesShelf, + onToggle: (value) { + appSettingsNotifier.update( + appSettings.copyWith( + homePageSettings: appSettings.homePageSettings.copyWith( + showPlayButtonOnContinueSeriesShelf: value, + ), + ), + ); + }, + ), + SettingsTile.switchTile( + title: const Text('Other shelves'), + leading: const Icon(Icons.all_inclusive), + description: const Text( + 'Show play button for all books in all remaining shelves', + ), + initialValue: appSettings + .homePageSettings + .showPlayButtonOnAllRemainingShelves, + onToggle: (value) { + appSettingsNotifier.update( + appSettings.copyWith( + homePageSettings: appSettings.homePageSettings.copyWith( + showPlayButtonOnAllRemainingShelves: value, + ), + ), + ); + }, + ), + SettingsTile.switchTile( + title: const Text('Listen Again'), + leading: const Icon(Icons.replay), + description: const Text( + 'Show play button for all books in listen again shelf', + ), + initialValue: + appSettings.homePageSettings.showPlayButtonOnListenAgainShelf, + onToggle: (value) { + appSettingsNotifier.update( + appSettings.copyWith( + homePageSettings: appSettings.homePageSettings.copyWith( + showPlayButtonOnListenAgainShelf: value, + ), + ), + ); + }, + ), + ], + ), + ], + ); + } +} diff --git a/lib/settings/view/notification_settings_page.dart b/lib/settings/view/notification_settings_page.dart new file mode 100644 index 0000000..c306f87 --- /dev/null +++ b/lib/settings/view/notification_settings_page.dart @@ -0,0 +1,382 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/models/app_settings.dart'; +import 'package:vaani/settings/view/buttons.dart'; +import 'package:vaani/settings/view/simple_settings_page.dart'; +import 'package:vaani/shared/extensions/enum.dart'; + +class NotificationSettingsPage extends HookConsumerWidget { + const NotificationSettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appSettings = ref.watch(appSettingsProvider); + final notificationSettings = appSettings.notificationSettings; + final primaryColor = Theme.of(context).colorScheme.primary; + return SimpleSettingsPage( + title: const Text('Notification Settings'), + sections: [ + SettingsSection( + margin: const EdgeInsetsDirectional.only( + start: 16.0, + end: 16.0, + top: 8.0, + bottom: 8.0, + ), + tiles: [ + // set the primary and secondary titles + SettingsTile( + title: const Text('Primary Title'), + description: Text.rich( + TextSpan( + text: 'The title of the notification\n', + children: [ + TextSpan( + text: notificationSettings.primaryTitle, + style: TextStyle( + fontWeight: FontWeight.bold, + color: primaryColor, + ), + ), + ], + ), + ), + leading: const Icon(Icons.title), + onPressed: (context) async { + // show the notification title picker + final selectedTitle = await showDialog( + context: context, + builder: (context) { + return NotificationTitlePicker( + initialValue: notificationSettings.primaryTitle, + title: 'Primary Title', + ); + }, + ); + if (selectedTitle != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.notificationSettings( + primaryTitle: selectedTitle, + ), + ); + } + }, + ), + + SettingsTile( + title: const Text('Secondary Title'), + description: Text.rich( + TextSpan( + text: 'The subtitle of the notification\n', + children: [ + TextSpan( + text: notificationSettings.secondaryTitle, + style: TextStyle( + fontWeight: FontWeight.bold, + color: primaryColor, + ), + ), + ], + ), + ), + leading: const Icon(Icons.title), + onPressed: (context) async { + // show the notification title picker + final selectedTitle = await showDialog( + context: context, + builder: (context) { + return NotificationTitlePicker( + initialValue: notificationSettings.secondaryTitle, + title: 'Secondary Title', + ); + }, + ); + if (selectedTitle != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.notificationSettings( + secondaryTitle: selectedTitle, + ), + ); + } + }, + ), + + // set forward and backward intervals + SettingsTile( + title: const Text('Forward Interval'), + description: Row( + children: [ + Text( + '${notificationSettings.fastForwardInterval.inSeconds} seconds', + ), + Expanded( + child: TimeIntervalSlider( + defaultValue: notificationSettings.fastForwardInterval, + onChangedEnd: (interval) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.notificationSettings( + fastForwardInterval: interval, + ), + ); + }, + ), + ), + ], + ), + leading: const Icon(Icons.fast_forward), + ), + SettingsTile( + title: const Text('Backward Interval'), + description: Row( + children: [ + Text( + '${notificationSettings.rewindInterval.inSeconds} seconds', + ), + Expanded( + child: TimeIntervalSlider( + defaultValue: notificationSettings.rewindInterval, + onChangedEnd: (interval) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.notificationSettings( + rewindInterval: interval, + ), + ); + }, + ), + ), + ], + ), + leading: const Icon(Icons.fast_rewind), + ), + // set the media controls + SettingsTile( + title: const Text('Media Controls'), + leading: const Icon(Icons.control_camera), + // description: const Text('Select the media controls to display'), + description: const Text('Select the media controls to display'), + trailing: Wrap( + spacing: 8.0, + children: notificationSettings.mediaControls + .map((control) => Icon(control.icon, color: primaryColor)) + .toList(), + ), + onPressed: (context) async { + final selectedControls = + await showDialog>( + context: context, + builder: (context) { + return MediaControlsPicker( + selectedControls: notificationSettings.mediaControls, + ); + }, + ); + if (selectedControls != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.notificationSettings( + mediaControls: selectedControls, + ), + ); + } + }, + ), + + // set the progress bar to show chapter progress + SettingsTile.switchTile( + title: const Text('Show Chapter Progress'), + leading: const Icon(Icons.book), + description: const Text( + 'instead of the overall progress of the book', + ), + initialValue: notificationSettings.progressBarIsChapterProgress, + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.notificationSettings( + progressBarIsChapterProgress: value, + ), + ); + }, + ), + ], + ), + ], + ); + } +} + +class MediaControlsPicker extends HookConsumerWidget { + const MediaControlsPicker({super.key, required this.selectedControls}); + + final List selectedControls; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedMediaControls = useState(selectedControls); + return AlertDialog( + title: const Text('Media Controls'), + actions: [ + const CancelButton(), + OkButton( + onPressed: () { + Navigator.of(context).pop(selectedMediaControls.value); + }, + ), + ], + // a list of chips to easily select the media controls to display + // with icons and labels + content: Wrap( + spacing: 8.0, + children: NotificationMediaControl.values + .map( + (control) => ChoiceChip( + avatar: Icon(control.icon), + label: Text(control.pascalCase), + selected: selectedMediaControls.value.contains(control), + onSelected: (selected) { + if (selected) { + selectedMediaControls.value = [ + ...selectedMediaControls.value, + control, + ]; + } else { + selectedMediaControls.value = [ + ...selectedMediaControls.value.where((c) => c != control), + ]; + } + }, + ), + ) + .toList(), + ), + ); + } +} + +class TimeIntervalSlider extends HookConsumerWidget { + const TimeIntervalSlider({ + super.key, + this.title, + required this.defaultValue, + this.onChanged, + this.onChangedEnd, + this.min = const Duration(seconds: 5), + this.max = const Duration(seconds: 120), + this.step = const Duration(seconds: 5), + }); + + final Widget? title; + final Duration defaultValue; + final ValueChanged? onChanged; + final ValueChanged? onChangedEnd; + final Duration min; + final Duration max; + final Duration step; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedInterval = useState(defaultValue); + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + title ?? const SizedBox.shrink(), + if (title != null) const SizedBox(height: 8.0), + Slider( + value: selectedInterval.value.inSeconds.toDouble(), + min: min.inSeconds.toDouble(), + max: max.inSeconds.toDouble(), + divisions: ((max.inSeconds - min.inSeconds) ~/ step.inSeconds), + label: '${selectedInterval.value.inSeconds} seconds', + onChanged: (value) { + selectedInterval.value = Duration(seconds: value.toInt()); + onChanged?.call(selectedInterval.value); + }, + onChangeEnd: (value) { + onChangedEnd?.call(selectedInterval.value); + }, + ), + ], + ); + } +} + +class NotificationTitlePicker extends HookConsumerWidget { + const NotificationTitlePicker({ + super.key, + required this.initialValue, + required this.title, + }); + + final String initialValue; + final String title; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedTitle = useState(initialValue); + final controller = useTextEditingController(text: initialValue); + return AlertDialog( + title: Text(title), + actions: [ + const CancelButton(), + OkButton( + onPressed: () { + Navigator.of(context).pop(selectedTitle.value); + }, + ), + ], + // a list of chips to easily insert available fields into the text field + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + autofocus: true, + controller: controller, + onChanged: (value) { + selectedTitle.value = value; + }, + decoration: InputDecoration( + helper: const Text('Select a field below to insert it'), + suffix: IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + controller.clear(); + selectedTitle.value = ''; + }, + ), + ), + ), + const SizedBox(height: 8.0), + Wrap( + spacing: 8.0, + children: NotificationTitleType.values + .map( + (type) => ActionChip( + label: Text(type.pascalCase), + onPressed: () { + final text = controller.text; + final newText = '$text\$${type.name}'; + controller.text = newText; + selectedTitle.value = newText; + }, + ), + ) + .toList(), + ), + ], + ), + ); + } +} diff --git a/lib/settings/view/player_settings_page.dart b/lib/settings/view/player_settings_page.dart new file mode 100644 index 0000000..1d93e71 --- /dev/null +++ b/lib/settings/view/player_settings_page.dart @@ -0,0 +1,447 @@ +import 'package:duration_picker/duration_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/view/buttons.dart'; +import 'package:vaani/settings/view/simple_settings_page.dart'; +import 'package:vaani/shared/extensions/duration_format.dart'; + +class PlayerSettingsPage extends HookConsumerWidget { + const PlayerSettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appSettings = ref.watch(appSettingsProvider); + final playerSettings = appSettings.playerSettings; + final primaryColor = Theme.of(context).colorScheme.primary; + + return SimpleSettingsPage( + title: const Text('Player Settings'), + sections: [ + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + tiles: [ + // preferred settings for every book + SettingsTile.switchTile( + title: const Text('Remember Player Settings for Every Book'), + leading: const Icon(Icons.settings_applications), + description: const Text( + 'Settings like speed, loudness, etc. will be remembered for every book', + ), + initialValue: playerSettings.configurePlayerForEveryBook, + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.playerSettings( + configurePlayerForEveryBook: value, + ), + ); + }, + ), + + // preferred default speed + SettingsTile( + title: const Text('Default Speed'), + trailing: Text( + '${playerSettings.preferredDefaultSpeed}x', + style: TextStyle( + color: primaryColor, + fontWeight: FontWeight.bold, + ), + ), + leading: const Icon(Icons.speed), + onPressed: (context) async { + final newSpeed = await showDialog( + context: context, + builder: (context) => SpeedPicker( + initialValue: playerSettings.preferredDefaultSpeed, + ), + ); + if (newSpeed != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.playerSettings( + preferredDefaultSpeed: newSpeed, + ), + ); + } + }, + ), + // preferred speed options + SettingsTile( + title: const Text('Speed Options'), + description: Text( + playerSettings.speedOptions.map((e) => '${e}x').join(', '), + style: TextStyle( + fontWeight: FontWeight.bold, + color: primaryColor, + ), + ), + leading: const Icon(Icons.speed), + onPressed: (context) async { + final newSpeedOptions = await showDialog?>( + context: context, + builder: (context) => SpeedOptionsPicker( + initialValue: playerSettings.speedOptions, + ), + ); + if (newSpeedOptions != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.playerSettings( + speedOptions: newSpeedOptions..sort(), + ), + ); + } + }, + ), + ], + ), + + // Playback Reporting + SettingsSection( + title: const Text('Playback Reporting'), + tiles: [ + SettingsTile( + title: const Text('Minimum Position to Report'), + description: Text.rich( + TextSpan( + text: 'Do not report playback for the first ', + children: [ + TextSpan( + text: playerSettings + .minimumPositionForReporting + .smartBinaryFormat, + style: TextStyle( + fontWeight: FontWeight.bold, + color: primaryColor, + ), + ), + const TextSpan(text: ' of the book'), + ], + ), + ), + leading: const Icon(Icons.timer), + onPressed: (context) async { + final newDuration = await showDialog( + context: context, + builder: (context) { + return TimeDurationSelector( + title: const Text('Ignore Playback Position Less Than'), + baseUnit: BaseUnit.second, + initialValue: playerSettings.minimumPositionForReporting, + ); + }, + ); + if (newDuration != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.playerSettings( + minimumPositionForReporting: newDuration, + ), + ); + } + }, + ), + // when to mark complete + SettingsTile( + title: const Text('Mark Complete When Time Left'), + description: Text.rich( + TextSpan( + text: 'Mark complete when less than ', + children: [ + TextSpan( + text: playerSettings + .markCompleteWhenTimeLeft + .smartBinaryFormat, + style: TextStyle( + fontWeight: FontWeight.bold, + color: primaryColor, + ), + ), + const TextSpan(text: ' left in the book'), + ], + ), + ), + leading: const Icon(Icons.cloud_done), + onPressed: (context) async { + final newDuration = await showDialog( + context: context, + builder: (context) { + return TimeDurationSelector( + title: const Text('Mark Complete When Time Left'), + baseUnit: BaseUnit.second, + initialValue: playerSettings.markCompleteWhenTimeLeft, + ); + }, + ); + if (newDuration != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.playerSettings( + markCompleteWhenTimeLeft: newDuration, + ), + ); + } + }, + ), + // playback report interval + SettingsTile( + title: const Text('Playback Report Interval'), + description: Text.rich( + TextSpan( + text: 'Report progress every ', + children: [ + TextSpan( + text: playerSettings + .playbackReportInterval + .smartBinaryFormat, + style: TextStyle( + fontWeight: FontWeight.bold, + color: primaryColor, + ), + ), + const TextSpan(text: ' to the server'), + ], + ), + ), + leading: const Icon(Icons.change_circle_outlined), + onPressed: (context) async { + final newDuration = await showDialog( + context: context, + builder: (context) { + return TimeDurationSelector( + title: const Text('Playback Report Interval'), + baseUnit: BaseUnit.second, + initialValue: playerSettings.playbackReportInterval, + ); + }, + ); + if (newDuration != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.playerSettings( + playbackReportInterval: newDuration, + ), + ); + } + }, + ), + ], + ), + // Display Settings + SettingsSection( + title: const Text('Display Settings'), + tiles: [ + // show total progress + SettingsTile.switchTile( + title: const Text('Show Total Progress'), + leading: const Icon(Icons.show_chart), + description: const Text( + 'Show the total progress of the book in the player', + ), + initialValue: + playerSettings.expandedPlayerSettings.showTotalProgress, + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.playerSettings + .expandedPlayerSettings(showTotalProgress: value), + ); + }, + ), + // show chapter progress + SettingsTile.switchTile( + title: const Text('Show Chapter Progress'), + leading: const Icon(Icons.show_chart), + description: const Text( + 'Show the progress of the current chapter in the player', + ), + initialValue: + playerSettings.expandedPlayerSettings.showChapterProgress, + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.playerSettings( + expandedPlayerSettings: playerSettings + .expandedPlayerSettings + .copyWith(showChapterProgress: value), + ), + ); + }, + ), + ], + ), + ], + ); + } +} + +class TimeDurationSelector extends HookConsumerWidget { + const TimeDurationSelector({ + super.key, + this.title = const Text('Select Duration'), + this.baseUnit = BaseUnit.second, + this.initialValue = Duration.zero, + }); + + final Widget title; + final BaseUnit baseUnit; + final Duration initialValue; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final duration = useState(initialValue); + return AlertDialog( + title: title, + content: DurationPicker( + duration: duration.value, + baseUnit: baseUnit, + onChange: (value) { + duration.value = value; + }, + ), + actions: [ + const CancelButton(), + OkButton( + onPressed: () { + Navigator.of(context).pop(duration.value); + }, + ), + ], + ); + } +} + +class SpeedPicker extends HookConsumerWidget { + const SpeedPicker({super.key, this.initialValue = 1}); + + final double initialValue; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final speedController = useTextEditingController( + text: initialValue.toString(), + ); + final speed = useState(initialValue); + return AlertDialog( + title: const Text('Select Speed'), + content: TextField( + controller: speedController, + onChanged: (value) => speed.value = double.tryParse(value), + onSubmitted: (value) { + Navigator.of(context).pop(speed.value); + }, + autofocus: true, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Speed', + helper: Text( + 'Enter the speed you want to set when playing for the first time', + ), + ), + ), + actions: [ + const CancelButton(), + OkButton( + onPressed: () { + Navigator.of(context).pop(speed.value); + }, + ), + ], + ); + } +} + +class SpeedOptionsPicker extends HookConsumerWidget { + const SpeedOptionsPicker({ + super.key, + this.initialValue = const [0.75, 1, 1.25, 1.5, 1.75, 2], + }); + + final List initialValue; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final speedOptionAddController = useTextEditingController(); + final speedOptions = useState>(initialValue); + final focusNode = useFocusNode(); + return AlertDialog( + title: const Text('Select Speed Options'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: + speedOptions.value + .map( + (speed) => Chip( + label: Text('${speed}x'), + onDeleted: speed == 1 + ? null + : () { + speedOptions.value = speedOptions.value.where(( + element, + ) { + // speed option 1 can't be removed + return element != speed; + }).toList(); + }, + ), + ) + .toList() + ..sort((a, b) { + // if (a.label == const Text('1x')) { + // return -1; + // } else if (b.label == const Text('1x')) { + // return 1; + // } + return a.label.toString().compareTo(b.label.toString()); + }), + ), + TextField( + focusNode: focusNode, + autofocus: true, + controller: speedOptionAddController, + onSubmitted: (value) { + final newSpeed = double.tryParse(value); + if (newSpeed != null && !speedOptions.value.contains(newSpeed)) { + speedOptions.value = [...speedOptions.value, newSpeed]; + } + speedOptionAddController.clear(); + focusNode.requestFocus(); + }, + keyboardType: TextInputType.number, + decoration: const InputDecoration( + labelText: 'Add Speed Option', + helper: Text('Enter a new speed option to add'), + ), + ), + ], + ), + actions: [ + const CancelButton(), + OkButton( + onPressed: () { + Navigator.of(context).pop(speedOptions.value); + }, + ), + ], + ); + } +} diff --git a/lib/settings/view/shake_detector_settings_page.dart b/lib/settings/view/shake_detector_settings_page.dart new file mode 100644 index 0000000..fe39b53 --- /dev/null +++ b/lib/settings/view/shake_detector_settings_page.dart @@ -0,0 +1,391 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/models/app_settings.dart'; +import 'package:vaani/settings/view/buttons.dart'; +import 'package:vaani/settings/view/simple_settings_page.dart'; +import 'package:vaani/shared/extensions/enum.dart'; + +class ShakeDetectorSettingsPage extends HookConsumerWidget { + const ShakeDetectorSettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appSettings = ref.watch(appSettingsProvider); + final shakeDetectionSettings = appSettings.shakeDetectionSettings; + final isShakeDetectionEnabled = shakeDetectionSettings.isEnabled; + final selectedValueColor = isShakeDetectionEnabled + ? Theme.of(context).colorScheme.primary + : Theme.of(context).disabledColor; + + return SimpleSettingsPage( + title: const Text('Shake Detector Settings'), + sections: [ + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + tiles: [ + SettingsTile.switchTile( + leading: shakeDetectionSettings.isEnabled + ? const Icon(Icons.vibration) + : const Icon(Icons.not_interested), + title: const Text('Enable Shake Detection'), + description: const Text( + 'Enable shake detection to do various actions', + ), + initialValue: shakeDetectionSettings.isEnabled, + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.shakeDetectionSettings( + isEnabled: value, + ), + ); + }, + ), + ], + ), + + // Shake Detection Settings + SettingsSection( + tiles: [ + SettingsTile( + enabled: isShakeDetectionEnabled, + leading: const Icon(Icons.flag_circle), + title: const Text('Shake Activation Threshold'), + description: const Text( + 'The higher the threshold, the harder you need to shake', + ), + trailing: Text( + '${shakeDetectionSettings.threshold} m/s²', + style: TextStyle( + color: selectedValueColor, + fontWeight: FontWeight.bold, + ), + ), + onPressed: (context) async { + final newThreshold = await showDialog( + context: context, + builder: (context) => ShakeForceSelector( + initialValue: shakeDetectionSettings.threshold, + ), + ); + + if (newThreshold != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.shakeDetectionSettings( + threshold: newThreshold, + ), + ); + } + }, + ), + + // shake action + SettingsTile( + enabled: isShakeDetectionEnabled, + leading: const Icon(Icons.directions_run), + title: const Text('Shake Action'), + description: const Text( + 'The action to perform when a shake is detected', + ), + trailing: Icon( + shakeDetectionSettings.shakeAction.icon, + color: selectedValueColor, + ), + onPressed: (context) async { + final newShakeAction = await showDialog( + context: context, + builder: (context) => ShakeActionSelector( + initialValue: shakeDetectionSettings.shakeAction, + ), + ); + + if (newShakeAction != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.shakeDetectionSettings( + shakeAction: newShakeAction, + ), + ); + } + }, + ), + + // shake feedback + SettingsTile( + enabled: isShakeDetectionEnabled, + leading: const Icon(Icons.feedback), + title: const Text('Shake Feedback'), + description: const Text( + 'The feedback to give when a shake is detected', + ), + trailing: shakeDetectionSettings.feedback.isEmpty + ? Icon( + Icons.not_interested, + color: Theme.of(context).disabledColor, + ) + : Wrap( + spacing: 8.0, + children: shakeDetectionSettings.feedback.map((feedback) { + return Icon(feedback.icon, color: selectedValueColor); + }).toList(), + ), + onPressed: (context) async { + final newFeedback = + await showDialog>( + context: context, + builder: (context) => ShakeFeedbackSelector( + initialValue: shakeDetectionSettings.feedback, + ), + ); + + if (newFeedback != null) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.shakeDetectionSettings( + feedback: newFeedback, + ), + ); + } + }, + ), + ], + ), + ], + ); + } +} + +class ShakeFeedbackSelector extends HookConsumerWidget { + const ShakeFeedbackSelector({ + super.key, + this.initialValue = const {ShakeDetectedFeedback.vibrate}, + }); + + final Set initialValue; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final feedback = useState(initialValue); + return AlertDialog( + title: const Text('Select Shake Feedback'), + content: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: ShakeDetectedFeedback.values + .map( + (feedbackType) => ChoiceChip( + avatar: Icon(feedbackType.icon), + label: Text(feedbackType.pascalCase), + tooltip: feedbackType.description, + onSelected: (val) { + if (feedback.value.contains(feedbackType)) { + feedback.value = feedback.value + .where((element) => element != feedbackType) + .toSet(); + } else { + feedback.value = {...feedback.value, feedbackType}; + } + }, + selected: feedback.value.contains(feedbackType), + ), + ) + .toList(), + ), + actions: [ + const CancelButton(), + OkButton( + onPressed: () { + Navigator.of(context).pop(feedback.value); + }, + ), + ], + ); + } +} + +class ShakeActionSelector extends HookConsumerWidget { + const ShakeActionSelector({ + super.key, + this.initialValue = ShakeAction.resetSleepTimer, + }); + + final ShakeAction initialValue; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final shakeAction = useState(initialValue); + return AlertDialog( + title: const Text('Select Shake Action'), + content: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: ShakeAction.values + .map( + // chips with radio buttons as one of the options can be selected + (action) => ChoiceChip( + avatar: Icon(action.icon), + label: Text(action.pascalCase), + onSelected: (val) { + shakeAction.value = action; + }, + selected: shakeAction.value == action, + ), + ) + .toList(), + ), + actions: [ + const CancelButton(), + OkButton( + onPressed: () { + Navigator.of(context).pop(shakeAction.value); + }, + ), + ], + ); + } +} + +class ShakeForceSelector extends HookConsumerWidget { + const ShakeForceSelector({super.key, this.initialValue = 6}); + + final double initialValue; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final shakeForce = useState(initialValue); + final controller = useTextEditingController(text: initialValue.toString()); + return AlertDialog( + title: const Text('Select Shake Activation Threshold'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + autofocus: true, + controller: controller, + onChanged: (value) { + final newThreshold = double.tryParse(value); + if (newThreshold != null) { + shakeForce.value = newThreshold; + } + }, + keyboardType: TextInputType.number, + decoration: InputDecoration( + // clear button + suffix: IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + controller.clear(); + shakeForce.value = 0; + }, + ), + helper: const Text('Enter a number to set the threshold in m/s²'), + ), + ), + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: ShakeForce.values + .map( + (force) => ChoiceChip( + label: Text(force.pascalCase), + onSelected: (val) { + controller.text = force.threshold.toString(); + shakeForce.value = force.threshold; + }, + selected: shakeForce.value == force.threshold, + ), + ) + .toList(), + ), + ], + ), + actions: [ + const CancelButton(), + OkButton( + onPressed: () { + Navigator.of(context).pop(shakeForce.value); + }, + ), + ], + ); + } +} + +enum ShakeForce { + whisper(0.5), + low(2.5), + medium(5), + high(7.5), + storm(10), + hurricane(15), + earthquake(20), + meteorShower(30), + supernova(40), + blackHole(50); + + const ShakeForce(this.threshold); + + final double threshold; +} + +extension ShakeActionIcon on ShakeAction { + IconData? get icon { + switch (this) { + case ShakeAction.none: + return Icons.not_interested; + case ShakeAction.resetSleepTimer: + return Icons.timer; + case ShakeAction.playPause: + return Icons.play_arrow; + // case ShakeAction.nextChapter: + // return Icons.skip_next; + // case ShakeAction.previousChapter: + // return Icons.skip_previous; + // case ShakeAction.volumeUp: + // return Icons.volume_up; + // case ShakeAction.volumeDown: + // return Icons.volume_down; + case ShakeAction.fastForward: + return Icons.fast_forward; + case ShakeAction.rewind: + return Icons.fast_rewind; + default: + return Icons.question_mark; + } + } +} + +extension on ShakeDetectedFeedback { + IconData? get icon { + switch (this) { + case ShakeDetectedFeedback.vibrate: + return Icons.vibration; + case ShakeDetectedFeedback.beep: + return Icons.volume_up; + default: + return Icons.question_mark; + } + } + + String get description { + switch (this) { + case ShakeDetectedFeedback.vibrate: + return 'Vibrate the device'; + case ShakeDetectedFeedback.beep: + return 'Play a beep sound'; + default: + return 'Unknown'; + } + } +} diff --git a/lib/settings/view/simple_settings_page.dart b/lib/settings/view/simple_settings_page.dart new file mode 100644 index 0000000..e1f9fd7 --- /dev/null +++ b/lib/settings/view/simple_settings_page.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/features/player/view/mini_player_bottom_padding.dart'; + +class SimpleSettingsPage extends HookConsumerWidget { + const SimpleSettingsPage({super.key, this.title, this.sections}); + + final Widget? title; + final List? sections; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + // appBar: AppBar( + // title: title, + // ), + // body: body, + // an app bar which is bigger than the default app bar but on scroll shrinks to the default app bar with the title being animated + body: CustomScrollView( + slivers: [ + SliverAppBar( + expandedHeight: 200.0, + floating: false, + pinned: true, + flexibleSpace: FlexibleSpaceBar( + title: title, + // background: Theme.of(context).primaryColor, + ), + ), + if (sections != null) + SliverList( + delegate: SliverChildListDelegate([ + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(20)), + child: SettingsList( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + sections: sections!, + ), + ), + ]), + ), + // some padding at the bottom + const SliverPadding(padding: EdgeInsets.only(bottom: 20)), + SliverToBoxAdapter(child: MiniPlayerBottomPadding()), + ], + ), + ); + } +} diff --git a/lib/settings/view/theme_settings_page.dart b/lib/settings/view/theme_settings_page.dart new file mode 100644 index 0000000..8727be7 --- /dev/null +++ b/lib/settings/view/theme_settings_page.dart @@ -0,0 +1,215 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/settings/view/buttons.dart'; +import 'package:vaani/settings/view/simple_settings_page.dart'; +import 'package:vaani/shared/extensions/enum.dart'; + +class ThemeSettingsPage extends HookConsumerWidget { + const ThemeSettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appSettings = ref.watch(appSettingsProvider); + final themeSettings = appSettings.themeSettings; + final primaryColor = Theme.of(context).colorScheme.primary; + + return SimpleSettingsPage( + title: const Text('Theme Settings'), + sections: [ + SettingsSection( + margin: const EdgeInsetsDirectional.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + tiles: [ + // choose system , light or dark theme + SettingsTile( + title: const Text('Theme Mode'), + description: SegmentedButton( + expandedInsets: const EdgeInsets.only(top: 8.0), + showSelectedIcon: true, + selectedIcon: const Icon(Icons.check), + selected: {themeSettings.themeMode}, + onSelectionChanged: (newSelection) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.themeSettings( + themeMode: newSelection.first, + ), + ); + }, + segments: [ + ButtonSegment( + value: ThemeMode.light, + icon: Icon(Icons.light_mode), + label: const Text('Light'), + ), + ButtonSegment( + value: ThemeMode.system, + icon: Icon(Icons.auto_awesome), + label: const Text('System'), + ), + ButtonSegment( + value: ThemeMode.dark, + icon: Icon(Icons.dark_mode), + label: const Text('Dark'), + ), + ], + ), + leading: Icon( + themeSettings.themeMode == ThemeMode.light + ? Icons.light_mode + : themeSettings.themeMode == ThemeMode.dark + ? Icons.dark_mode + : Icons.auto_awesome, + ), + ), + + // high contrast mode + SettingsTile.switchTile( + leading: themeSettings.highContrast + ? const Icon(Icons.accessibility) + : const Icon(Icons.accessibility_new_outlined), + initialValue: themeSettings.highContrast, + title: const Text('High Contrast Mode'), + description: const Text( + 'Increase the contrast between the background and the text', + ), + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.themeSettings(highContrast: value), + ); + }, + ), + + // use material theme from system + SettingsTile.switchTile( + initialValue: themeSettings.useMaterialThemeFromSystem, + title: Platform.isAndroid + ? const Text('Use Material You') + : const Text('Material Theme from System'), + description: const Text( + 'Use the system theme colors for the app', + ), + leading: themeSettings.useMaterialThemeFromSystem + ? const Icon(Icons.auto_awesome) + : const Icon(Icons.auto_fix_off), + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.themeSettings( + useMaterialThemeFromSystem: value, + ), + ); + }, + ), + + // TODO choose the primary color + // SettingsTile.navigation( + // title: const Text('Primary Color'), + // description: const Text( + // 'Choose the primary color for the app', + // ), + // leading: const Icon(Icons.colorize), + // trailing: Icon( + // Icons.circle, + // color: themeSettings.customThemeColor.toColor(), + // ), + // onPressed: (context) async { + // final selectedColor = await showDialog( + // context: context, + // builder: (context) { + // return SimpleDialog( + // title: const Text('Select Primary Color'), + // children: [ + // for (final color in Colors.primaries) + // SimpleDialogOption( + // onPressed: () { + // Navigator.pop(context, color); + // }, + // child: Container( + // color: color, + // height: 48, + // ), + // ), + // ], + // ); + // }, + // ); + // if (selectedColor != null) { + // ref.read(appSettingsProvider.notifier).update( + // appSettings.copyWith.themeSettings( + // customThemeColor: selectedColor.toHexString(), + // ), + // ); + // } + // }, + // ), + + // use theme throughout the app when playing item + SettingsTile.switchTile( + initialValue: themeSettings.useCurrentPlayerThemeThroughoutApp, + title: const Text('Adapt theme from currently playing item'), + description: const Text( + 'Use the theme colors from the currently playing item for the app', + ), + leading: themeSettings.useCurrentPlayerThemeThroughoutApp + ? const Icon(Icons.auto_fix_high) + : const Icon(Icons.auto_fix_off), + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.themeSettings( + useCurrentPlayerThemeThroughoutApp: value, + ), + ); + }, + ), + + SettingsTile.switchTile( + initialValue: themeSettings.useMaterialThemeOnItemPage, + title: const Text('Adaptive Theme on Item Page'), + description: const Text( + 'get fancy with the colors on the item page at the cost of some performance', + ), + leading: themeSettings.useMaterialThemeOnItemPage + ? const Icon(Icons.auto_fix_high) + : const Icon(Icons.auto_fix_off), + onToggle: (value) { + ref + .read(appSettingsProvider.notifier) + .update( + appSettings.copyWith.themeSettings( + useMaterialThemeOnItemPage: value, + ), + ); + }, + ), + ], + ), + ], + ); + } +} + +extension ColorExtension on Color { + String toHexString() { + return '#${value.toRadixString(16).substring(2)}'; + } +} + +extension StringExtension on String { + Color toColor() { + return Color(int.parse('0xff$substring(1)')); + } +} diff --git a/lib/settings/view/widgets/navigation_with_switch_tile.dart b/lib/settings/view/widgets/navigation_with_switch_tile.dart new file mode 100644 index 0000000..f2d90f9 --- /dev/null +++ b/lib/settings/view/widgets/navigation_with_switch_tile.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_settings_ui/flutter_settings_ui.dart'; + +class NavigationWithSwitchTile extends AbstractSettingsTile { + const NavigationWithSwitchTile({ + this.leading, + // this.trailing, + required this.value, + required this.title, + this.description, + this.descriptionInlineIos = false, + this.onPressed, + this.enabled = true, + this.backgroundColor, + super.key, + this.onToggle, + }); + + final Widget title; + final Widget? description; + final Color? backgroundColor; + final bool descriptionInlineIos; + final bool enabled; + final Widget? leading; + final Function(BuildContext)? onPressed; + final bool value; + final Function(bool)? onToggle; + + @override + Widget build(BuildContext context) { + return SettingsTile.navigation( + title: title, + description: description, + backgroundColor: backgroundColor, + descriptionInlineIos: descriptionInlineIos, + enabled: enabled, + leading: leading, + onPressed: onPressed, + trailing: IntrinsicHeight( + child: Row( + children: [ + VerticalDivider( + color: Theme.of(context).dividerColor.withValues(alpha: 0.5), + indent: 8.0, + endIndent: 8.0, + ), + Switch.adaptive(value: value, onChanged: onToggle), + ], + ), + ), + ); + } +} diff --git a/lib/shared/extensions/duration_format.dart b/lib/shared/extensions/duration_format.dart index d871424..b492a37 100644 --- a/lib/shared/extensions/duration_format.dart +++ b/lib/shared/extensions/duration_format.dart @@ -9,11 +9,34 @@ extension DurationFormat on Duration { final minutes = inMinutes.remainder(60); final seconds = inSeconds.remainder(60); if (hours > 0) { - return '${hours}h ${minutes}m'; + // skip minutes if it's 0 + if (minutes == 0) { + return smartSingleFormat; + } + return '${Duration(hours: hours).smartBinaryFormat} ${Duration(minutes: minutes).smartSingleFormat}'; } else if (minutes > 0) { - return '${minutes}m ${seconds}s'; + if (seconds == 0) { + return smartSingleFormat; + } + return '${Duration(minutes: minutes).smartSingleFormat} ${Duration(seconds: seconds).smartSingleFormat}'; } else { - return '${seconds}s'; + return smartSingleFormat; + } + } + + /// formats the duration using only 1 unit + /// if the duration is more than 1 hour, it will return `10h` + /// if the duration is less than 1 hour, it will return `30m` + /// if the duration is less than 1 minute, it will return `20s` + /// + /// rest of the duration will be ignored + String get smartSingleFormat { + if (inHours > 0) { + return '${inHours}h'; + } else if (inMinutes > 0) { + return '${inMinutes}m'; + } else { + return '${inSeconds}s'; } } } diff --git a/lib/shared/extensions/enum.dart b/lib/shared/extensions/enum.dart new file mode 100644 index 0000000..0def3ea --- /dev/null +++ b/lib/shared/extensions/enum.dart @@ -0,0 +1,22 @@ +extension TitleCase on Enum { + String get properName { + final name = toString().split('.').last; + return name[0].toUpperCase() + name.substring(1); + } + + String get titleCase { + return name + .replaceAllMapped(RegExp(r'([A-Z])'), (match) => ' ${match.group(0)}') + .trim(); + } + + String get pascalCase { + // capitalize the first letter of each word + return name + .replaceAllMapped(RegExp(r'([A-Z])'), (match) => ' ${match.group(0)}') + .trim() + .split(' ') + .map((word) => word[0].toUpperCase() + word.substring(1)) + .join(' '); + } +} diff --git a/lib/shared/extensions/item_files.dart b/lib/shared/extensions/item_files.dart new file mode 100644 index 0000000..b658f24 --- /dev/null +++ b/lib/shared/extensions/item_files.dart @@ -0,0 +1,10 @@ +import 'package:shelfsdk/audiobookshelf_api.dart'; + +extension TotalSize on LibraryItemExpanded { + int get totalSize { + return libraryFiles.fold( + 0, + (previousValue, element) => previousValue + element.metadata.size, + ); + } +} diff --git a/lib/shared/extensions/model_conversions.dart b/lib/shared/extensions/model_conversions.dart index 38403d8..b3ff0aa 100644 --- a/lib/shared/extensions/model_conversions.dart +++ b/lib/shared/extensions/model_conversions.dart @@ -47,8 +47,8 @@ extension ShelfConversion on Shelf { extension UserConversion on User { UserWithSessionAndMostRecentProgress - get asUserWithSessionAndMostRecentProgress => - UserWithSessionAndMostRecentProgress.fromJson(toJson()); + get asUserWithSessionAndMostRecentProgress => + UserWithSessionAndMostRecentProgress.fromJson(toJson()); User get asUser => User.fromJson(toJson()); } diff --git a/lib/shared/extensions/obfuscation.dart b/lib/shared/extensions/obfuscation.dart new file mode 100644 index 0000000..2a057a4 --- /dev/null +++ b/lib/shared/extensions/obfuscation.dart @@ -0,0 +1,162 @@ +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; +import 'package:vaani/settings/models/api_settings.dart'; +import 'package:vaani/settings/models/audiobookshelf_server.dart'; +import 'package:vaani/settings/models/authenticated_user.dart'; + +// bool kReleaseMode = true; + +extension ObfuscateString on String { + String obfuscate() { + if (!kReleaseMode) { + return this; + } + return 'obfuscated'; + } +} + +extension ObfuscateURI on Uri { + /// keeps everything except the base url for security reasons + Uri obfuscate() { + if (!kReleaseMode) { + return this; + } + + // do not obfuscate the local host + if ([null, 'localhost'].contains(host)) { + return this; + } + + // do not obfuscate file urls + if (scheme == 'file') { + return this; + } + + return replace( + userInfo: userInfo == '' ? '' : 'userInfoObfuscated', + host: 'hostObfuscated', + ); + } +} + +extension ObfuscateList on List { + List obfuscate() { + return map((e) { + if (e is AuthenticatedUser) { + return e.obfuscate() as T; + } else if (e is AudiobookShelfServer) { + return e.obfuscate() as T; + } else if (e is Uri) { + return e.obfuscate() as T; + } else { + return e; + } + }).toList(); + } +} + +extension ObfuscateSet on Set { + Set obfuscate() { + return toList().obfuscate().toSet(); + } +} + +extension ObfuscateAuthenticatedUser on AuthenticatedUser { + AuthenticatedUser obfuscate() { + if (!kReleaseMode) { + return this; + } + return copyWith( + username: username == null ? null : 'usernameObfuscated', + authToken: 'authTokenObfuscated', + server: server.obfuscate(), + ); + } +} + +extension ObfuscateServer on AudiobookShelfServer { + AudiobookShelfServer obfuscate() { + if (!kReleaseMode) { + return this; + } + return copyWith(serverUrl: serverUrl.obfuscate()); + } +} + +extension ObfuscateApiSettings on ApiSettings { + ApiSettings obfuscate() { + if (!kReleaseMode) { + return this; + } + return copyWith( + activeServer: activeServer?.obfuscate(), + activeUser: activeUser?.obfuscate(), + ); + } +} + +extension ObfuscateRequest on http.BaseRequest { + http.BaseRequest obfuscate() { + if (!kReleaseMode) { + return this; + } + return http.Request(method, url.obfuscate()); + } +} + +extension ObfuscateResponse on http.Response { + http.Response obfuscate() { + if (!kReleaseMode) { + return this; + } + return http.Response( + obfuscateBody(), + statusCode, + headers: headers, + request: request?.obfuscate(), + ); + } + + String obfuscateBody() { + if (!kReleaseMode) { + return body; + } + // replace any email addresses with emailObfuscated + // replace any phone numbers with phoneObfuscated + // replace any urls with urlObfuscated + // replace any tokens with tokenObfuscated + // token regex is `"token": "..."` + return body + .replaceAll( + RegExp( + r'(\b\w+@\w+\.\w+\b)|' + r'(\b\d{3}-\d{3}-\d{4}\b)|' + r'(\bhttps?://\S+\b)', + ), + 'obfuscated', + ) + .replaceAll( + RegExp(r'"?token"?:?\s*"[^"]+"'), + '"token": "tokenObfuscated"', + ); + } +} + +extension ObfuscateLoginResponse on shelfsdk.LoginResponse { + shelfsdk.LoginResponse obfuscate() { + if (!kReleaseMode) { + return this; + } + return copyWith(user: user.obfuscate()); + } +} + +extension ObfuscateUser on shelfsdk.User { + shelfsdk.User obfuscate() { + if (!kReleaseMode) { + return this; + } + return shelfsdk.User.fromJson(toJson()..['token'] = 'tokenObfuscated'); + } +} diff --git a/lib/shared/extensions/time_of_day.dart b/lib/shared/extensions/time_of_day.dart index 702c5b6..a0b0f08 100644 --- a/lib/shared/extensions/time_of_day.dart +++ b/lib/shared/extensions/time_of_day.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; extension ToTimeOfDay on Duration { TimeOfDay toTimeOfDay() { - return TimeOfDay(hour: inHours, minute: inMinutes % 60); + return TimeOfDay(hour: inHours % 24, minute: inMinutes % 60); } } @@ -28,4 +28,16 @@ extension TimeOfDayExtension on TimeOfDay { bool isBefore(TimeOfDay other) => this < other; bool isAfter(TimeOfDay other) => this > other; + + bool isBetween(TimeOfDay start, TimeOfDay end) { + // needs more logic to handle the case where start is after end + //but on the other day + if (start == end) { + return this == start; + } + if (start < end) { + return this >= start && this <= end; + } + return this >= start || this <= end; + } } diff --git a/lib/shared/hooks.dart b/lib/shared/hooks.dart index 8e27b3a..3b9fd8e 100644 --- a/lib/shared/hooks.dart +++ b/lib/shared/hooks.dart @@ -7,24 +7,18 @@ void useInterval(VoidCallback callback, Duration delay) { final savedCallback = useRef(callback); savedCallback.value = callback; - useEffect( - () { - final timer = Timer.periodic(delay, (_) => savedCallback.value()); - return timer.cancel; - }, - [delay], - ); + useEffect(() { + final timer = Timer.periodic(delay, (_) => savedCallback.value()); + return timer.cancel; + }, [delay]); } void useTimer(VoidCallback callback, Duration delay) { final savedCallback = useRef(callback); savedCallback.value = callback; - useEffect( - () { - final timer = Timer(delay, savedCallback.value); - return timer.cancel; - }, - [delay], - ); + useEffect(() { + final timer = Timer(delay, savedCallback.value); + return timer.cancel; + }, [delay]); } diff --git a/lib/shared/icons/abs_icons.dart b/lib/shared/icons/abs_icons.dart new file mode 100644 index 0000000..ccceb39 --- /dev/null +++ b/lib/shared/icons/abs_icons.dart @@ -0,0 +1,163 @@ +/// Flutter icons AbsIcons +/// Copyright (C) 2025 by original authors @ fluttericon.com, fontello.com +/// This font was generated by FlutterIcon.com, which is derived from Fontello. +/// +/// To use this font, place it in your fonts/ directory and include the +/// following in your pubspec.yaml +/// +/// flutter: +/// fonts: +/// - family: AbsIcons +/// fonts: +/// - asset: fonts/AbsIcons.ttf +/// +/// +/// +library; +// ignore_for_file: constant_identifier_names + +import 'package:flutter/widgets.dart' show IconData; + +class AbsIcons { + AbsIcons._(); + + static const _kFontFam = 'AbsIcons'; + static const String? _kFontPkg = null; + + static const IconData audiobookshelf = IconData( + 0xe900, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData microphone_2 = IconData( + 0xe901, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData microphone_1 = IconData( + 0xe902, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData radio = IconData( + 0xe903, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData podcast = IconData( + 0xe904, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData books_1 = IconData( + 0xe905, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData database_2 = IconData( + 0xe906, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData headphones = IconData( + 0xe910, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData music = IconData( + 0xe911, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData video = IconData( + 0xe914, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData microphone_3 = IconData( + 0xe91e, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData book = IconData( + 0xe91f, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData books_2 = IconData( + 0xe920, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData file_picture = IconData( + 0xe927, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData database_1 = IconData( + 0xe964, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData rocket = IconData( + 0xe9a5, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData power = IconData( + 0xe9b5, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData star = IconData( + 0xe9d9, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData heart = IconData( + 0xe9da, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + static const IconData rss = IconData( + 0xea9b, + fontFamily: _kFontFam, + fontPackage: _kFontPkg, + ); + + static final Map _iconMap = { + 'audiobookshelf': audiobookshelf, + 'microphone_2': microphone_2, + 'microphone_1': microphone_1, + 'radio': radio, + 'podcast': podcast, + 'books_1': books_1, + 'database_2': database_2, + 'headphones': headphones, + 'music': music, + 'video': video, + 'microphone_3': microphone_3, + 'book': book, + 'books_2': books_2, + 'file_picture': file_picture, + 'database_1': database_1, + 'rocket': rocket, + 'power': power, + 'star': star, + 'heart': heart, + 'rss': rss, + }; + + /// Returns the IconData corresponding to the [iconName] string. + /// + /// If the [iconName] is not found in the map, returns null. + /// Considers null or empty strings as invalid. + static IconData? getIconByName(String? iconName) { + if (iconName == null || iconName.isEmpty) { + return null; + } + return _iconMap[iconName.toLowerCase()]; + } + + static Map get iconMap => _iconMap; +} diff --git a/lib/shared/utils.dart b/lib/shared/utils.dart index 6fce1a0..3d600b4 100644 --- a/lib/shared/utils.dart +++ b/lib/shared/utils.dart @@ -1,4 +1,3 @@ - import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/shared/widgets/add_new_server.dart b/lib/shared/widgets/add_new_server.dart index c04d8fd..450874d 100644 --- a/lib/shared/widgets/add_new_server.dart +++ b/lib/shared/widgets/add_new_server.dart @@ -2,6 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:vaani/api/api_provider.dart'; +import 'package:vaani/main.dart'; + +final httpUrlRegExp = RegExp('https?://'); class AddNewServer extends HookConsumerWidget { const AddNewServer({ @@ -25,7 +28,8 @@ class AddNewServer extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final myController = controller ?? useTextEditingController(); + final myController = + controller ?? useTextEditingController(text: 'https://'); var newServerURI = useValueListenable(myController); final isServerAlive = ref.watch(isServerAliveProvider(newServerURI.text)); bool isServerAliveValue = isServerAlive.when( @@ -34,21 +38,44 @@ class AddNewServer extends HookConsumerWidget { error: (error, _) => false, ); + Uri parsedUri = Uri.parse(''); + + try { + parsedUri = Uri.parse(newServerURI.text); + } on FormatException { + // prepend https:// if not present + if (!newServerURI.text.startsWith(httpUrlRegExp)) { + myController.text = 'https://${newServerURI.text}'; + parsedUri = Uri.parse(myController.text); + } + } catch (e) { + // do nothing + appLogger.severe('Error parsing URI: $e'); + } + final canSubmit = + !readOnly && + (isServerAliveValue || (allowEmpty && newServerURI.text.isEmpty)); return TextFormField( readOnly: readOnly, controller: controller, keyboardType: TextInputType.url, autofillHints: const [AutofillHints.url], - textInputAction: TextInputAction.next, + textInputAction: TextInputAction.done, + onFieldSubmitted: canSubmit + ? (_) { + onPressed?.call(); + } + : null, decoration: InputDecoration( labelText: 'Server URI', labelStyle: TextStyle( - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.8), ), border: const OutlineInputBorder(), - prefixText: - myController.text.startsWith(RegExp('https?://')) ? '' : 'https://', - prefixIcon: ServerAliveIcon(server: Uri.parse(newServerURI.text)), + prefixText: myController.text.startsWith(httpUrlRegExp) + ? '' + : 'https://', + prefixIcon: ServerAliveIcon(server: parsedUri), // add server button suffixIcon: onPressed == null @@ -62,10 +89,10 @@ class AddNewServer extends HookConsumerWidget { focusColor: Theme.of(context).colorScheme.onSurface, // should be enabled when - onPressed: !readOnly && - (isServerAliveValue || - (allowEmpty && newServerURI.text.isEmpty)) - ? onPressed + onPressed: canSubmit + ? () { + onPressed?.call(); + } : null, // disable button if server is not alive ), ), @@ -76,10 +103,7 @@ class AddNewServer extends HookConsumerWidget { } class ServerAliveIcon extends HookConsumerWidget { - const ServerAliveIcon({ - super.key, - required this.server, - }); + const ServerAliveIcon({super.key, required this.server}); final Uri server; @@ -96,8 +120,8 @@ class ServerAliveIcon extends HookConsumerWidget { message: server.toString().isEmpty ? 'Server Status' : isServerAliveValue - ? 'Server connected' - : 'Cannot connect to server', + ? 'Server connected' + : 'Cannot connect to server', child: server.toString().isEmpty ? Icon( Icons.cloud_outlined, diff --git a/lib/shared/widgets/drawer.dart b/lib/shared/widgets/drawer.dart index be224e9..56bec1f 100644 --- a/lib/shared/widgets/drawer.dart +++ b/lib/shared/widgets/drawer.dart @@ -3,11 +3,8 @@ import 'package:go_router/go_router.dart'; import 'package:vaani/features/you/view/server_manager.dart'; import 'package:vaani/router/router.dart'; - class MyDrawer extends StatelessWidget { - const MyDrawer({ - super.key, - }); + const MyDrawer({super.key}); @override Widget build(BuildContext context) { @@ -17,10 +14,7 @@ class MyDrawer extends StatelessWidget { const DrawerHeader( child: Text( 'Vaani', - style: TextStyle( - fontStyle: FontStyle.italic, - fontSize: 30, - ), + style: TextStyle(fontStyle: FontStyle.italic, fontSize: 30), ), ), ListTile( diff --git a/lib/shared/widgets/expandable_description.dart b/lib/shared/widgets/expandable_description.dart index 06605e4..bc2cee9 100644 --- a/lib/shared/widgets/expandable_description.dart +++ b/lib/shared/widgets/expandable_description.dart @@ -39,7 +39,7 @@ class ExpandableDescription extends HookWidget { // header with carrot icon is tapable InkWell( borderRadius: BorderRadius.circular(8), - + onTap: () { isDescExpanded.value = !isDescExpanded.value; if (isDescExpanded.value) { @@ -55,10 +55,7 @@ class ExpandableDescription extends HookWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // header text - Text( - style: textTheme.titleMedium, - title, - ), + Text(style: textTheme.titleMedium, title), // carrot icon AnimatedRotation( turns: isDescExpanded.value ? 0.5 : 0, @@ -79,11 +76,7 @@ class ExpandableDescription extends HookWidget { child: AnimatedSwitcher( duration: duration * 3, child: isDescExpanded.value - ? Text( - style: textTheme.bodyMedium, - content, - maxLines: null, - ) + ? Text(style: textTheme.bodyMedium, content, maxLines: null) : Text( style: textTheme.bodyMedium, content, diff --git a/lib/shared/widgets/not_implemented.dart b/lib/shared/widgets/not_implemented.dart index 801f152..5c10a60 100644 --- a/lib/shared/widgets/not_implemented.dart +++ b/lib/shared/widgets/not_implemented.dart @@ -2,9 +2,6 @@ import 'package:flutter/material.dart'; void showNotImplementedToast(BuildContext context) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Not implemented"), - showCloseIcon: true, - ), + const SnackBar(content: Text("Not implemented"), showCloseIcon: true), ); } diff --git a/lib/shared/widgets/shelves/author_shelf.dart b/lib/shared/widgets/shelves/author_shelf.dart index aa0acce..36dbadf 100644 --- a/lib/shared/widgets/shelves/author_shelf.dart +++ b/lib/shared/widgets/shelves/author_shelf.dart @@ -6,11 +6,7 @@ import 'package:vaani/shared/widgets/shelves/home_shelf.dart'; /// A shelf that displays Authors on the home page class AuthorHomeShelf extends HookConsumerWidget { - const AuthorHomeShelf({ - super.key, - required this.shelf, - required this.title, - }); + const AuthorHomeShelf({super.key, required this.shelf, required this.title}); final String title; final AuthorShelf shelf; @@ -20,9 +16,7 @@ class AuthorHomeShelf extends HookConsumerWidget { return SimpleHomeShelf( title: title, children: shelf.entities - .map( - (item) => AuthorOnShelf(item: item), - ) + .map((item) => AuthorOnShelf(item: item)) .toList(), ); } @@ -30,10 +24,7 @@ class AuthorHomeShelf extends HookConsumerWidget { // a widget to display a item on the shelf class AuthorOnShelf extends HookConsumerWidget { - const AuthorOnShelf({ - super.key, - required this.item, - }); + const AuthorOnShelf({super.key, required this.item}); final Author item; diff --git a/lib/shared/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart index e4d264a..0919161 100644 --- a/lib/shared/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -8,8 +8,7 @@ import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shimmer/shimmer.dart' show Shimmer; import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/image_provider.dart'; -import 'package:vaani/api/library_item_provider.dart' - show libraryItemProvider; +import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider; import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/features/item_viewer/view/library_item_actions.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; @@ -18,7 +17,7 @@ import 'package:vaani/router/router.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/widgets/shelves/home_shelf.dart'; -import 'package:vaani/theme/theme_from_cover_provider.dart'; +import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; /// A shelf that displays books on the home page class BookHomeShelf extends HookConsumerWidget { @@ -26,10 +25,12 @@ class BookHomeShelf extends HookConsumerWidget { super.key, required this.shelf, required this.title, + this.showPlayButton = false, }); final String title; final LibraryItemShelf shelf; + final bool showPlayButton; @override Widget build(BuildContext context, WidgetRef ref) { @@ -39,10 +40,11 @@ class BookHomeShelf extends HookConsumerWidget { .map( (item) => switch (item.mediaType) { MediaType.book => BookOnShelf( - item: item, - key: ValueKey(shelf.id + item.id), - heroTagSuffix: shelf.id, - ), + item: item, + key: ValueKey(shelf.id + item.id), + heroTagSuffix: shelf.id, + showPlayButton: showPlayButton, + ), _ => Container(), }, ) @@ -72,7 +74,7 @@ class BookOnShelf extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final book = item.media.asBookMinified; final metadata = book.metadata.asBookMetadataMinified; - final coverImage = ref.watch(coverImageProvider(item)); + final coverImage = ref.watch(coverImageProvider(item.id)); return LayoutBuilder( builder: (context, constraints) { final height = min(constraints.maxHeight, 500); @@ -81,14 +83,8 @@ class BookOnShelf extends HookConsumerWidget { // open the book context.pushNamed( Routes.libraryItem.name, - pathParameters: { - Routes.libraryItem.pathParamName!: item.id, - }, - extra: LibraryItemExtras( - book: book, - heroTagSuffix: heroTagSuffix, - coverImage: coverImage.valueOrNull, - ), + pathParameters: {Routes.libraryItem.pathParamName!: item.id}, + extra: LibraryItemExtras(book: book, heroTagSuffix: heroTagSuffix), ); } @@ -98,8 +94,11 @@ class BookOnShelf extends HookConsumerWidget { onTap: handleTapOnBook, borderRadius: BorderRadius.circular(10), child: Padding( - padding: - const EdgeInsets.only(bottom: 8.0, right: 4.0, left: 4.0), + padding: const EdgeInsets.only( + bottom: 8.0, + right: 4.0, + left: 4.0, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -111,51 +110,55 @@ class BookOnShelf extends HookConsumerWidget { alignment: Alignment.bottomRight, children: [ Hero( - tag: HeroTagPrefixes.bookCover + + tag: + HeroTagPrefixes.bookCover + item.id + heroTagSuffix, child: ClipRRect( borderRadius: BorderRadius.circular(10), - child: coverImage.when( - data: (image) { - // return const BookCoverSkeleton(); - if (image.isEmpty) { + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: coverImage.when( + data: (image) { + // return const BookCoverSkeleton(); + if (image.isEmpty) { + return const Icon(Icons.error); + } + var imageWidget = Image.memory( + image, + fit: BoxFit.fill, + cacheWidth: + (height * + 1.2 * + MediaQuery.of( + context, + ).devicePixelRatio) + .round(), + ); + return Container( + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.onPrimaryContainer, + ), + child: imageWidget, + ); + }, + loading: () { + return const Center( + child: BookCoverSkeleton(), + ); + }, + error: (error, stack) { return const Icon(Icons.error); - } - var imageWidget = Image.memory( - image, - fit: BoxFit.fill, - cacheWidth: (height * - 1.2 * - MediaQuery.of(context) - .devicePixelRatio) - .round(), - ); - return Container( - decoration: BoxDecoration( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - child: imageWidget, - ); - }, - loading: () { - return const Center( - child: BookCoverSkeleton(), - ); - }, - error: (error, stack) { - return const Icon(Icons.error); - }, + }, + ), ), ), ), // a play button on the book cover if (showPlayButton) - _BookOnShelfPlayButton( - libraryItemId: item.id, - ), + _BookOnShelfPlayButton(libraryItemId: item.id), ], ), ), @@ -198,10 +201,7 @@ class BookOnShelf extends HookConsumerWidget { } class _BookOnShelfPlayButton extends HookConsumerWidget { - const _BookOnShelfPlayButton({ - super.key, - required this.libraryItemId, - }); + const _BookOnShelfPlayButton({required this.libraryItemId}); /// the id of the library item of the book final String libraryItemId; @@ -214,8 +214,9 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { player.book?.libraryItemId == libraryItemId; final isPlayingThisBook = player.playing && isCurrentBookSetInPlayer; - final userProgress = me.valueOrNull?.mediaProgress - ?.firstWhereOrNull((element) => element.libraryItemId == libraryItemId); + final userProgress = me.value?.mediaProgress?.firstWhereOrNull( + (element) => element.libraryItemId == libraryItemId, + ); final isBookCompleted = userProgress?.isFinished ?? false; const size = 40.0; @@ -223,15 +224,16 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { // if there is user progress for this book show a circular progress indicator around the play button var strokeWidth = size / 8; - final useMaterialThemeOnItemPage = - ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage; + final useMaterialThemeOnItemPage = ref + .watch(appSettingsProvider) + .themeSettings + .useMaterialThemeOnItemPage; AsyncValue coverColorScheme = const AsyncValue.loading(); if (useMaterialThemeOnItemPage && isCurrentBookSetInPlayer) { - final itemFromApi = ref.watch(libraryItemProvider(libraryItemId)); coverColorScheme = ref.watch( themeOfLibraryItemProvider( - itemFromApi.valueOrNull, + libraryItemId, brightness: Theme.of(context).brightness, ), ); @@ -240,8 +242,7 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { return Theme( // if current book is set in player, get theme from the cover image data: ThemeData( - colorScheme: - coverColorScheme.valueOrNull ?? Theme.of(context).colorScheme, + colorScheme: coverColorScheme.value ?? Theme.of(context).colorScheme, ), child: Padding( padding: EdgeInsets.all(strokeWidth / 2 + 2), @@ -256,8 +257,9 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { child: CircularProgressIndicator( value: userProgress.progress, strokeWidth: strokeWidth, - backgroundColor: - Theme.of(context).colorScheme.onPrimary.withOpacity(0.8), + backgroundColor: Theme.of( + context, + ).colorScheme.onPrimary.withValues(alpha: 0.8), valueColor: AlwaysStoppedAnimation( Theme.of(context).colorScheme.primary, ), @@ -268,19 +270,18 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { IconButton( color: Theme.of(context).colorScheme.primary, style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.zero, - ), - minimumSize: WidgetStateProperty.all( - const Size(size, size), - ), + padding: WidgetStateProperty.all(EdgeInsets.zero), + minimumSize: WidgetStateProperty.all(const Size(size, size)), backgroundColor: WidgetStateProperty.all( - Theme.of(context).colorScheme.onPrimary.withOpacity(0.9), + Theme.of( + context, + ).colorScheme.onPrimary.withValues(alpha: 0.9), ), ), onPressed: () async { - final book = - await ref.watch(libraryItemProvider(libraryItemId).future); + final book = await ref.watch( + libraryItemProvider(libraryItemId).future, + ); libraryItemPlayButtonOnPressed( ref: ref, @@ -306,9 +307,7 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { // a skeleton for the book cover class BookCoverSkeleton extends StatelessWidget { - const BookCoverSkeleton({ - super.key, - }); + const BookCoverSkeleton({super.key}); @override Widget build(BuildContext context) { @@ -317,12 +316,13 @@ class BookCoverSkeleton extends StatelessWidget { child: SizedBox( width: 150, child: Shimmer.fromColors( - baseColor: Theme.of(context).colorScheme.surface.withOpacity(0.3), - highlightColor: - Theme.of(context).colorScheme.onSurface.withOpacity(0.1), - child: Container( - color: Theme.of(context).colorScheme.surface, - ), + baseColor: Theme.of( + context, + ).colorScheme.surface.withValues(alpha: 0.3), + highlightColor: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.1), + child: Container(color: Theme.of(context).colorScheme.surface), ), ), ); diff --git a/lib/shared/widgets/shelves/home_shelf.dart b/lib/shared/widgets/shelves/home_shelf.dart index ed22f8d..3770fe1 100644 --- a/lib/shared/widgets/shelves/home_shelf.dart +++ b/lib/shared/widgets/shelves/home_shelf.dart @@ -15,22 +15,25 @@ class HomeShelf extends HookConsumerWidget { super.key, required this.shelf, required this.title, + this.showPlayButton = false, }); final String title; final Shelf shelf; + final bool showPlayButton; @override Widget build(BuildContext context, WidgetRef ref) { return switch (shelf.type) { ShelfType.book => BookHomeShelf( - title: title, - shelf: shelf.asLibraryItemShelf, - ), + title: title, + shelf: shelf.asLibraryItemShelf, + showPlayButton: showPlayButton, + ), ShelfType.authors => AuthorHomeShelf( - title: title, - shelf: shelf.asAuthorShelf, - ), + title: title, + shelf: shelf.asAuthorShelf, + ), _ => Container(), }; } @@ -72,9 +75,7 @@ class SimpleHomeShelf extends HookConsumerWidget { scrollDirection: Axis.horizontal, itemBuilder: (context, index) { if (index == 0 || index == children.length + 1) { - return const SizedBox( - width: 8, - ); + return const SizedBox(width: 8); } return children[index - 1]; }, @@ -85,7 +86,8 @@ class SimpleHomeShelf extends HookConsumerWidget { return const SizedBox(width: 4); }, - itemCount: children.length + + itemCount: + children.length + 2, // add some extra space at the start and end so that the first and last items are not at the edge ), ), diff --git a/lib/shared/widgets/vaani_logo.dart b/lib/shared/widgets/vaani_logo.dart new file mode 100644 index 0000000..a5b4e11 --- /dev/null +++ b/lib/shared/widgets/vaani_logo.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class VaaniLogo extends StatelessWidget { + const VaaniLogo({ + super.key, + this.size, + this.duration = const Duration(milliseconds: 750), + this.curve = Curves.fastOutSlowIn, + }); + + final double? size; + final Duration duration; + final Curve curve; + + @override + Widget build(BuildContext context) { + final IconThemeData iconTheme = IconTheme.of(context); + final double? iconSize = size ?? iconTheme.size; + return AnimatedContainer( + width: iconSize, + height: iconSize, + duration: duration, + curve: curve, + child: Image.asset('assets/images/vaani_logo_foreground.png'), + ); + } +} diff --git a/lib/theme/dark.dart b/lib/theme/dark.dart deleted file mode 100644 index 5473632..0000000 --- a/lib/theme/dark.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:vaani/theme/theme.dart'; - -final ThemeData darkTheme = ThemeData( - brightness: Brightness.dark, - colorScheme: ColorScheme.fromSeed( - seedColor: brandColor, - brightness: Brightness.dark, - ), -); diff --git a/lib/theme/light.dart b/lib/theme/light.dart deleted file mode 100644 index aa6ba4d..0000000 --- a/lib/theme/light.dart +++ /dev/null @@ -1,10 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:vaani/theme/theme.dart'; - -final ThemeData lightTheme = ThemeData( - brightness: Brightness.light, - colorScheme: ColorScheme.fromSeed( - seedColor: brandColor, - brightness: Brightness.light, - ), -); diff --git a/lib/theme/providers/system_theme_provider.dart b/lib/theme/providers/system_theme_provider.dart new file mode 100644 index 0000000..cc01415 --- /dev/null +++ b/lib/theme/providers/system_theme_provider.dart @@ -0,0 +1,67 @@ +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:logging/logging.dart'; +import 'package:material_color_utilities/material_color_utilities.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'system_theme_provider.g.dart'; + +final _logger = Logger('SystemThemeProvider'); + +/// copied from [DynamicColorBuilder] +@Riverpod(keepAlive: true) +FutureOr<(ColorScheme light, ColorScheme dark)?> systemTheme( + Ref ref, { + bool highContrast = false, +}) async { + _logger.fine('Generating system theme'); + ColorScheme? schemeLight; + ColorScheme? schemeDark; + // Platform messages may fail, so we use a try/catch PlatformException. + try { + CorePalette? corePalette = await DynamicColorPlugin.getCorePalette(); + + if (corePalette != null) { + _logger.fine('dynamic_color: Core palette detected.'); + schemeLight = corePalette.toColorScheme(brightness: Brightness.light); + schemeDark = corePalette.toColorScheme(brightness: Brightness.dark); + } + } on PlatformException { + _logger.warning('dynamic_color: Failed to obtain core palette.'); + } + + if (schemeLight == null || schemeDark == null) { + try { + final Color? accentColor = await DynamicColorPlugin.getAccentColor(); + + if (accentColor != null) { + _logger.fine('dynamic_color: Accent color detected.'); + schemeLight = ColorScheme.fromSeed( + seedColor: accentColor, + brightness: Brightness.light, + ); + schemeDark = ColorScheme.fromSeed( + seedColor: accentColor, + brightness: Brightness.dark, + ); + } + } on PlatformException { + _logger.warning('dynamic_color: Failed to obtain accent color.'); + } + } + + if (schemeLight == null || schemeDark == null) { + _logger.warning( + 'dynamic_color: Dynamic color not detected on this device.', + ); + return null; + } + // set high contrast theme + if (highContrast) { + schemeLight = schemeLight.copyWith(surface: Colors.white).harmonized(); + schemeDark = schemeDark.copyWith(surface: Colors.black).harmonized(); + } + return (schemeLight, schemeDark); +} diff --git a/lib/theme/providers/system_theme_provider.g.dart b/lib/theme/providers/system_theme_provider.g.dart new file mode 100644 index 0000000..a28f0f0 --- /dev/null +++ b/lib/theme/providers/system_theme_provider.g.dart @@ -0,0 +1,96 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'system_theme_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// copied from [DynamicColorBuilder] + +@ProviderFor(systemTheme) +final systemThemeProvider = SystemThemeFamily._(); + +/// copied from [DynamicColorBuilder] + +final class SystemThemeProvider + extends + $FunctionalProvider< + AsyncValue<(ColorScheme, ColorScheme)?>, + (ColorScheme, ColorScheme)?, + FutureOr<(ColorScheme, ColorScheme)?> + > + with + $FutureModifier<(ColorScheme, ColorScheme)?>, + $FutureProvider<(ColorScheme, ColorScheme)?> { + /// copied from [DynamicColorBuilder] + SystemThemeProvider._({ + required SystemThemeFamily super.from, + required bool super.argument, + }) : super( + retry: null, + name: r'systemThemeProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$systemThemeHash(); + + @override + String toString() { + return r'systemThemeProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement<(ColorScheme, ColorScheme)?> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr<(ColorScheme, ColorScheme)?> create(Ref ref) { + final argument = this.argument as bool; + return systemTheme(ref, highContrast: argument); + } + + @override + bool operator ==(Object other) { + return other is SystemThemeProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$systemThemeHash() => r'c78d3d94683624a80b296594268c5fd4295e77a3'; + +/// copied from [DynamicColorBuilder] + +final class SystemThemeFamily extends $Family + with + $FunctionalFamilyOverride, bool> { + SystemThemeFamily._() + : super( + retry: null, + name: r'systemThemeProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: false, + ); + + /// copied from [DynamicColorBuilder] + + SystemThemeProvider call({bool highContrast = false}) => + SystemThemeProvider._(argument: highContrast, from: this); + + @override + String toString() => r'systemThemeProvider'; +} diff --git a/lib/theme/theme_from_cover_provider.dart b/lib/theme/providers/theme_from_cover_provider.dart similarity index 74% rename from lib/theme/theme_from_cover_provider.dart rename to lib/theme/providers/theme_from_cover_provider.dart index e6bb8b6..3e9a667 100644 --- a/lib/theme/theme_from_cover_provider.dart +++ b/lib/theme/providers/theme_from_cover_provider.dart @@ -1,8 +1,9 @@ +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/image_provider.dart'; part 'theme_from_cover_provider.g.dart'; @@ -11,18 +12,28 @@ final _logger = Logger('ThemeFromCoverProvider'); @Riverpod(keepAlive: true) Future> themeFromCover( - ThemeFromCoverRef ref, + Ref ref, ImageProvider img, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) async { // ! add deliberate delay to simulate a long running task as it interferes with other animations await Future.delayed(500.ms); _logger.fine('Generating color scheme from cover image'); - return ColorScheme.fromImageProvider( + var theme = await ColorScheme.fromImageProvider( provider: img, brightness: brightness, ); + // set high contrast theme + if (highContrast) { + theme = theme + .copyWith( + surface: brightness == Brightness.light ? Colors.white : Colors.black, + ) + .harmonized(); + } + return theme; // TODO isolate is not working // see https://github.com/flutter/flutter/issues/119207 // use isolate to generate the color scheme @@ -48,17 +59,21 @@ Future> themeFromCover( @Riverpod(keepAlive: true) FutureOr themeOfLibraryItem( - ThemeOfLibraryItemRef ref, - LibraryItem? item, { + Ref ref, + String? itemId, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) async { - if (item == null) { + if (itemId == null) { return null; } - final coverImage = await ref.watch(coverImageProvider(item).future); + final coverImage = await ref.watch(coverImageProvider(itemId).future); final val = await ref.watch( - themeFromCoverProvider(MemoryImage(coverImage), brightness: brightness) - .future, + themeFromCoverProvider( + MemoryImage(coverImage), + brightness: brightness, + highContrast: highContrast, + ).future, ); return val; // coverImage.when( diff --git a/lib/theme/providers/theme_from_cover_provider.g.dart b/lib/theme/providers/theme_from_cover_provider.g.dart new file mode 100644 index 0000000..844e608 --- /dev/null +++ b/lib/theme/providers/theme_from_cover_provider.g.dart @@ -0,0 +1,202 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'theme_from_cover_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(themeFromCover) +final themeFromCoverProvider = ThemeFromCoverFamily._(); + +final class ThemeFromCoverProvider + extends + $FunctionalProvider< + AsyncValue>, + FutureOr, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + ThemeFromCoverProvider._({ + required ThemeFromCoverFamily super.from, + required (ImageProvider, {Brightness brightness, bool highContrast}) + super.argument, + }) : super( + retry: null, + name: r'themeFromCoverProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$themeFromCoverHash(); + + @override + String toString() { + return r'themeFromCoverProvider' + '' + '$argument'; + } + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + final argument = + this.argument + as ( + ImageProvider, { + Brightness brightness, + bool highContrast, + }); + return themeFromCover( + ref, + argument.$1, + brightness: argument.brightness, + highContrast: argument.highContrast, + ); + } + + @override + bool operator ==(Object other) { + return other is ThemeFromCoverProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$themeFromCoverHash() => r'afdeddc4bfe2fe46a4185143d3a88a23565e33f4'; + +final class ThemeFromCoverFamily extends $Family + with + $FunctionalFamilyOverride< + FutureOr>, + (ImageProvider, {Brightness brightness, bool highContrast}) + > { + ThemeFromCoverFamily._() + : super( + retry: null, + name: r'themeFromCoverProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: false, + ); + + ThemeFromCoverProvider call( + ImageProvider img, { + Brightness brightness = Brightness.dark, + bool highContrast = false, + }) => ThemeFromCoverProvider._( + argument: (img, brightness: brightness, highContrast: highContrast), + from: this, + ); + + @override + String toString() => r'themeFromCoverProvider'; +} + +@ProviderFor(themeOfLibraryItem) +final themeOfLibraryItemProvider = ThemeOfLibraryItemFamily._(); + +final class ThemeOfLibraryItemProvider + extends + $FunctionalProvider< + AsyncValue, + ColorScheme?, + FutureOr + > + with $FutureModifier, $FutureProvider { + ThemeOfLibraryItemProvider._({ + required ThemeOfLibraryItemFamily super.from, + required (String?, {Brightness brightness, bool highContrast}) + super.argument, + }) : super( + retry: null, + name: r'themeOfLibraryItemProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$themeOfLibraryItemHash(); + + @override + String toString() { + return r'themeOfLibraryItemProvider' + '' + '$argument'; + } + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = + this.argument as (String?, {Brightness brightness, bool highContrast}); + return themeOfLibraryItem( + ref, + argument.$1, + brightness: argument.brightness, + highContrast: argument.highContrast, + ); + } + + @override + bool operator ==(Object other) { + return other is ThemeOfLibraryItemProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$themeOfLibraryItemHash() => + r'0b2df397b2938003a9de6beb6d4204401a05370c'; + +final class ThemeOfLibraryItemFamily extends $Family + with + $FunctionalFamilyOverride< + FutureOr, + (String?, {Brightness brightness, bool highContrast}) + > { + ThemeOfLibraryItemFamily._() + : super( + retry: null, + name: r'themeOfLibraryItemProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: false, + ); + + ThemeOfLibraryItemProvider call( + String? itemId, { + Brightness brightness = Brightness.dark, + bool highContrast = false, + }) => ThemeOfLibraryItemProvider._( + argument: (itemId, brightness: brightness, highContrast: highContrast), + from: this, + ); + + @override + String toString() => r'themeOfLibraryItemProvider'; +} diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart index 72e81b1..35f4ad8 100644 --- a/lib/theme/theme.dart +++ b/lib/theme/theme.dart @@ -1,9 +1,15 @@ -import 'dart:ui'; - -export 'dark.dart'; -export 'light.dart'; - +import 'package:flutter/material.dart'; // brand color rgb(49, 27, 146) rgb(96, 76, 236) const brandColor = Color(0xFF311B92); const brandColorLight = Color(0xFF604CEC); + +final brandLightColorScheme = ColorScheme.fromSeed( + seedColor: brandColor, + brightness: Brightness.light, +); + +final brandDarkColorScheme = ColorScheme.fromSeed( + seedColor: brandColor, + brightness: Brightness.dark, +); diff --git a/lib/theme/theme_from_cover_provider.g.dart b/lib/theme/theme_from_cover_provider.g.dart deleted file mode 100644 index 8201917..0000000 --- a/lib/theme/theme_from_cover_provider.g.dart +++ /dev/null @@ -1,324 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'theme_from_cover_provider.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$themeFromCoverHash() => r'a549513a0dcdff76be94488baf38a8b886ce63eb'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -/// See also [themeFromCover]. -@ProviderFor(themeFromCover) -const themeFromCoverProvider = ThemeFromCoverFamily(); - -/// See also [themeFromCover]. -class ThemeFromCoverFamily extends Family>> { - /// See also [themeFromCover]. - const ThemeFromCoverFamily(); - - /// See also [themeFromCover]. - ThemeFromCoverProvider call( - ImageProvider img, { - Brightness brightness = Brightness.dark, - }) { - return ThemeFromCoverProvider( - img, - brightness: brightness, - ); - } - - @override - ThemeFromCoverProvider getProviderOverride( - covariant ThemeFromCoverProvider provider, - ) { - return call( - provider.img, - brightness: provider.brightness, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'themeFromCoverProvider'; -} - -/// See also [themeFromCover]. -class ThemeFromCoverProvider extends FutureProvider> { - /// See also [themeFromCover]. - ThemeFromCoverProvider( - ImageProvider img, { - Brightness brightness = Brightness.dark, - }) : this._internal( - (ref) => themeFromCover( - ref as ThemeFromCoverRef, - img, - brightness: brightness, - ), - from: themeFromCoverProvider, - name: r'themeFromCoverProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$themeFromCoverHash, - dependencies: ThemeFromCoverFamily._dependencies, - allTransitiveDependencies: - ThemeFromCoverFamily._allTransitiveDependencies, - img: img, - brightness: brightness, - ); - - ThemeFromCoverProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.img, - required this.brightness, - }) : super.internal(); - - final ImageProvider img; - final Brightness brightness; - - @override - Override overrideWith( - FutureOr> Function(ThemeFromCoverRef provider) - create, - ) { - return ProviderOverride( - origin: this, - override: ThemeFromCoverProvider._internal( - (ref) => create(ref as ThemeFromCoverRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - img: img, - brightness: brightness, - ), - ); - } - - @override - FutureProviderElement> createElement() { - return _ThemeFromCoverProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is ThemeFromCoverProvider && - other.img == img && - other.brightness == brightness; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, img.hashCode); - hash = _SystemHash.combine(hash, brightness.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin ThemeFromCoverRef on FutureProviderRef> { - /// The parameter `img` of this provider. - ImageProvider get img; - - /// The parameter `brightness` of this provider. - Brightness get brightness; -} - -class _ThemeFromCoverProviderElement - extends FutureProviderElement> - with ThemeFromCoverRef { - _ThemeFromCoverProviderElement(super.provider); - - @override - ImageProvider get img => (origin as ThemeFromCoverProvider).img; - @override - Brightness get brightness => (origin as ThemeFromCoverProvider).brightness; -} - -String _$themeOfLibraryItemHash() => - r'575a390a0ab0e66cf54cb090a358c08847270798'; - -/// See also [themeOfLibraryItem]. -@ProviderFor(themeOfLibraryItem) -const themeOfLibraryItemProvider = ThemeOfLibraryItemFamily(); - -/// See also [themeOfLibraryItem]. -class ThemeOfLibraryItemFamily extends Family> { - /// See also [themeOfLibraryItem]. - const ThemeOfLibraryItemFamily(); - - /// See also [themeOfLibraryItem]. - ThemeOfLibraryItemProvider call( - LibraryItem? item, { - Brightness brightness = Brightness.dark, - }) { - return ThemeOfLibraryItemProvider( - item, - brightness: brightness, - ); - } - - @override - ThemeOfLibraryItemProvider getProviderOverride( - covariant ThemeOfLibraryItemProvider provider, - ) { - return call( - provider.item, - brightness: provider.brightness, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'themeOfLibraryItemProvider'; -} - -/// See also [themeOfLibraryItem]. -class ThemeOfLibraryItemProvider extends FutureProvider { - /// See also [themeOfLibraryItem]. - ThemeOfLibraryItemProvider( - LibraryItem? item, { - Brightness brightness = Brightness.dark, - }) : this._internal( - (ref) => themeOfLibraryItem( - ref as ThemeOfLibraryItemRef, - item, - brightness: brightness, - ), - from: themeOfLibraryItemProvider, - name: r'themeOfLibraryItemProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$themeOfLibraryItemHash, - dependencies: ThemeOfLibraryItemFamily._dependencies, - allTransitiveDependencies: - ThemeOfLibraryItemFamily._allTransitiveDependencies, - item: item, - brightness: brightness, - ); - - ThemeOfLibraryItemProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.item, - required this.brightness, - }) : super.internal(); - - final LibraryItem? item; - final Brightness brightness; - - @override - Override overrideWith( - FutureOr Function(ThemeOfLibraryItemRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: ThemeOfLibraryItemProvider._internal( - (ref) => create(ref as ThemeOfLibraryItemRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - item: item, - brightness: brightness, - ), - ); - } - - @override - FutureProviderElement createElement() { - return _ThemeOfLibraryItemProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is ThemeOfLibraryItemProvider && - other.item == item && - other.brightness == brightness; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, item.hashCode); - hash = _SystemHash.combine(hash, brightness.hashCode); - - return _SystemHash.finish(hash); - } -} - -mixin ThemeOfLibraryItemRef on FutureProviderRef { - /// The parameter `item` of this provider. - LibraryItem? get item; - - /// The parameter `brightness` of this provider. - Brightness get brightness; -} - -class _ThemeOfLibraryItemProviderElement - extends FutureProviderElement with ThemeOfLibraryItemRef { - _ThemeOfLibraryItemProviderElement(super.provider); - - @override - LibraryItem? get item => (origin as ThemeOfLibraryItemProvider).item; - @override - Brightness get brightness => - (origin as ThemeOfLibraryItemProvider).brightness; -} -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 879195f..f06b290 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,14 +6,18 @@ #include "generated_plugin_registrant.h" -#include +#include +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); - isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) dynamic_color_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); + dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); + g_autoptr(FlPluginRegistrar) isar_plus_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "IsarPlusFlutterLibsPlugin"); + isar_plus_flutter_libs_plugin_register_with_registrar(isar_plus_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 026cbff..17405b8 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,7 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST - isar_flutter_libs + dynamic_color + isar_plus_flutter_libs media_kit_libs_linux url_launcher_linux ) diff --git a/linux/my_application.cc b/linux/my_application.cc index 8a52c8b..4c7da0c 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -17,6 +17,14 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); + + GList *windows = gtk_application_get_windows(GTK_APPLICATION(application)); + if (windows) + { + gtk_window_present(GTK_WINDOW(windows->data)); + return; + } + GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); @@ -40,11 +48,11 @@ static void my_application_activate(GApplication* application) { if (use_header_bar) { GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "vaani"); + gtk_header_bar_set_title(header_bar, "Vaani"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); } else { - gtk_window_set_title(window, "vaani"); + gtk_window_set_title(window, "Vaani"); } gtk_window_set_default_size(window, 1280, 720); @@ -78,7 +86,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch g_application_activate(application); *exit_status = 0; - return TRUE; + return FALSE; } // Implements GApplication::startup. @@ -119,6 +127,6 @@ static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, + "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, nullptr)); } diff --git a/linux/packaging/appimage/make_config.yaml b/linux/packaging/appimage/make_config.yaml new file mode 100644 index 0000000..b1183ae --- /dev/null +++ b/linux/packaging/appimage/make_config.yaml @@ -0,0 +1,34 @@ +display_name: Vaani +package_name: vaani + +maintainer: + name: Dr.Blank + email: drblankdev@gmail.com + +priority: optional + +section: x11 + +installed_size: 75700 + +essential: false + +icon: assets/icon/logo.png + +postuninstall_scripts: + - echo "Sorry to see you go." + +keywords: + - Audiobook + - Audiobook Player + - Audiobookshelf + +generic_name: Audiobook Player + +categories: + - AudioVideo + - Audio + - Player + +startup_notify: true +# TODO: Review and update fields for AppImage specifics (e.g., icon, metadata). diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml new file mode 100644 index 0000000..5579be4 --- /dev/null +++ b/linux/packaging/deb/make_config.yaml @@ -0,0 +1,51 @@ +display_name: Vaani +package_name: vaani + +maintainer: + name: Dr.Blank + email: drblankdev@gmail.com + +priority: optional + +section: x11 + +installed_size: 75700 + +essential: false + +icon: assets/icon/logo.png + +description: + short: Beautiful, Fast and Functional Audiobook Player for your Audiobookshelf server. + long: | + Vaani is a client for your (self-hosted) Audiobookshelf server. + + Features: + - Functional Player: Speed Control, Sleep Timer, Shake to Control Player + - Save data with Offline listening and caching + - Material Design + - Extensive Settings to customize every tiny detail + + Note: you need an Audiobookshelf server setup for this app to work. + Please see https://www.audiobookshelf.org/ on how to setup one if not already. + +postuninstall_scripts: + - echo "Sorry to see you go." + +keywords: + - Audiobook + - Audiobook Player + - Audiobookshelf + +generic_name: Audiobook Player + +categories: + - AudioVideo + - Audio + - Player + +startup_notify: true + +# https://github.com/llfbandit/app_links/blob/051f53fa6039cbfaef0fcde73df20fef9e248cab/doc/README_linux.md +supported_mime_type: + - x-scheme-handler/vaani diff --git a/privacy-policy.md b/privacy-policy.md new file mode 100644 index 0000000..0ec8a98 --- /dev/null +++ b/privacy-policy.md @@ -0,0 +1,25 @@ +**Privacy Policy** + +This privacy policy applies to the Vaani app (hereby referred to as "Application") for mobile devices that was created by Dr. Blank (hereby referred to as "Service Provider") as an Open Source service. This service is intended for use "AS IS". + +**Information Collection and Use** + +The Application does not collect any information by itself. It is a client for a self-hosted server, meaning that users are responsible for setting up their own servers to use the Application. Any information collected will be by the user's server and not by the Service Provider. + +**Changes** + +This Privacy Policy may be updated from time to time for any reason. The Service Provider will notify you of any changes to the Privacy Policy by updating this page with the new Privacy Policy. You are advised to consult this Privacy Policy regularly for any changes, as continued use is deemed approval of all changes. + +This privacy policy is effective as of 2024-10-01 + +**Your Consent** + +By using the Application, you are consenting to the processing of your information as set forth in this Privacy Policy now and as amended by us. + +**Contact Us** + +If you have any questions regarding privacy while using the Application, or have questions about the practices, please contact the Service Provider via email at drblankdev@gmail.com. + +* * * + +This privacy policy page was generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.nisrulz.com/) diff --git a/pubspec.lock b/pubspec.lock index 2fe54a5..03a42ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,31 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: c209688d9f5a5f26b2fb47a188131a6fb9e876ae9e47af3737c0b4f58a93470d url: "https://pub.dev" source: hosted - version: "72.0.0" - _macros: - dependency: transitive - description: dart - source: sdk - version: "0.3.2" + version: "91.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: f51c8499b35f9b26820cfe914828a6a98a94efd5cc78b37bb7d03debae3a1d08 url: "https://pub.dev" source: hosted - version: "6.7.0" - analyzer_plugin: + version: "8.4.1" + analyzer_buffer: dependency: transitive description: - name: analyzer_plugin - sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" + name: analyzer_buffer + sha256: aba2f75e63b3135fd1efaa8b6abefe1aa6e41b6bd9806221620fa48f98156033 url: "https://pub.dev" source: hosted - version: "0.11.3" + version: "0.1.11" animated_list_plus: dependency: "direct main" description: @@ -47,61 +42,61 @@ packages: source: hosted version: "2.0.10" archive: - dependency: transitive + dependency: "direct main" description: name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "3.6.1" + version: "4.0.7" args: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.0" async: dependency: transitive description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.13.0" audio_service: - dependency: transitive + dependency: "direct main" description: name: audio_service - sha256: "9dd5ba7e77567b290c35908b1950d61485b4dfdd3a0ac398e98cfeec04651b75" + sha256: cb122c7c2639d2a992421ef96b67948ad88c5221da3365ccef1031393a76e044 url: "https://pub.dev" source: hosted - version: "0.18.15" + version: "0.18.18" audio_service_platform_interface: dependency: transitive description: name: audio_service_platform_interface - sha256: "8431a455dac9916cc9ee6f7da5620a666436345c906ad2ebb7fa41d18b3c1bf4" + sha256: "6283782851f6c8b501b60904a32fc7199dc631172da0629d7301e66f672ab777" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.3" audio_service_web: dependency: transitive description: name: audio_service_web - sha256: "4cdc2127cd4562b957fb49227dc58e3303fafb09bde2573bc8241b938cf759d9" + sha256: b8ea9243201ee53383157fbccf13d5d2a866b5dda922ec19d866d1d5d70424df url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.1.4" audio_session: dependency: "direct main" description: name: audio_session - sha256: "343e83bc7809fbda2591a49e525d6b63213ade10c76f15813be9aed6657b3261" + sha256: "2b7fff16a552486d078bfc09a8cde19f426dc6d6329262b684182597bec5b1ac" url: "https://pub.dev" source: hosted - version: "0.1.21" + version: "0.1.25" audio_video_progress_bar: dependency: "direct main" description: @@ -122,66 +117,50 @@ packages: dependency: "direct main" description: name: background_downloader - sha256: "5323dfc5519d7a646f7e61adee31f44f2ede54861d5318dfd451a8e38a285538" + sha256: d3016a9eb584f6cb16384c8b4a008943c39119730d60046044349b5dbbda4ccb url: "https://pub.dev" source: hosted - version: "8.5.3" + version: "9.2.2" boolean_selector: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" build: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: c1668065e9ba04752570ad7e038288559d1e2ca5c6d0131c0f5f55e39e777413 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "4.0.3" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4f64382b97504dc2fcdf487d5aae33418e08b4703fc21249e4db6d804a4d0187" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" - build_resolvers: - dependency: transitive - description: - name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" - url: "https://pub.dev" - source: hosted - version: "2.4.2" + version: "4.0.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 + sha256: "110c56ef29b5eb367b4d17fc79375fa8c18a6cd7acd92c05bb3986c17a079057" url: "https://pub.dev" source: hosted - version: "2.4.12" - build_runner_core: - dependency: transitive - description: - name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 - url: "https://pub.dev" - source: hosted - version: "7.3.2" + version: "2.10.4" built_collection: dependency: transitive description: @@ -194,10 +173,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "426cf75afdb23aa74bd4e471704de3f9393f3c7b04c1e2d9c6f1073ae0b8b139" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.12.1" cached_network_image: dependency: "direct main" description: @@ -222,14 +201,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + chalkdart: + dependency: transitive + description: + name: chalkdart + sha256: "7ffc6bd39c81453fb9ba8dbce042a9c960219b75ea1c07196a7fa41c2fab9e86" + url: "https://pub.dev" + source: hosted + version: "3.0.5" characters: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -238,30 +225,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" - ci: + cli_config: dependency: transitive description: - name: ci - sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec url: "https://pub.dev" source: hosted - version: "0.1.0" - cli_util: - dependency: transitive - description: - name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 - url: "https://pub.dev" - source: hosted - version: "0.4.1" + version: "0.2.0" clock: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" coast: dependency: "direct main" description: @@ -274,26 +253,34 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" convert: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" + coverage: + dependency: transitive + description: + name: coverage + sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d" + url: "https://pub.dev" + source: hosted + version: "1.15.0" cross_file: dependency: transitive description: @@ -306,10 +293,10 @@ packages: dependency: transitive description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" cupertino_icons: dependency: "direct main" description: @@ -318,54 +305,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" - custom_lint: - dependency: "direct dev" - description: - name: custom_lint - sha256: "4939d89e580c36215e48a7de8fd92f22c79dcc3eb11fda84f3402b3b45aec663" - url: "https://pub.dev" - source: hosted - version: "0.6.5" - custom_lint_builder: - dependency: transitive - description: - name: custom_lint_builder - sha256: d9e5bb63ed52c1d006f5a1828992ba6de124c27a531e8fba0a31afffa81621b3 - url: "https://pub.dev" - source: hosted - version: "0.6.5" - custom_lint_core: - dependency: transitive - description: - name: custom_lint_core - sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d" - url: "https://pub.dev" - source: hosted - version: "0.6.5" dart_style: dependency: transitive description: name: dart_style - sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" + sha256: a9c30492da18ff84efe2422ba2d319a89942d93e58eb0b73d32abe822ef54b7b url: "https://pub.dev" source: hosted - version: "2.3.6" + version: "3.1.3" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 + sha256: "0c6396126421b590089447154c5f98a5de423b70cfb15b1578fd018843ee6f53" url: "https://pub.dev" source: hosted - version: "10.1.2" + version: "11.4.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" + dio: + dependency: transitive + description: + name: dio + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" + url: "https://pub.dev" + source: hosted + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" duration_picker: dependency: "direct main" description: @@ -374,6 +353,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d + url: "https://pub.dev" + source: hosted + version: "1.7.0" easy_stepper: dependency: "direct main" description: @@ -386,42 +373,42 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.3" ffi: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" file_picker: - dependency: transitive + dependency: "direct main" description: name: file_picker - sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" + sha256: "77f8e81d22d2a07d0dee2c62e1dda71dc1da73bf43bb2d45af09727406167964" url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "10.1.9" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -431,10 +418,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5" + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.2" flutter_cache_manager: dependency: "direct main" description: @@ -443,54 +430,38 @@ packages: url: "https://pub.dev" source: hosted version: "3.4.1" - flutter_colorpicker: - dependency: transitive - description: - name: flutter_colorpicker - sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" - url: "https://pub.dev" - source: hosted - version: "1.1.0" flutter_hooks: dependency: "direct main" description: name: flutter_hooks - sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + sha256: b772e710d16d7a20c0740c4f855095026b31c7eb5ba3ab67d2bd52021cd9461d url: "https://pub.dev" source: hosted - version: "0.20.5" + version: "0.21.2" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" url: "https://pub.dev" source: hosted - version: "4.0.0" - flutter_material_pickers: - dependency: "direct main" - description: - name: flutter_material_pickers - sha256: "1f0977df9d3977c6621fff602f6956107cf5ff0df58d3441459e5b2e37256131" - url: "https://pub.dev" - source: hosted - version: "3.7.0" + version: "5.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e url: "https://pub.dev" source: hosted - version: "2.0.22" + version: "2.0.28" flutter_riverpod: dependency: transitive description: name: flutter_riverpod - sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" + sha256: "38ec6c303e2c83ee84512f5fc2a82ae311531021938e63d7137eccc107bf3c02" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "3.1.0" flutter_settings_ui: dependency: "direct main" description: @@ -503,10 +474,10 @@ packages: dependency: transitive description: name: flutter_shaders - sha256: "02750b545c01ff4d8e9bbe8f27a7731aa3778402506c67daa1de7f5fc3f4befe" + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.1.3" flutter_test: dependency: "direct dev" description: flutter @@ -521,26 +492,26 @@ packages: dependency: "direct main" description: name: font_awesome_flutter - sha256: "275ff26905134bcb59417cf60ad979136f1f8257f2f449914b2c3e05bbb4cd6f" + sha256: d3a89184101baec7f4600d58840a764d2ef760fe1c5a20ef9e6b0e9b24a07a3a url: "https://pub.dev" source: hosted - version: "10.7.0" + version: "10.8.0" freezed: dependency: "direct dev" description: name: freezed - sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" + sha256: "13065f10e135263a4f5a4391b79a8efc5fb8106f8dd555a9e49b750b45393d77" url: "https://pub.dev" source: hosted - version: "2.5.7" + version: "3.2.3" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "3.1.0" frontend_server_client: dependency: transitive description: @@ -553,18 +524,18 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" go_router: dependency: "direct main" description: name: go_router - sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 url: "https://pub.dev" source: hosted - version: "14.2.7" + version: "14.8.1" graphs: dependency: transitive description: @@ -573,62 +544,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - hive: + hive_plus_secure: dependency: "direct main" description: - name: hive - sha256: "10819524df282842ebae12870e2e0e9ebc3e5c4637bec741ad39b919c589cb20" + name: hive_plus_secure + sha256: "0bf08f07b42bc42137cfb151ee7fbe417c8295db69d13bb316d81abecfb02aed" url: "https://pub.dev" source: hosted - version: "4.0.0-dev.2" + version: "1.1.25" hooks_riverpod: dependency: "direct main" description: name: hooks_riverpod - sha256: "97266a91c994951a06ef0ff3a1c7fb261e52ec7f74e87f0614ea0b7411b859b2" + sha256: b880efcd17757af0aa242e5dceac2fb781a014c22a32435a5daa8f17e9d5d8a9 url: "https://pub.dev" source: hosted - version: "2.5.2" - hotreloader: - dependency: transitive - description: - name: hotreloader - sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e - url: "https://pub.dev" - source: hosted - version: "4.2.0" + version: "3.1.0" http: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.4.0" http_multi_server: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.2" image: dependency: transitive description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.5.4" infinite_listview: dependency: transitive description: @@ -641,34 +604,34 @@ packages: dependency: transitive 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: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" - isar: + version: "1.0.5" + isar_plus: dependency: "direct main" description: - name: isar - sha256: ebf74d87c400bd9f7da14acb31932b50c2407edbbd40930da3a6c2a8143f85a8 + name: isar_plus + sha256: "43d874216d2f1fcec06f209ebbc75e7ca9705076d8c7077ec6ac5ea511dad9dd" url: "https://pub.dev" source: hosted - version: "4.0.0-dev.14" - isar_flutter_libs: + version: "1.2.0" + isar_plus_flutter_libs: dependency: "direct main" description: - name: isar_flutter_libs - sha256: "04a3f4035e213ddb6e78d0132a7c80296a085c2088c2a761b4a42ee5add36983" + name: isar_plus_flutter_libs + sha256: e142590a13b5c9d349555ebd72a25ee34901992a2bb32ba6ccfd414519b48adf url: "https://pub.dev" source: hosted - version: "4.0.0-dev.14" + version: "1.2.0" js: dependency: transitive description: @@ -689,82 +652,83 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c5b2ee75210a0f263c6c7b9eeea80553dbae96ea1bf57f02484e806a3ffdffa3 url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.11.2" just_audio: dependency: "direct main" description: name: just_audio - sha256: d8e8aaf417d33e345299c17f6457f72bd4ba0c549dc34607abb5183a354edc4d + sha256: f978d5b4ccea08f267dae0232ec5405c1b05d3f3cd63f82097ea46c015d5c09e url: "https://pub.dev" source: hosted - version: "0.9.40" + version: "0.9.46" just_audio_background: dependency: "direct main" description: - name: just_audio_background - sha256: "7547b076d5445431c780b0915707f18baa6d9588077d5d8f811e8a77b4e0bec5" - url: "https://pub.dev" - source: hosted - version: "0.0.1-beta.13" + path: just_audio_background + ref: media-notification-config + resolved-ref: fce45f334f0838cb6f630548efb65fec40ff17b4 + url: "https://github.com/Dr-Blank/just_audio" + source: git + version: "0.0.1-beta.15" just_audio_media_kit: dependency: "direct main" description: name: just_audio_media_kit - sha256: "7f57d317fafa04cb3e70b924e8f632ffb7eca7a97a369e1e44738ed89fbd5da1" + sha256: f3cf04c3a50339709e87e90b4e841eef4364ab4be2bdbac0c54cc48679f84d23 url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.0" just_audio_platform_interface: dependency: transitive description: name: just_audio_platform_interface - sha256: "0243828cce503c8366cc2090cefb2b3c871aa8ed2f520670d76fd47aa1ab2790" + sha256: "4cd94536af0219fa306205a58e78d67e02b0555283c1c094ee41e402a14a5c4a" url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.5.0" just_audio_web: dependency: transitive description: name: just_audio_web - sha256: b163878529d9b028c53a6972fcd58cae2405bcd11cbfcea620b6fb9f151429d6 + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.16" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: name: lints - sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "5.1.1" list_wheel_scroll_view_nls: dependency: "direct main" description: @@ -773,62 +737,78 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + logger: + dependency: transitive + description: + name: logger + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 + url: "https://pub.dev" + source: hosted + version: "2.6.2" logging: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" + logging_appenders: + dependency: "direct main" + description: + name: logging_appenders + sha256: e329e7472f99416d0edaaf6451fe6c02dec91d34535bd252e284a0b94ab23d79 + url: "https://pub.dev" + source: hosted + version: "1.3.1" lottie: dependency: "direct main" description: name: lottie - sha256: "6a24ade5d3d918c306bb1c21a6b9a04aab0489d51a2582522eea820b4093b62b" + sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950 url: "https://pub.dev" source: hosted - version: "3.1.2" - macros: - dependency: transitive - description: - name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" - url: "https://pub.dev" - source: hosted - version: "0.1.2-main.4" + version: "3.3.1" matcher: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: - dependency: transitive + dependency: "direct main" description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted version: "0.11.1" + material_symbols_icons: + dependency: "direct main" + description: + name: material_symbols_icons + sha256: "7c50901b39d1ad645ee25d920aed008061e1fd541a897b4ebf2c01d966dbf16b" + url: "https://pub.dev" + source: hosted + version: "4.2815.1" media_kit: dependency: transitive description: name: media_kit - sha256: "1f1deee148533d75129a6f38251ff8388e33ee05fc2d20a6a80e57d6051b7b62" + sha256: "48c10c3785df5d88f0eef970743f8c99b2e5da2b34b9d8f9876e598f62d9e776" url: "https://pub.dev" source: hosted - version: "1.1.11" + version: "1.2.0" media_kit_libs_linux: dependency: "direct main" description: name: media_kit_libs_linux - sha256: e186891c31daa6bedab4d74dcdb4e8adfccc7d786bfed6ad81fe24a3b3010310 + sha256: "2b473399a49ec94452c4d4ae51cfc0f6585074398d74216092bf3d54aac37ecf" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.2.1" media_kit_libs_windows_audio: dependency: "direct main" description: @@ -841,18 +821,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.17.0" mime: dependency: transitive description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" miniplayer: dependency: "direct main" description: @@ -862,6 +842,22 @@ packages: url: "https://github.com/Dr-Blank/miniplayer.git" source: git version: "1.0.3" + mockito: + dependency: transitive + description: + name: mockito + sha256: dac24d461418d363778d53198d9ac0510b9d073869f078450f195766ec48d05e + url: "https://pub.dev" + source: hosted + version: "5.6.1" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" numberpicker: dependency: "direct main" description: @@ -882,58 +878,58 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "8.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.2.0" path: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_provider: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.10" + version: "2.2.17" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -958,22 +954,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" + url: "https://pub.dev" + source: hosted + version: "11.4.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.dev" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" platform: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -990,64 +1034,64 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 + url: "https://pub.dev" + source: hosted + version: "6.0.2" pub_semver: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.5.0" riverpod: dependency: transitive description: name: riverpod - sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d + sha256: "16ff608d21e8ea64364f2b7c049c94a02ab81668f78845862b6e88b71dd4935a" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "3.1.0" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: ac28d7bc678471ec986b42d88e5a0893513382ff7542c7ac9634463b044ac72c + sha256: "947b05d04c52a546a2ac6b19ef2a54b08520ff6bdf9f23d67957a4c8df1c3bc0" url: "https://pub.dev" source: hosted - version: "0.5.4" + version: "1.0.0-dev.8" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c + sha256: cc1474bc2df55ec3c1da1989d139dcef22cd5e2bd78da382e867a69a8eca2e46 url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "4.0.0" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "63311e361ffc578d655dfc31b48dfa4ed3bc76fd06f9be845e9bf97c5c11a429" + sha256: e43b1537229cc8f487f09b0c20d15dba840acbadcf5fc6dad7ad5e8ab75950dc url: "https://pub.dev" source: hosted - version: "2.4.3" - riverpod_lint: - dependency: "direct dev" - description: - name: riverpod_lint - sha256: a35a92f2c2a4b7a5d95671c96c5432b42c20f26bb3e985e83d0b186471b61a85 - url: "https://pub.dev" - source: hosted - version: "2.3.13" + version: "4.0.0+1" rxdart: - dependency: "direct main" + dependency: transitive description: name: rxdart sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" @@ -1058,10 +1102,10 @@ packages: dependency: transitive description: name: safe_local_storage - sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440 + sha256: e9a21b6fec7a8aa62cc2585ff4c1b127df42f3185adbd2aca66b47abe2e80236 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.1" scroll_loop_auto_scroll: dependency: "direct main" description: @@ -1074,34 +1118,66 @@ packages: dependency: "direct main" description: name: sensors_plus - sha256: "90f2d38471ca75625f6569d1044d783e0add43548692fbe6e53b008a38a8313a" + sha256: "905282c917c6bb731c242f928665c2ea15445aa491249dea9d98d7c79dc8fd39" url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.1.1" sensors_plus_platform_interface: dependency: transitive description: name: sensors_plus_platform_interface - sha256: b6cacfe243cbeb16403ba688cb0d7054ad4dccb946dcd1254bebdf345fe4b187 + sha256: "58815d2f5e46c0c41c40fb39375d3f127306f7742efe3b891c0b1c87e2b5cd5d" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da + url: "https://pub.dev" + source: hosted + version: "10.1.4" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b + url: "https://pub.dev" + source: hosted + version: "5.0.2" shelf: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0" shelfsdk: dependency: "direct main" description: @@ -1121,47 +1197,63 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" socket_io_client: dependency: transitive description: name: socket_io_client - sha256: "15c9deae04e2b21065adbe864a641e0a5fc2499ff4c89e98a3877aea22ce841c" + sha256: c8471c2c6843cf308a5532ff653f2bcdb7fa9ae79d84d1179920578a06624f0d url: "https://pub.dev" source: hosted - version: "3.0.0-beta.2" + version: "3.1.2" socket_io_common: dependency: transitive description: name: socket_io_common - sha256: "5ad0f12e1b17f4300b0d6a27feaf1b7c1153ca21498ba307f0297b81449f7a44" + sha256: "162fbaecbf4bf9a9372a62a341b3550b51dcef2f02f3e5830a297fd48203d45b" url: "https://pub.dev" source: hosted - version: "3.0.0-beta.0" + version: "3.1.1" source_gen: dependency: transitive description: name: source_gen - sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + sha256: "07b277b67e0096c45196cbddddf2d8c6ffc49342e88bf31d460ce04605ddac75" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "4.1.1" source_helper: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "6a3c6cc82073a8797f8c4dc4572146114a39652851c157db37e964d9c7038723" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.8" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" + url: "https://pub.dev" + source: hosted + version: "0.10.13" source_span: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -1174,26 +1266,50 @@ packages: dependency: transitive description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" + sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" url: "https://pub.dev" source: hosted - version: "2.5.4+2" + version: "2.5.5" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" stack_trace: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" state_notifier: dependency: transitive description: @@ -1206,66 +1322,74 @@ packages: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" synchronized: dependency: transitive description: name: synchronized - sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.3.1" term_glyph: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" + test: + dependency: transitive + description: + name: test + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + url: "https://pub.dev" + source: hosted + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.2" - timing: + version: "0.7.7" + test_core: dependency: transitive description: - name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + name: test_core + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "0.6.12" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_platform: dependency: transitive description: @@ -1278,50 +1402,50 @@ packages: dependency: transitive description: name: uri_parser - sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835" + sha256: ff4d2c720aca3f4f7d5445e23b11b2d15ef8af5ddce5164643f38ff962dcb270 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "3.0.0" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab + sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79" url: "https://pub.dev" source: hosted - version: "6.3.10" + version: "6.3.16" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.3" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1334,98 +1458,122 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" uuid: dependency: transitive description: name: uuid - sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.4.2" + version: "4.5.1" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" + vibration: + dependency: "direct main" + description: + name: vibration + sha256: "804ee8f9628f31ee71fbe6137a2bc6206a64e101ec22cd9dd6d3a7dc0272591b" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + vibration_platform_interface: + dependency: transitive + description: + name: vibration_platform_interface + sha256: "03e9deaa4df48a1a6212e281bfee5f610d62e9247929dd2f26f4efd4fa5e225c" + url: "https://pub.dev" + source: hosted + version: "0.1.0" vm_service: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "15.0.0" watcher: dependency: transitive description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" web_socket: 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: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.3" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" win32: dependency: transitive description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.13.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "2.1.0" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1438,10 +1586,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" + dart: ">=3.10.0 <4.0.0" + flutter: "3.38.6" diff --git a/pubspec.yaml b/pubspec.yaml index b08f1d1..6475fe3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,12 +16,11 @@ 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.0.3+1 +version: 0.0.19+10 environment: - sdk: ">=3.3.4 <4.0.0" - -isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used + sdk: ">=3.10.0 <4.0.0" + flutter: 3.38.6 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -32,38 +31,49 @@ isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used dependencies: animated_list_plus: ^0.5.2 animated_theme_switcher: ^2.0.10 - audio_session: ^0.1.19 + archive: ^4.0.5 + audio_service: ^0.18.15 + audio_session: ^0.1.23 audio_video_progress_bar: ^2.0.2 auto_scroll_text: ^0.0.7 - background_downloader: ^8.5.2 + background_downloader: ^9.2.0 cached_network_image: ^3.3.1 coast: ^2.0.2 collection: ^1.18.0 cupertino_icons: ^1.0.6 - device_info_plus: ^10.1.0 + device_info_plus: ^11.3.3 duration_picker: ^1.2.0 + dynamic_color: ^1.7.0 easy_stepper: ^0.8.4 + file_picker: ^10.0.0 flutter: sdk: flutter flutter_animate: ^4.5.0 flutter_cache_manager: ^3.3.2 - flutter_hooks: ^0.20.5 - flutter_material_pickers: ^3.6.0 + flutter_hooks: ^0.21.2 flutter_settings_ui: ^3.0.1 font_awesome_flutter: ^10.7.0 - freezed_annotation: ^2.4.1 + freezed_annotation: ^3.1.0 go_router: ^14.0.2 - hive: ^4.0.0-dev.2 - hooks_riverpod: ^2.5.1 - isar: ^4.0.0-dev.13 - isar_flutter_libs: ^4.0.0-dev.13 + hive_plus_secure: ^1.1.25 + hooks_riverpod: ^3.0.0 + isar_plus: ^1.1.0 + isar_plus_flutter_libs: ^1.1.0 json_annotation: ^4.9.0 just_audio: ^0.9.37 - just_audio_background: ^0.0.1-beta.11 + just_audio_background: + # TODO Remove git dep when https://github.com/ryanheise/just_audio/issues/912 is closed + git: + url: https://github.com/Dr-Blank/just_audio + ref: media-notification-config + path: just_audio_background just_audio_media_kit: ^2.0.4 list_wheel_scroll_view_nls: ^0.0.3 logging: ^1.2.0 + logging_appenders: ^1.3.1 lottie: ^3.1.0 + material_color_utilities: ^0.11.1 + material_symbols_icons: ^4.2785.1 media_kit_libs_linux: any media_kit_libs_windows_audio: any miniplayer: @@ -74,24 +84,25 @@ dependencies: package_info_plus: ^8.0.0 path: ^1.9.0 path_provider: ^2.1.0 - riverpod_annotation: ^2.3.5 - rxdart: ^0.28.0 + permission_handler: ^11.3.1 + riverpod_annotation: 4.0.0 scroll_loop_auto_scroll: ^0.0.5 sensors_plus: ^6.0.1 + share_plus: ^10.0.2 shelfsdk: path: ./shelfsdk shimmer: ^3.0.0 url_launcher: ^6.2.6 + vibration: ^3.1.3 dev_dependencies: build_runner: ^2.4.9 - custom_lint: ^0.6.4 - flutter_lints: ^4.0.0 + # custom_lint: ^0.8.1 + flutter_lints: ^5.0.0 flutter_test: sdk: flutter - freezed: ^2.5.2 + freezed: ^3.1.0 json_serializable: ^6.8.0 - riverpod_generator: ^2.4.2 - riverpod_lint: ^2.3.10 + riverpod_generator: ^4.0.0+1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -106,6 +117,9 @@ flutter: assets: - assets/ - assets/animations/ + - assets/sounds/ + - assets/images/ + - assets/fonts/ # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg # An image asset can refer to one or more resolution-specific "variants", see @@ -131,3 +145,7 @@ flutter: # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages + fonts: + - family: AbsIcons + fonts: + - asset: assets/fonts/AbsIcons.ttf diff --git a/shelfsdk b/shelfsdk new file mode 160000 index 0000000..34b2b98 --- /dev/null +++ b/shelfsdk @@ -0,0 +1 @@ +Subproject commit 34b2b98a90e3851f50f9657b1ff5bae5f385dbf9 diff --git a/test/features/logging/providers/logs_provider_test.dart b/test/features/logging/providers/logs_provider_test.dart new file mode 100644 index 0000000..68590b8 --- /dev/null +++ b/test/features/logging/providers/logs_provider_test.dart @@ -0,0 +1,24 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:logging/logging.dart'; +import 'package:logging_appenders/logging_appenders.dart'; +import 'package:vaani/features/logging/providers/logs_provider.dart'; + +void main() { + test( + 'Should parse log line', + () async { + final formatter = DefaultLogRecordFormatter(); + final logRecord = LogRecord( + Level.INFO, + 'getting location for name: "logs"', + 'GoRouter', + ); + final expected = parseLogLine( + formatter.format(logRecord), + ); + expect(logRecord.message, expected.message); + expect(logRecord.level, expected.level); + expect(logRecord.loggerName, expected.loggerName); + }, + ); +} diff --git a/test/shared/extensions/time_of_day_test.dart b/test/shared/extensions/time_of_day_test.dart new file mode 100644 index 0000000..131ba6d --- /dev/null +++ b/test/shared/extensions/time_of_day_test.dart @@ -0,0 +1,136 @@ +// ignore_for_file: constant_identifier_names + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:vaani/shared/extensions/time_of_day.dart'; + +const TIME_08_30_AM = TimeOfDay(hour: 8, minute: 30); +const TIME_09_00_AM = TimeOfDay(hour: 9, minute: 0); +const TIME_10_30_AM = TimeOfDay(hour: 10, minute: 30); +const TIME_11_00_AM = TimeOfDay(hour: 11, minute: 0); +const TIME_12_00_PM = TimeOfDay(hour: 12, minute: 0); +const TIME_10_00_PM = TimeOfDay(hour: 22, minute: 0); +void main() { + group('ToTimeOfDay extension', () { + test('Duration to TimeOfDay conversion', () { + final duration = Duration(hours: 10, minutes: 30); + expect(duration.toTimeOfDay(), TIME_10_30_AM); + }); + + test('Duration to TimeOfDay conversion with more than 24 hours', () { + final duration = Duration(hours: 32, minutes: 30); + expect(duration.toTimeOfDay(), TIME_08_30_AM); + }); + }); + + group('ToDuration extension', () { + test('TimeOfDay to Duration conversion', () { + final duration = TIME_10_30_AM.toDuration(); + expect(duration.inHours, 10); + expect(duration.inMinutes % 60, 30); + }); + }); + + group('Compare TimeOfDay', () { + test('compareTo method', () { + expect(TIME_10_30_AM.compareTo(TIME_12_00_PM), -1); + expect(TIME_12_00_PM.compareTo(TIME_10_30_AM), 1); + expect(TIME_10_30_AM.compareTo(TIME_10_30_AM), 0); + }); + + test('operator <', () { + expect(TIME_10_30_AM < TIME_12_00_PM, true); + expect(TIME_12_00_PM < TIME_10_30_AM, false); + }); + + test('operator >', () { + expect(TIME_10_30_AM > TIME_10_00_PM, false); + expect(TIME_10_00_PM > TIME_10_30_AM, true); + }); + + test('operator <=', () { + expect(TIME_10_30_AM <= TIME_12_00_PM, true); + expect(TIME_12_00_PM <= TIME_10_30_AM, false); + expect(TIME_10_30_AM <= TIME_10_30_AM, true); + }); + + test('operator >=', () { + expect(TIME_10_30_AM >= TIME_10_00_PM, false); + expect(TIME_10_00_PM >= TIME_10_30_AM, true); + expect(TIME_10_30_AM >= TIME_10_30_AM, true); + }); + }); + + group('isBetween method', () { + void testIsBetween( + TimeOfDay time, + TimeOfDay start, + TimeOfDay end, + bool expectedResult, + ) { + test('TimeOfDay $time is between $start and $end', () { + expect(time.isBetween(start, end), expectedResult); + }); + } + + final testCases = [ + ( + time: TIME_10_30_AM, + start: TIME_09_00_AM, + end: TIME_11_00_AM, + expectedResult: true + ), + ( + time: TIME_08_30_AM, + start: TIME_09_00_AM, + end: TIME_11_00_AM, + expectedResult: false + ), + ( + time: TIME_10_30_AM, + start: TIME_11_00_AM, + end: TIME_09_00_AM, + expectedResult: false + ), + ( + time: TIME_08_30_AM, + start: TIME_10_00_PM, + end: TIME_09_00_AM, + expectedResult: true + ), + ( + time: TIME_12_00_PM, + start: TIME_11_00_AM, + end: TIME_09_00_AM, + expectedResult: true + ), + ( + time: TIME_10_00_PM, + start: TIME_11_00_AM, + end: TIME_09_00_AM, + expectedResult: true + ), + ( + time: TIME_09_00_AM, + start: TIME_09_00_AM, + end: TIME_09_00_AM, + expectedResult: true + ), + ( + time: TIME_10_00_PM, + start: TIME_09_00_AM, + end: TIME_09_00_AM, + expectedResult: false + ), + ]; + + for (var testCase in testCases) { + testIsBetween( + testCase.time, + testCase.start, + testCase.end, + testCase.expectedResult, + ); + } + }); +} diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 1e47bc9..050981d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,15 +6,24 @@ #include "generated_plugin_registrant.h" -#include +#include +#include #include +#include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { - IsarFlutterLibsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); + DynamicColorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); + IsarPlusFlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("IsarPlusFlutterLibsPlugin")); MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c41e9ee..5b8e406 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,8 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST - isar_flutter_libs + dynamic_color + isar_plus_flutter_libs media_kit_libs_windows_audio + permission_handler_windows + share_plus url_launcher_windows )