diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..c0ef54d --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.32.0" +} \ 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 9ec7edd..3e3ee30 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -40,7 +40,7 @@ template: | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION exclude-labels: - - "skip-changelog" + - "skip changelog" exclude-contributors: - "Dr-Blank" @@ -55,15 +55,15 @@ autolabeler: branch: - '/feature\/.+/' title: - - "/feat(ure)?/i" + - "/^feat(ure)?/i" body: - "/JIRA-[0-9]{1,4}/" - label: "chore" title: - - "/chore/i" + - "/^chore\b/i" - label: "ui" title: - "/^ui\b/i" - label: "refactor" title: - - "/refactor/i" + - "/^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 e5544be..0000000 --- a/.github/workflows/flutter_release.yaml +++ /dev/null @@ -1,84 +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: 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-release-universal}.apk - - - name: Build App Bundle - run: flutter build appbundle --release - - - 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@v6 - env: - GITHUB_TOKEN: ${{ github.token }} - - - name: Create GitHub Release - uses: ncipollo/release-action@v1 - with: - artifacts: "build/app/outputs/flutter-apk/*-release*.apk,build/app/outputs/bundle/release/*.aab" - name: v${{ steps.version.outputs.version }} - tag: ${{ github.ref }} - body: ${{ steps.generate_release_notes.outputs.body }} diff --git a/.github/workflows/flutter_test.yaml b/.github/workflows/flutter_test.yaml deleted file mode 100644 index 9c74813..0000000 --- a/.github/workflows/flutter_test.yaml +++ /dev/null @@ -1,53 +0,0 @@ -name: Flutter Test - -on: - push: - branches: - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - - 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: Checkout shelfsdk - uses: actions/checkout@v3 - with: - repository: Dr-Blank/shelfsdk - path: ./shelfsdk - - - 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: Set Up Java - uses: actions/setup-java@v3.12.0 - with: - distribution: "oracle" - java-version: "17" - - - name: Build APK - run: flutter build apk --release - - - name: Upload APKs - uses: actions/upload-artifact@v4 - with: - name: app-release - path: build/app/outputs/flutter-apk/*.apk 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..9185c43 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.32.0", + "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 c50a48f..14f16fb 100644 --- a/README.md +++ b/README.md @@ -18,19 +18,25 @@ Client for [Audiobookshelf](https://github.com/advplyr/audiobookshelf) server ma ### Android -[Get it on GitHub](https://github.com/Dr-Blank/Vaani/releases/latest/download/app-release-universal.apk) [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) -Playstore App is in closed testing. To join testing -1. [Join the Google Group](https://groups.google.com/g/vaani-app) -2. [Join on Android](https://play.google.com/store/apps/details?id=dr.blank.vaani) Or [Join on Web](https://play.google.com/apps/testing/dr.blank.vaani) +*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. 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 b13356f..0161f0d 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -31,7 +31,11 @@ if (keystorePropertiesFile.exists()) { android { namespace "dr.blank.vaani" compileSdk flutter.compileSdkVersion - ndkVersion flutter.ndkVersion + // 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 @@ -46,6 +50,15 @@ 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" @@ -80,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" + } +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 71b0a89..3856060 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,10 +7,13 @@ + + 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 1d3745f..a991727 100644 --- a/lib/api/api_provider.dart +++ b/lib/api/api_provider.dart @@ -2,12 +2,15 @@ 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'; @@ -32,7 +35,7 @@ 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; @@ -45,10 +48,10 @@ AudiobookshelfApi audiobookshelfApi(AudiobookshelfApiRef ref, Uri? baseUrl) { /// /// 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( @@ -59,7 +62,7 @@ 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; } @@ -77,7 +80,7 @@ FutureOr isServerAlive(IsServerAliveRef ref, String address) async { /// fetch status of server @riverpod FutureOr serverStatus( - ServerStatusRef ref, + Ref ref, Uri baseUrl, [ ResponseErrorHandler? responseErrorHandler, ]) async { @@ -97,17 +100,28 @@ 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!); + 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), + 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 key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}'; final cachedRes = await apiResponseCacheManager.getFileFromMemory( key, ) ?? @@ -127,7 +141,7 @@ class PersonalizedView extends _$PersonalizedView { } } - // ! exagerated delay + // ! exaggerated delay // await Future.delayed(const Duration(seconds: 2)); final res = await api.libraries .getPersonalized(libraryId: apiSettings.activeLibraryId!); @@ -151,6 +165,7 @@ class PersonalizedView extends _$PersonalizedView { // 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(); } } @@ -158,7 +173,7 @@ class PersonalizedView extends _$PersonalizedView { /// fetch continue listening audiobooks @riverpod FutureOr fetchContinueListening( - FetchContinueListeningRef ref, + Ref ref, ) async { final api = ref.watch(authenticatedApiProvider); final res = await api.me.getSessions(); @@ -170,9 +185,50 @@ FutureOr fetchContinueListening( @riverpod FutureOr me( - MeRef ref, + 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 7758c7b..619a729 100644 --- a/lib/api/api_provider.g.dart +++ b/lib/api/api_provider.g.dart @@ -6,7 +6,7 @@ part of 'api_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$audiobookshelfApiHash() => r'2c310ea77fea9918ccf96180a92075acd037bd95'; +String _$audiobookshelfApiHash() => r'f23a06c404e11867a7f796877eaca99b8ff25458'; /// Copied from Dart SDK class _SystemHash { @@ -154,6 +154,8 @@ class AudiobookshelfApiProvider extends AutoDisposeProvider { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin AudiobookshelfApiRef on AutoDisposeProviderRef { /// The parameter `baseUrl` of this provider. Uri? get baseUrl; @@ -168,7 +170,7 @@ class _AudiobookshelfApiProviderElement Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl; } -String _$authenticatedApiHash() => r'f555efb6eede590b5a8d60cad2e6bfc2847e2d14'; +String _$authenticatedApiHash() => r'284be2c39823c20fb70035a136c430862c28fa27'; /// get the api instance for the authenticated user /// @@ -186,8 +188,10 @@ final authenticatedApiProvider = Provider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef AuthenticatedApiRef = ProviderRef; -String _$isServerAliveHash() => r'6ff90b6e0febd2cd4a4d3a5209a59afc778cd3b6'; +String _$isServerAliveHash() => r'bb3a53cae1eb64b8760a56864feed47b7a3f1c29'; /// ping the server to check if it is reachable /// @@ -314,6 +318,8 @@ class IsServerAliveProvider extends AutoDisposeFutureProvider { } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin IsServerAliveRef on AutoDisposeFutureProviderRef { /// The parameter `address` of this provider. String get address; @@ -327,7 +333,7 @@ class _IsServerAliveProviderElement String get address => (origin as IsServerAliveProvider).address; } -String _$serverStatusHash() => r'd7079e19e68f5f61b0afa0f73a2af8807c4b3cf6'; +String _$serverStatusHash() => r'2d9c5d6f970caec555e5322d43a388ea8572619f'; /// fetch status of server /// @@ -467,6 +473,8 @@ class ServerStatusProvider } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin ServerStatusRef on AutoDisposeFutureProviderRef { /// The parameter `baseUrl` of this provider. Uri get baseUrl; @@ -488,7 +496,7 @@ class _ServerStatusProviderElement } String _$fetchContinueListeningHash() => - r'f65fe3ac3a31b8ac074330525c5d2cc4b526802d'; + r'50aeb77369eda38d496b2f56f3df2aea135dab45'; /// fetch continue listening audiobooks /// @@ -505,9 +513,11 @@ final fetchContinueListeningProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef FetchContinueListeningRef = AutoDisposeFutureProviderRef; -String _$meHash() => r'bdc664c4fd867ad13018fa769ce7a6913248c44f'; +String _$meHash() => r'b3b6d6d940b465c60d0c29cd6e81ba2fcccab186'; /// See also [me]. @ProviderFor(me) @@ -520,8 +530,139 @@ final meProvider = AutoDisposeFutureProvider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef MeRef = AutoDisposeFutureProviderRef; -String _$personalizedViewHash() => r'4c392ece4650bdc36d7195a0ddb8810e8fe4caa9'; +String _$loginHash() => r'99410c2bed9c8f412c7b47c4e655db64e0054be2'; + +/// See also [login]. +@ProviderFor(login) +const loginProvider = LoginFamily(); + +/// See also [login]. +class LoginFamily extends Family> { + /// See also [login]. + const LoginFamily(); + + /// See also [login]. + LoginProvider call({ + AuthenticatedUser? user, + }) { + return LoginProvider( + user: user, + ); + } + + @override + LoginProvider getProviderOverride( + covariant LoginProvider provider, + ) { + return call( + user: provider.user, + ); + } + + 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'loginProvider'; +} + +/// See also [login]. +class LoginProvider extends AutoDisposeFutureProvider { + /// See also [login]. + LoginProvider({ + AuthenticatedUser? user, + }) : this._internal( + (ref) => login( + ref as LoginRef, + user: user, + ), + from: loginProvider, + name: r'loginProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$loginHash, + dependencies: LoginFamily._dependencies, + allTransitiveDependencies: LoginFamily._allTransitiveDependencies, + user: user, + ); + + LoginProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.user, + }) : super.internal(); + + final AuthenticatedUser? user; + + @override + Override overrideWith( + FutureOr Function(LoginRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: LoginProvider._internal( + (ref) => create(ref as LoginRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + user: user, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _LoginProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is LoginProvider && other.user == user; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, user.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin LoginRef on AutoDisposeFutureProviderRef { + /// The parameter `user` of this provider. + AuthenticatedUser? get user; +} + +class _LoginProviderElement + extends AutoDisposeFutureProviderElement with LoginRef { + _LoginProviderElement(super.provider); + + @override + AuthenticatedUser? get user => (origin as LoginProvider).user; +} + +String _$personalizedViewHash() => r'425e89d99d7e4712b4d6a688f3a12442bd66584f'; /// fetch the personalized view /// @@ -540,4 +681,4 @@ final personalizedViewProvider = 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/api/authenticated_user_provider.g.dart b/lib/api/authenticated_user_provider.g.dart deleted file mode 100644 index 65f7c32..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'1983527595207c63a12bc84cf0bf1a3c1d729506'; - -/// 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 86% rename from lib/api/authenticated_user_provider.dart rename to lib/api/authenticated_users_provider.dart index c4f065c..5e78fab 100644 --- a/lib/api/authenticated_user_provider.dart +++ b/lib/api/authenticated_users_provider.dart @@ -8,15 +8,15 @@ import 'package:vaani/settings/models/audiobookshelf_server.dart'; 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((_, __) { @@ -56,6 +56,7 @@ class AuthenticatedUser extends _$AuthenticatedUser { 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( @@ -82,9 +83,12 @@ class AuthenticatedUser extends _$AuthenticatedUser { // also remove the user from the active user final apiSettings = ref.read(apiSettingsProvider); if (apiSettings.activeUser == user) { + // 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: null, + 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..44a2610 --- /dev/null +++ b/lib/api/authenticated_users_provider.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'authenticated_users_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$authenticatedUsersHash() => + r'5fdd472f62fc3b73ff8417cdce9f02e86c33d00f'; + +/// provides with a set of authenticated users +/// +/// Copied from [AuthenticatedUsers]. +@ProviderFor(AuthenticatedUsers) +final authenticatedUsersProvider = AutoDisposeNotifierProvider< + AuthenticatedUsers, Set>.internal( + AuthenticatedUsers.new, + name: r'authenticatedUsersProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$authenticatedUsersHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AuthenticatedUsers + = 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, deprecated_member_use_from_same_package diff --git a/lib/api/image_provider.g.dart b/lib/api/image_provider.g.dart index 5fa646e..25f94d6 100644 --- a/lib/api/image_provider.g.dart +++ b/lib/api/image_provider.g.dart @@ -155,6 +155,8 @@ class CoverImageProvider } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin CoverImageRef on StreamNotifierProviderRef { /// The parameter `itemId` of this provider. String get itemId; @@ -169,4 +171,4 @@ class _CoverImageProviderElement String get itemId => (origin as CoverImageProvider).itemId; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/api/library_item_provider.g.dart b/lib/api/library_item_provider.g.dart index af297d0..5261d9c 100644 --- a/lib/api/library_item_provider.g.dart +++ b/lib/api/library_item_provider.g.dart @@ -170,6 +170,8 @@ class LibraryItemProvider extends StreamNotifierProviderImpl { /// The parameter `id` of this provider. @@ -184,4 +186,4 @@ class _LibraryItemProviderElement extends StreamNotifierProviderElement< String get id => (origin as LibraryItemProvider).id; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/api/library_provider.dart b/lib/api/library_provider.dart new file mode 100644 index 0000000..62c79d8 --- /dev/null +++ b/lib/api/library_provider.dart @@ -0,0 +1,58 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart' show Ref; +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..a8bc88a --- /dev/null +++ b/lib/api/library_provider.g.dart @@ -0,0 +1,192 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'library_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$libraryHash() => r'f8a34100acb58f02fa958c71a629577bf815710e'; + +/// 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 [library]. +@ProviderFor(library) +const libraryProvider = LibraryFamily(); + +/// See also [library]. +class LibraryFamily extends Family> { + /// See also [library]. + const LibraryFamily(); + + /// See also [library]. + LibraryProvider call( + String id, + ) { + return LibraryProvider( + id, + ); + } + + @override + LibraryProvider getProviderOverride( + covariant LibraryProvider 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'libraryProvider'; +} + +/// See also [library]. +class LibraryProvider extends AutoDisposeFutureProvider { + /// See also [library]. + LibraryProvider( + String id, + ) : this._internal( + (ref) => library( + ref as LibraryRef, + id, + ), + from: libraryProvider, + name: r'libraryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$libraryHash, + dependencies: LibraryFamily._dependencies, + allTransitiveDependencies: LibraryFamily._allTransitiveDependencies, + id: id, + ); + + LibraryProvider._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 + Override overrideWith( + FutureOr Function(LibraryRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: LibraryProvider._internal( + (ref) => create(ref as LibraryRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + id: id, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _LibraryProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is LibraryProvider && other.id == id; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, id.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin LibraryRef on AutoDisposeFutureProviderRef { + /// The parameter `id` of this provider. + String get id; +} + +class _LibraryProviderElement extends AutoDisposeFutureProviderElement + with LibraryRef { + _LibraryProviderElement(super.provider); + + @override + String get id => (origin as LibraryProvider).id; +} + +String _$currentLibraryHash() => r'658498a531e04a01e2b3915a3319101285601118'; + +/// See also [currentLibrary]. +@ProviderFor(currentLibrary) +final currentLibraryProvider = AutoDisposeFutureProvider.internal( + currentLibrary, + name: r'currentLibraryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$currentLibraryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef CurrentLibraryRef = AutoDisposeFutureProviderRef; +String _$librariesHash() => r'95ebd4d1ac0cc2acf7617dc22895eff0ca30600f'; + +/// See also [Libraries]. +@ProviderFor(Libraries) +final librariesProvider = + AutoDisposeAsyncNotifierProvider>.internal( + Libraries.new, + name: r'librariesProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$librariesHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$Libraries = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/api/server_provider.dart b/lib/api/server_provider.dart index f4d8a21..ef1c864 100644 --- a/lib/api/server_provider.dart +++ b/lib/api/server_provider.dart @@ -1,7 +1,6 @@ -import 'package:collection/collection.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; @@ -50,7 +49,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer { if (_box.isNotEmpty) { final foundServers = _box.getRange(0, _box.length); _logger.info('found servers in box: ${foundServers.obfuscate()}'); - return foundServers.whereNotNull().toSet(); + return foundServers.nonNulls.toSet(); } else { _logger.info('no settings found in box'); return {}; @@ -89,7 +88,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer { } // 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 7ff40c2..ff2406a 100644 --- a/lib/api/server_provider.g.dart +++ b/lib/api/server_provider.g.dart @@ -7,7 +7,7 @@ part of 'server_provider.dart'; // ************************************************************************** String _$audiobookShelfServerHash() => - r'0084fb72c4c54323207928b95716cfd9ca496c11'; + r'31a96b431221965cd586aad670a32ca901539e41'; /// provides with a set of servers added by the user /// @@ -27,4 +27,4 @@ final audiobookShelfServerProvider = AutoDisposeNotifierProvider< 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/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/features/downloads/providers/download_manager.dart b/lib/features/downloads/providers/download_manager.dart index 9f56129..6ffdded 100644 --- a/lib/features/downloads/providers/download_manager.dart +++ b/lib/features/downloads/providers/download_manager.dart @@ -1,4 +1,5 @@ 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'; @@ -122,7 +123,7 @@ class ItemDownloadProgress extends _$ItemDownloadProgress { @riverpod FutureOr> downloadHistory( - DownloadHistoryRef ref, { + Ref ref, { String? group, }) async { return await FileDownloader().database.allRecords(group: group); diff --git a/lib/features/downloads/providers/download_manager.g.dart b/lib/features/downloads/providers/download_manager.g.dart index 47cbf4d..6b08886 100644 --- a/lib/features/downloads/providers/download_manager.g.dart +++ b/lib/features/downloads/providers/download_manager.g.dart @@ -6,7 +6,7 @@ part of 'download_manager.dart'; // RiverpodGenerator // ************************************************************************** -String _$downloadHistoryHash() => r'76c449e8abfa61d57566991686f534a06dc7fef7'; +String _$downloadHistoryHash() => r'4d8b84e30f7ff5ae69d23c8e03ff24af1234a1ad'; /// Copied from Dart SDK class _SystemHash { @@ -143,6 +143,8 @@ class DownloadHistoryProvider } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin DownloadHistoryRef on AutoDisposeFutureProviderRef> { /// The parameter `group` of this provider. String? get group; @@ -318,6 +320,8 @@ class IsItemDownloadingProvider } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin IsItemDownloadingRef on AutoDisposeNotifierProviderRef { /// The parameter `id` of this provider. String get id; @@ -463,6 +467,8 @@ class ItemDownloadProgressProvider extends AutoDisposeAsyncNotifierProviderImpl< } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin ItemDownloadProgressRef on AutoDisposeAsyncNotifierProviderRef { /// The parameter `id` of this provider. String get id; @@ -607,6 +613,8 @@ class IsItemDownloadedProvider } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin IsItemDownloadedRef on AutoDisposeAsyncNotifierProviderRef { /// The parameter `item` of this provider. LibraryItemExpanded get item; @@ -621,4 +629,4 @@ class _IsItemDownloadedProviderElement LibraryItemExpanded get item => (origin as IsItemDownloadedProvider).item; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/downloads/view/downloads_page.dart b/lib/features/downloads/view/downloads_page.dart index e8c0e9c..7c5dbfb 100644 --- a/lib/features/downloads/view/downloads_page.dart +++ b/lib/features/downloads/view/downloads_page.dart @@ -13,7 +13,6 @@ class DownloadsPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( title: const Text('Downloads'), - backgroundColor: Colors.transparent, ), body: Center( // history of downloads diff --git a/lib/features/explore/providers/search_controller.g.dart b/lib/features/explore/providers/search_controller.g.dart index 4e6fa28..fab5671 100644 --- a/lib/features/explore/providers/search_controller.g.dart +++ b/lib/features/explore/providers/search_controller.g.dart @@ -26,4 +26,4 @@ final globalSearchControllerProvider = 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/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..764b781 100644 --- a/lib/features/explore/providers/search_result_provider.g.dart +++ b/lib/features/explore/providers/search_result_provider.g.dart @@ -6,7 +6,7 @@ part of 'search_result_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$searchResultHash() => r'9baa643cce24f3a5e022f42202e423373939ef95'; +String _$searchResultHash() => r'33785de298ad0d53c9d21e8fec88ba2f22f1363f'; /// Copied from Dart SDK class _SystemHash { @@ -167,6 +167,8 @@ class SearchResultProvider } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin SearchResultRef on AutoDisposeFutureProviderRef { /// The parameter `query` of this provider. String get query; @@ -186,4 +188,4 @@ class _SearchResultProviderElement int get limit => (origin as SearchResultProvider).limit; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/explore/view/explore_page.dart b/lib/features/explore/view/explore_page.dart index 2a46983..1682254 100644 --- a/lib/features/explore/view/explore_page.dart +++ b/lib/features/explore/view/explore_page.dart @@ -30,7 +30,6 @@ class ExplorePage extends HookConsumerWidget { return Scaffold( appBar: AppBar( title: const Text('Explore'), - backgroundColor: Colors.transparent, ), body: const MySearchBar(), ); @@ -98,8 +97,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, diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index 1b1b5a8..fe5525f 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -425,7 +425,6 @@ class DownloadSheet extends HookConsumerWidget { class _LibraryItemPlayButton extends HookConsumerWidget { const _LibraryItemPlayButton({ - super.key, required this.item, }); 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 83b5555..da4adea 100644 --- a/lib/features/item_viewer/view/library_item_hero_section.dart +++ b/lib/features/item_viewer/view/library_item_hero_section.dart @@ -15,7 +15,7 @@ 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/theme_from_cover_provider.dart'; +import 'package:vaani/theme/providers/theme_from_cover_provider.dart'; class LibraryItemHeroSection extends HookConsumerWidget { const LibraryItemHeroSection({ @@ -78,7 +78,6 @@ class LibraryItemHeroSection extends HookConsumerWidget { class _BookDetails extends HookConsumerWidget { const _BookDetails({ - super.key, required this.id, this.extraMap, }); @@ -136,7 +135,6 @@ class _BookDetails extends HookConsumerWidget { class _LibraryItemProgressIndicator extends HookConsumerWidget { const _LibraryItemProgressIndicator({ - super.key, required this.id, }); @@ -201,8 +199,10 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget { '${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), ), ), ], @@ -213,7 +213,6 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget { class _HeroSectionSubLabelWithIcon extends HookConsumerWidget { const _HeroSectionSubLabelWithIcon({ - super.key, required this.icon, required this.text, }); @@ -230,7 +229,7 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget { 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( @@ -260,7 +259,6 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget { class _BookSeries extends StatelessWidget { const _BookSeries({ - super.key, required this.itemBookMetadata, required this.bookDetailsCached, }); @@ -306,7 +304,6 @@ class _BookSeries extends StatelessWidget { class _BookNarrators extends StatelessWidget { const _BookNarrators({ - super.key, required this.itemBookMetadata, required this.bookDetailsCached, }); @@ -342,7 +339,6 @@ class _BookNarrators extends StatelessWidget { class _BookCover extends HookConsumerWidget { const _BookCover({ - super.key, required this.itemId, }); @@ -353,16 +349,17 @@ class _BookCover extends HookConsumerWidget { final coverImage = ref.watch(coverImageProvider(itemId)); final themeData = Theme.of(context); // final item = ref.watch(libraryItemProvider(itemId)); - final useMaterialThemeOnItemPage = - ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage; + final themeSettings = ref.watch(appSettingsProvider).themeSettings; ColorScheme? coverColorScheme; - if (useMaterialThemeOnItemPage) { + if (themeSettings.useMaterialThemeOnItemPage) { coverColorScheme = ref .watch( themeOfLibraryItemProvider( itemId, brightness: Theme.of(context).brightness, + highContrast: themeSettings.highContrast || + MediaQuery.of(context).highContrast, ), ) .valueOrNull; @@ -371,7 +368,7 @@ class _BookCover extends HookConsumerWidget { return ThemeSwitcher( builder: (context) { // change theme after 2 seconds - if (useMaterialThemeOnItemPage) { + if (themeSettings.useMaterialThemeOnItemPage) { Future.delayed(150.ms, () { try { ThemeSwitcher.of(context).changeTheme( @@ -415,7 +412,6 @@ class _BookCover extends HookConsumerWidget { class _BookTitle extends StatelessWidget { const _BookTitle({ - super.key, required this.extraMap, required this.itemBookMetadata, }); @@ -449,7 +445,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 ?? '', ), @@ -460,7 +456,6 @@ class _BookTitle extends StatelessWidget { class _BookAuthors extends StatelessWidget { const _BookAuthors({ - super.key, required this.itemBookMetadata, required this.bookDetailsCached, }); diff --git a/lib/features/item_viewer/view/library_item_metadata.dart b/lib/features/item_viewer/view/library_item_metadata.dart index 5874a92..664daac 100644 --- a/lib/features/item_viewer/view/library_item_metadata.dart +++ b/lib/features/item_viewer/view/library_item_metadata.dart @@ -96,7 +96,10 @@ class LibraryItemMetadata extends HookConsumerWidget { return VerticalDivider( indent: 6, endIndent: 6, - color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), + color: Theme.of(context) + .colorScheme + .onSurface + .withValues(alpha: 0.6), ); }, ), @@ -109,7 +112,6 @@ class LibraryItemMetadata extends HookConsumerWidget { /// key-value pair to display as column class _MetadataItem extends StatelessWidget { const _MetadataItem({ - super.key, required this.title, required this.value, }); @@ -126,7 +128,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, @@ -134,7 +136,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 9e98003..e7b9310 100644 --- a/lib/features/item_viewer/view/library_item_page.dart +++ b/lib/features/item_viewer/view/library_item_page.dart @@ -3,10 +3,11 @@ 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/shared/widgets/expandable_description.dart'; @@ -23,19 +24,89 @@ class LibraryItemPage extends HookConsumerWidget { final String itemId; final Object? extra; + static const double _showFabThreshold = 300.0; @override Widget build(BuildContext context, WidgetRef ref) { final additionalItemData = extra is LibraryItemExtras ? extra as LibraryItemExtras : null; + final scrollController = useScrollController(); + final showFab = useState(false); + + // 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; + } + } + + scrollController.addListener(listener); + // Initial check in case the view starts scrolled (less likely but safe) + WidgetsBinding.instance.addPostFrameCallback((_) { + if (scrollController.hasClients && context.mounted) { + listener(); + } + }); + + // 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( @@ -56,9 +127,7 @@ class LibraryItemPage extends HookConsumerWidget { 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()), ], ), ), 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..fd6b621 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,80 @@ 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)).valueOrNull; + + 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..4327b17 100644 --- a/lib/features/library_browser/view/library_browser_page.dart +++ b/lib/features/library_browser/view/library_browser_page.dart @@ -1,47 +1,83 @@ 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).valueOrNull; + + // 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/providers/logs_provider.dart b/lib/features/logging/providers/logs_provider.dart index ecdc0b6..9fd9e4a 100644 --- a/lib/features/logging/providers/logs_provider.dart +++ b/lib/features/logging/providers/logs_provider.dart @@ -1,11 +1,11 @@ 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 @@ -29,11 +29,23 @@ class Logs extends _$Logs { } Future getZipFilePath() async { + final String targetZipPath = await generateZipFilePath(); var encoder = ZipFileEncoder(); - encoder.create(await generateZipFilePath()); - encoder.addFile(File(await getLoggingFilePath())); - encoder.close(); - return encoder.zipPath; + 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; } } @@ -43,7 +55,7 @@ Future generateZipFilePath() async { } String generateZipFileName() { - return 'vaani-${DateTime.now().toIso8601String()}.zip'; + return 'vaani-${DateTime.now().microsecondsSinceEpoch}.zip'; } Level parseLevel(String level) { diff --git a/lib/features/logging/providers/logs_provider.g.dart b/lib/features/logging/providers/logs_provider.g.dart index 094893e..53babc0 100644 --- a/lib/features/logging/providers/logs_provider.g.dart +++ b/lib/features/logging/providers/logs_provider.g.dart @@ -6,7 +6,7 @@ part of 'logs_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$logsHash() => r'901376741d17ddbb889d1b7b96bc2882289720a0'; +String _$logsHash() => r'aa9d3d56586cba6ddf69615320ea605d071ea5e2'; /// See also [Logs]. @ProviderFor(Logs) @@ -22,4 +22,4 @@ final logsProvider = typedef _$Logs = AutoDisposeAsyncNotifier>; // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/logging/view/logs_page.dart b/lib/features/logging/view/logs_page.dart index f5ee104..74d1ad3 100644 --- a/lib/features/logging/view/logs_page.dart +++ b/lib/features/logging/view/logs_page.dart @@ -1,6 +1,3 @@ -import 'dart:io'; - -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -76,32 +73,77 @@ class LogsPage extends HookConsumerWidget { } }, ), - IconButton( - tooltip: 'Download logs', - icon: const Icon(Icons.download), - onPressed: () async { - appLogger.info('Preparing logs for download'); - 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, - ); - if (outputFile != null) { - try { - final file = File(outputFile); - final zipFile = File(zipLogFilePath); - await zipFile.copy(file.path); - } catch (e) { - appLogger.severe('Error saving file: $e'); - } - } else { - appLogger.info('Download cancelled'); - } - }, - ), + // 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), @@ -122,7 +164,7 @@ class LogsPage extends HookConsumerWidget { ), ], ), - // a column with listview.builder and a scrollable list of logs + // a column with ListView.builder and a scrollable list of logs body: Column( children: [ // a filter for log levels, loggers, and search diff --git a/lib/features/onboarding/providers/oauth_provider.dart b/lib/features/onboarding/providers/oauth_provider.dart index 79445d9..fb7856b 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'; @@ -61,7 +62,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..8f44538 100644 --- a/lib/features/onboarding/providers/oauth_provider.g.dart +++ b/lib/features/onboarding/providers/oauth_provider.g.dart @@ -7,7 +7,7 @@ part of 'oauth_provider.dart'; // ************************************************************************** String _$loginInExchangeForCodeHash() => - r'e931254959d9eb8196439c6b0c884c26cbe17c2f'; + r'bfc3945529048a0f536052fd5579b76457560fcd'; /// Copied from Dart SDK class _SystemHash { @@ -179,6 +179,8 @@ class LoginInExchangeForCodeProvider } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin LoginInExchangeForCodeRef on AutoDisposeFutureProviderRef { /// The parameter `oauthState` of this provider. String get oauthState; @@ -221,4 +223,4 @@ final oauthFlowsProvider = 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/onboarding/view/callback_page.dart b/lib/features/onboarding/view/callback_page.dart index dd3de86..f4bb098 100644 --- a/lib/features/onboarding/view/callback_page.dart +++ b/lib/features/onboarding/view/callback_page.dart @@ -98,7 +98,6 @@ class BackToLoginButton extends StatelessWidget { class _SomethingWentWrong extends StatelessWidget { const _SomethingWentWrong({ - super.key, this.message = 'Error with OAuth flow', }); diff --git a/lib/features/onboarding/view/onboarding_single_page.dart b/lib/features/onboarding/view/onboarding_single_page.dart index 5ceff08..b9a7eb5 100644 --- a/lib/features/onboarding/view/onboarding_single_page.dart +++ b/lib/features/onboarding/view/onboarding_single_page.dart @@ -13,89 +13,122 @@ class OnboardingSinglePage extends HookConsumerWidget { super.key, }); + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + 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()), + ), + ), + ), + ); + }, + ), + ); + } +} + +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() ?? '', + text: apiSettings.activeServer?.serverUrl.toString() ?? 'https://', ); 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 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, + ), ), - ); - } - - 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, - ), - ), - 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( + const SizedBox.square( + dimension: 16.0, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: AnimatedSwitcher( duration: 500.ms, transitionBuilder: fadeSlideTransitionBuilder, child: canUserLogin.value - ? UserLoginWidget( - server: audiobookshelfUri, + ? Text( + 'Server connected, please login', + key: const ValueKey('connected'), + style: Theme.of(context).textTheme.bodyMedium, ) - // ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0) - : const RedirectToABS().animate().fadeIn().slideY( - curve: Curves.easeInOut, - duration: 500.ms, - ), + : 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, + ), + ), + ], ); } } diff --git a/lib/features/onboarding/view/user_login.dart b/lib/features/onboarding/view/user_login.dart index 2b3d575..8aeff14 100644 --- a/lib/features/onboarding/view/user_login.dart +++ b/lib/features/onboarding/view/user_login.dart @@ -1,33 +1,42 @@ 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({ + 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 serverStatusError = useMemoized(() => ErrorResponseHandler(), []); final serverStatus = ref.watch(serverStatusProvider(server, serverStatusError.storeError)); - final api = ref.watch(audiobookshelfApiProvider(server)); - return serverStatus.when( data: (value) { if (value == null) { @@ -42,6 +51,7 @@ class UserLoginWidget extends HookConsumerWidget { openIDAvailable: value.authMethods?.contains(AuthMethod.openid) ?? false, openIDButtonText: value.authFormData?.authOpenIDButtonText, + onSuccess: onSuccess, ); }, loading: () { @@ -88,6 +98,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget { this.openIDAvailable = false, this.onPressed, this.openIDButtonText, + this.onSuccess, }); final Uri server; @@ -95,6 +106,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,8 +116,6 @@ class UserLoginMultipleAuth extends HookConsumerWidget { localAvailable ? AuthMethodChoice.local : AuthMethodChoice.authToken, ); - final apiSettings = ref.watch(apiSettingsProvider); - model.AudiobookShelfServer addServer() { var newServer = model.AudiobookShelfServer( serverUrl: server, @@ -119,9 +129,9 @@ class UserLoginMultipleAuth extends HookConsumerWidget { newServer = e.server; } finally { ref.read(apiSettingsProvider.notifier).updateState( - apiSettings.copyWith( - activeServer: newServer, - ), + ref.read(apiSettingsProvider).copyWith( + activeServer: newServer, + ), ); } return newServer; @@ -130,11 +140,11 @@ 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, @@ -172,28 +182,38 @@ class UserLoginMultipleAuth extends HookConsumerWidget { } }, ), - ], + ].animate(interval: 100.ms).fadeIn( + duration: 150.ms, + curve: Curves.easeIn, + ), ), - const SizedBox.square( - dimension: 8, + ), + 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( + server: server, + addServer: addServer, + onSuccess: onSuccess, + ), + AuthMethodChoice.openid => UserLoginWithOpenID( + server: server, + addServer: addServer, + openIDButtonText: openIDButtonText, + onSuccess: onSuccess, + ), + }, ), - switch (methodChoice.value) { - AuthMethodChoice.authToken => UserLoginWithToken( - server: server, - addServer: addServer, - ), - AuthMethodChoice.local => UserLoginWithPassword( - server: server, - addServer: addServer, - ), - AuthMethodChoice.openid => UserLoginWithOpenID( - server: server, - addServer: addServer, - openIDButtonText: openIDButtonText, - ), - }, - ], - ), + ), + ], ), ), ), 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 71baa42..b3a1d9e 100644 --- a/lib/features/onboarding/view/user_login_with_open_id.dart +++ b/lib/features/onboarding/view/user_login_with_open_id.dart @@ -20,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) { diff --git a/lib/features/onboarding/view/user_login_with_password.dart b/lib/features/onboarding/view/user_login_with_password.dart index 91eeea0..210da77 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), ); @@ -76,92 +80,94 @@ 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) + ), + 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 - .onSurface - .withOpacity(0.8), + .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'), + ), + ], ), ), ), @@ -207,8 +213,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..7d2fcfb 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,10 @@ 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(), ), 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..221433f 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 @@ -157,6 +157,8 @@ class BookSettingsProvider } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin BookSettingsRef on AutoDisposeNotifierProviderRef { /// The parameter `bookId` of this provider. String get bookId; @@ -171,4 +173,4 @@ class _BookSettingsProviderElement String get bookId => (origin as BookSettingsProvider).bookId; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart index 8b8936d..3c21f16 100644 --- a/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart +++ b/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart @@ -23,4 +23,4 @@ final 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/player/playlist_provider.g.dart b/lib/features/player/playlist_provider.g.dart index 061289c..abf7c33 100644 --- a/lib/features/player/playlist_provider.g.dart +++ b/lib/features/player/playlist_provider.g.dart @@ -22,4 +22,4 @@ final playlistProvider = 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/player/providers/audiobook_player.g.dart b/lib/features/player/providers/audiobook_player.g.dart index a1068eb..d38dd30 100644 --- a/lib/features/player/providers/audiobook_player.g.dart +++ b/lib/features/player/providers/audiobook_player.g.dart @@ -43,4 +43,4 @@ final 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/player/providers/currently_playing_provider.dart b/lib/features/player/providers/currently_playing_provider.dart index 3ceff3e..e8b8af9 100644 --- a/lib/features/player/providers/currently_playing_provider.dart +++ b/lib/features/player/providers/currently_playing_provider.dart @@ -1,3 +1,4 @@ +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'; @@ -9,7 +10,7 @@ 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; @@ -21,7 +22,7 @@ BookExpanded? currentlyPlayingBook(CurrentlyPlayingBookRef ref) { /// 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(); @@ -32,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 6dc1c2a..428aa0b 100644 --- a/lib/features/player/providers/currently_playing_provider.g.dart +++ b/lib/features/player/providers/currently_playing_provider.g.dart @@ -7,7 +7,7 @@ part of 'currently_playing_provider.dart'; // ************************************************************************** String _$currentlyPlayingBookHash() => - r'7440b0d54cb364f66e704783652e8f1490ae90e0'; + r'e4258694c8f0d1e89651b330fae0f672ca13a484'; /// See also [currentlyPlayingBook]. @ProviderFor(currentlyPlayingBook) @@ -22,9 +22,11 @@ final currentlyPlayingBookProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef CurrentlyPlayingBookRef = AutoDisposeProviderRef; String _$currentPlayingChapterHash() => - r'a084da724e3d8bb1b1475e867ab3200d7d61d827'; + r'73db8b8a9058573bb0c68ec5d5f8aba9306f3d24'; /// provided the current chapter of the book being played /// @@ -41,9 +43,11 @@ final currentPlayingChapterProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef CurrentPlayingChapterRef = AutoDisposeProviderRef; String _$currentBookMetadataHash() => - r'9088debba151894b61f2dcba1bba12a89244b9b1'; + r'f537ef4ef19280bc952de658ecf6520c535ae344'; /// provides the book metadata of the currently playing book /// @@ -60,6 +64,8 @@ final currentBookMetadataProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/player/providers/player_form.dart b/lib/features/player/providers/player_form.dart index 975f47f..10376ee 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'; @@ -26,7 +27,7 @@ extension on Ref { @Riverpod(keepAlive: true) Raw> playerExpandProgressNotifier( - PlayerExpandProgressNotifierRef ref, + Ref ref, ) { final ValueNotifier playerExpandProgress = ValueNotifier(playerMinHeight); @@ -46,7 +47,7 @@ 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, + Ref ref, ) { final playerExpandProgress = ref.watch(playerExpandProgressNotifierProvider); @@ -60,3 +61,20 @@ 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..6dcfcf9 100644 --- a/lib/features/player/providers/player_form.g.dart +++ b/lib/features/player/providers/player_form.g.dart @@ -7,7 +7,7 @@ part of 'player_form.dart'; // ************************************************************************** String _$playerExpandProgressNotifierHash() => - r'e4817361b9a311b61ca23e51082ed11b0a1120ab'; + r'1ac7172d90a070f96222286edd1a176be197f378'; /// See also [playerExpandProgressNotifier]. @ProviderFor(playerExpandProgressNotifier) @@ -22,9 +22,11 @@ final playerExpandProgressNotifierProvider = allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef PlayerExpandProgressNotifierRef = ProviderRef>>; -String _$playerHeightHash() => r'26dbcb180d494575488d700bd5bdb58c02c224a9'; +String _$playerHeightHash() => r'3f031eaffdffbb2c6ddf7eb1aba31bf1619260fc'; /// See also [playerHeight]. @ProviderFor(playerHeight) @@ -37,6 +39,25 @@ final playerHeightProvider = Provider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef PlayerHeightRef = ProviderRef; +String _$isPlayerActiveHash() => r'2c7ca125423126fb5f0ef218d37bc8fe0ca9ec98'; + +/// See also [isPlayerActive]. +@ProviderFor(isPlayerActive) +final isPlayerActiveProvider = Provider.internal( + isPlayerActive, + name: r'isPlayerActiveProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$isPlayerActiveHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef IsPlayerActiveRef = 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/player/view/audiobook_player.dart b/lib/features/player/view/audiobook_player.dart index 46ed7aa..1940ba7 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'; @@ -65,6 +65,8 @@ class AudiobookPlayer extends HookConsumerWidget { themeOfLibraryItemProvider( itemBeingPlayed.valueOrNull?.id, brightness: Theme.of(context).brightness, + highContrast: appSettings.themeSettings.highContrast || + MediaQuery.of(context).highContrast, ), ); 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 5fe0ea7..0deda52 100644 --- a/lib/features/player/view/player_when_expanded.dart +++ b/lib/features/player/view/player_when_expanded.dart @@ -104,8 +104,10 @@ 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), @@ -171,7 +173,7 @@ class PlayerWhenExpanded extends HookConsumerWidget { color: Theme.of(context) .colorScheme .onSurface - .withOpacity(0.7), + .withValues(alpha: 0.7), ), maxLines: 1, overflow: TextOverflow.ellipsis, diff --git a/lib/features/player/view/player_when_minimized.dart b/lib/features/player/view/player_when_minimized.dart index 435a6de..1a5774b 100644 --- a/lib/features/player/view/player_when_minimized.dart +++ b/lib/features/player/view/player_when_minimized.dart @@ -93,7 +93,7 @@ class PlayerWhenMinimized extends HookConsumerWidget { color: Theme.of(context) .colorScheme .onSurface - .withOpacity(0.7), + .withValues(alpha: 0.7), ), ), ], diff --git a/lib/features/player/view/widgets/chapter_selection_button.dart b/lib/features/player/view/widgets/chapter_selection_button.dart index 889392a..04cbd0e 100644 --- a/lib/features/player/view/widgets/chapter_selection_button.dart +++ b/lib/features/player/view/widgets/chapter_selection_button.dart @@ -1,13 +1,18 @@ 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/main.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({ @@ -67,6 +72,7 @@ class ChapterSelectionModal extends HookConsumerWidget { useTimer(scrollToCurrentChapter, 500.ms); // useInterval(scrollToCurrentChapter, 500.ms); + final theme = Theme.of(context); return Column( children: [ ListTile( @@ -81,24 +87,41 @@ 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, + ), + 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/playing_indicator_icon.dart b/lib/features/player/view/widgets/playing_indicator_icon.dart new file mode 100644 index 0000000..d179797 --- /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 65a3bfa..ab564b1 100644 --- a/lib/features/player/view/widgets/speed_selector.dart +++ b/lib/features/player/view/widgets/speed_selector.dart @@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; 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'; -import 'package:vaani/shared/hooks.dart'; const double itemExtent = 25; diff --git a/lib/features/shake_detection/providers/shake_detector.g.dart b/lib/features/shake_detection/providers/shake_detector.g.dart index 0f4285f..ed81aaf 100644 --- a/lib/features/shake_detection/providers/shake_detector.g.dart +++ b/lib/features/shake_detection/providers/shake_detector.g.dart @@ -23,4 +23,4 @@ final shakeDetectorProvider = typedef _$ShakeDetector = 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart index 7daade0..0b1db3f 100644 --- a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart +++ b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart @@ -22,4 +22,4 @@ final 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/features/sleep_timer/view/sleep_timer_button.dart b/lib/features/sleep_timer/view/sleep_timer_button.dart index 075b0e4..9712813 100644 --- a/lib/features/sleep_timer/view/sleep_timer_button.dart +++ b/lib/features/sleep_timer/view/sleep_timer_button.dart @@ -11,7 +11,6 @@ import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' import 'package:vaani/main.dart'; import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/shared/extensions/duration_format.dart'; -import 'package:vaani/shared/hooks.dart'; class SleepTimerButton extends HookConsumerWidget { const SleepTimerButton({ diff --git a/lib/features/you/view/server_manager.dart b/lib/features/you/view/server_manager.dart index 8a385f2..75e3f43 100644 --- a/lib/features/you/view/server_manager.dart +++ b/lib/features/you/view/server_manager.dart @@ -2,16 +2,22 @@ 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/main.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/extensions/obfuscation.dart'; -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({ @@ -20,15 +26,6 @@ class ServerManagerPage extends HookConsumerWidget { @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(); - - appLogger.fine('registered servers: ${registeredServers.obfuscate()}'); - appLogger.fine('available users: ${availableUsers.obfuscate()}'); return Scaffold( appBar: AppBar( title: const Text('Manage Accounts'), @@ -36,240 +33,118 @@ class ServerManagerPage extends HookConsumerWidget { 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({ +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, }); @@ -278,178 +153,220 @@ class _AddUserDialog extends HookConsumerWidget { @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..a673332 --- /dev/null +++ b/lib/features/you/view/widgets/library_switch_chip.dart @@ -0,0 +1,225 @@ +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 2a7bee2..ca789db 100644 --- a/lib/features/you/view/you_page.dart +++ b/lib/features/you/view/you_page.dart @@ -2,10 +2,14 @@ import 'package:flutter/material.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/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({ @@ -15,10 +19,10 @@ class YouPage extends HookConsumerWidget { @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', @@ -62,7 +66,35 @@ 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: () { @@ -105,7 +137,6 @@ class YouPage extends HookConsumerWidget { showNotImplementedToast(context); }, ), - AboutListTile( icon: const Icon(Icons.info), applicationName: AppMetadata.appName, @@ -128,13 +159,16 @@ class YouPage extends HookConsumerWidget { Theme.of(context).colorScheme.primary, BlendMode.srcIn, ), - child: const VaaniLogo(), + child: const VaaniLogo( + size: 48, + ), ), ), ], ), ), ), + SliverToBoxAdapter(child: MiniPlayerBottomPadding()), ], ), ); @@ -149,6 +183,10 @@ class UserBar extends HookConsumerWidget { @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) { @@ -160,19 +198,30 @@ class UserBar extends HookConsumerWidget { // first letter of the username child: Text( userData.username[0].toUpperCase(), - style: const TextStyle( - fontSize: 32, + style: textTheme.headlineLarge?.copyWith( fontWeight: FontWeight.bold, ), ), ), const SizedBox(width: 16), - Text( - userData.username, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), + 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), + ), + ), + ], ), ], ); @@ -182,29 +231,3 @@ class UserBar extends HookConsumerWidget { ); } } - -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/main.dart b/lib/main.dart index 7f54ec1..6020a6a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:logging/logging.dart'; @@ -7,12 +8,15 @@ 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/core/init.dart'; -import 'package:vaani/features/player/providers/audiobook_player.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/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'); @@ -51,19 +55,88 @@ 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.valueOrNull != null) { + lightColorScheme = themes.valueOrNull!.$1; + darkColorScheme = themes.valueOrNull!.$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.valueOrNull != null && themeDark.valueOrNull != null) { + lightColorScheme = themeLight.valueOrNull!; + darkColorScheme = themeDark.valueOrNull!; + } + } + } 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); } @@ -90,6 +163,7 @@ class _EagerInitialization extends ConsumerWidget { 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 954f3f3..13352be 100644 --- a/lib/models/error_response.dart +++ b/lib/models/error_response.dart @@ -7,14 +7,18 @@ 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.obfuscate()}'); + 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 4ff81c4..3ba1d98 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -6,6 +6,8 @@ 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'; @@ -17,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', @@ -64,9 +67,21 @@ class HomePage extends HookConsumerWidget { // .where((element) => !element.id.contains('discover')) .map((shelf) { 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( @@ -76,7 +91,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, ), diff --git a/lib/pages/library_page.dart b/lib/pages/library_page.dart index 18eedee..a6950a6 100644 --- a/lib/pages/library_page.dart +++ b/lib/pages/library_page.dart @@ -61,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, ), diff --git a/lib/router/constants.dart b/lib/router/constants.dart index 9d01c29..79c4556 100644 --- a/lib/router/constants.dart +++ b/lib/router/constants.dart @@ -27,6 +27,11 @@ class Routes { pathName: 'config', name: 'settings', ); + static const themeSettings = _SimpleRoute( + pathName: 'theme', + name: 'themeSettings', + parentRoute: settings, + ); static const autoSleepTimerSettings = _SimpleRoute( pathName: 'autoSleepTimer', name: 'autoSleepTimerSettings', @@ -47,6 +52,11 @@ class Routes { name: 'shakeDetectorSettings', parentRoute: settings, ); + static const homePageSettings = _SimpleRoute( + pathName: 'homePage', + name: 'homePageSettings', + parentRoute: settings, + ); // search and explore static const search = _SimpleRoute( diff --git a/lib/router/models/library_item_extras.dart b/lib/router/models/library_item_extras.dart index 0d50040..55b734e 100644 --- a/lib/router/models/library_item_extras.dart +++ b/lib/router/models/library_item_extras.dart @@ -16,5 +16,4 @@ class LibraryItemExtras with _$LibraryItemExtras { BookMinified? book, @Default('') String heroTagSuffix, }) = _LibraryItemExtras; - } diff --git a/lib/router/router.dart b/lib/router/router.dart index c5b1a67..eda348e 100644 --- a/lib/router/router.dart +++ b/lib/router/router.dart @@ -17,6 +17,8 @@ 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'; @@ -178,6 +180,13 @@ class MyAppRouter { // builder: (context, state) => const AppSettingsPage(), pageBuilder: defaultPageBuilder(const AppSettingsPage()), routes: [ + GoRoute( + path: Routes.themeSettings.pathName, + name: Routes.themeSettings.name, + pageBuilder: defaultPageBuilder( + const ThemeSettingsPage(), + ), + ), GoRoute( path: Routes.autoSleepTimerSettings.pathName, name: Routes.autoSleepTimerSettings.name, @@ -205,6 +214,13 @@ class MyAppRouter { const ShakeDetectorSettingsPage(), ), ), + GoRoute( + path: Routes.homePageSettings.pathName, + name: Routes.homePageSettings.name, + pageBuilder: defaultPageBuilder( + const HomePageSettingsPage(), + ), + ), ], ), GoRoute( diff --git a/lib/router/scaffold_with_nav_bar.dart b/lib/router/scaffold_with_nav_bar.dart index 0a89162..27c3355 100644 --- a/lib/router/scaffold_with_nav_bar.dart +++ b/lib/router/scaffold_with_nav_bar.dart @@ -2,12 +2,15 @@ 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 @@ -111,17 +114,39 @@ class ScaffoldWithNavBar extends HookConsumerWidget { // 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) => NavigationDestination( - icon: Icon(item.icon), - selectedIcon: item.activeIcon != null - ? Icon(item.activeIcon) - : Icon(item.icon), - label: item.name, - ), - ) - .toList(), + destinations: _navigationItems.map((item) { + final isDestinationLibrary = item.name == 'Library'; + var currentLibrary = + ref.watch(currentLibraryProvider).valueOrNull; + 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), ), @@ -191,16 +216,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 +236,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/settings/api_settings_provider.g.dart b/lib/settings/api_settings_provider.g.dart index d9a222c..eff1d41 100644 --- a/lib/settings/api_settings_provider.g.dart +++ b/lib/settings/api_settings_provider.g.dart @@ -22,4 +22,4 @@ final 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/settings/app_settings_provider.dart b/lib/settings/app_settings_provider.dart index 6d61774..b86e736 100644 --- a/lib/settings/app_settings_provider.dart +++ b/lib/settings/app_settings_provider.dart @@ -47,11 +47,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; } diff --git a/lib/settings/app_settings_provider.g.dart b/lib/settings/app_settings_provider.g.dart index df95738..8f3d8f5 100644 --- a/lib/settings/app_settings_provider.g.dart +++ b/lib/settings/app_settings_provider.g.dart @@ -6,7 +6,7 @@ part of 'app_settings_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$appSettingsHash() => r'f51d55f117692d4fb9f4b4febf02906c0953d334'; +String _$appSettingsHash() => r'314d7936f54550f57d308056a99230402342a6d0'; /// See also [AppSettings]. @ProviderFor(AppSettings) @@ -39,4 +39,4 @@ final sleepTimerSettingsProvider = typedef _$SleepTimerSettings = 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/settings/constants.dart b/lib/settings/constants.dart index 9ab7b55..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._(); diff --git a/lib/settings/metadata/metadata_provider.dart b/lib/settings/metadata/metadata_provider.dart index 053a67b..16802da 100644 --- a/lib/settings/metadata/metadata_provider.dart +++ b/lib/settings/metadata/metadata_provider.dart @@ -1,12 +1,13 @@ 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 @@ -27,7 +28,7 @@ Future deviceName(DeviceNameRef ref) async { } @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 @@ -48,7 +49,7 @@ Future deviceModel(DeviceModelRef ref) async { } @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 @@ -69,7 +70,7 @@ Future deviceSdkVersion(DeviceSdkVersionRef ref) async { } @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 diff --git a/lib/settings/metadata/metadata_provider.g.dart b/lib/settings/metadata/metadata_provider.g.dart index 858c351..b83c0f0 100644 --- a/lib/settings/metadata/metadata_provider.g.dart +++ b/lib/settings/metadata/metadata_provider.g.dart @@ -6,7 +6,7 @@ part of 'metadata_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$deviceNameHash() => r'bc206a3a8c14f3da6e257e92e1ccdc79364f4e28'; +String _$deviceNameHash() => r'9e38adda74e70a91851a682f05228bd759356dcc'; /// See also [deviceName]. @ProviderFor(deviceName) @@ -19,8 +19,10 @@ final deviceNameProvider = FutureProvider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef DeviceNameRef = FutureProviderRef; -String _$deviceModelHash() => r'3d7e8ef4a37b90f98e38dc8d5f16ca30f71e15b2'; +String _$deviceModelHash() => r'922b13d9e35b5b5c5b8e96f2f2c2ae594f4f41f2'; /// See also [deviceModel]. @ProviderFor(deviceModel) @@ -33,8 +35,10 @@ final deviceModelProvider = FutureProvider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef DeviceModelRef = FutureProviderRef; -String _$deviceSdkVersionHash() => r'501b01ae679e02fc5082feabea81cea0fa74afd7'; +String _$deviceSdkVersionHash() => r'33178d80590808d1f4cca2be8a3b52c6f6724cac'; /// See also [deviceSdkVersion]. @ProviderFor(deviceSdkVersion) @@ -48,9 +52,11 @@ final deviceSdkVersionProvider = FutureProvider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element typedef DeviceSdkVersionRef = FutureProviderRef; String _$deviceManufacturerHash() => - r'f0a57e6a92b551fbe266d0a6a29d35dc497882a9'; + r'39250767deb8635fa7c7e18bae23576b9b863e04'; /// See also [deviceManufacturer]. @ProviderFor(deviceManufacturer) @@ -64,6 +70,8 @@ final deviceManufacturerProvider = FutureProvider.internal( allTransitiveDependencies: null, ); +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element 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 +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index 80262ea..fe1f7be 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -19,6 +19,7 @@ class AppSettings with _$AppSettings { @Default(NotificationSettings()) NotificationSettings notificationSettings, @Default(ShakeDetectionSettings()) ShakeDetectionSettings shakeDetectionSettings, + @Default(HomePageSettings()) HomePageSettings homePageSettings, }) = _AppSettings; factory AppSettings.fromJson(Map json) => @@ -28,7 +29,10 @@ class AppSettings with _$AppSettings { @freezed 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; @@ -227,3 +231,16 @@ enum ShakeAction { } enum ShakeDetectedFeedback { vibrate, beep } + +@freezed +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 3f8fd48..17dea47 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -29,6 +29,7 @@ mixin _$AppSettings { throw _privateConstructorUsedError; ShakeDetectionSettings get shakeDetectionSettings => throw _privateConstructorUsedError; + HomePageSettings get homePageSettings => throw _privateConstructorUsedError; /// Serializes this AppSettings to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -52,7 +53,8 @@ abstract class $AppSettingsCopyWith<$Res> { SleepTimerSettings sleepTimerSettings, DownloadSettings downloadSettings, NotificationSettings notificationSettings, - ShakeDetectionSettings shakeDetectionSettings}); + ShakeDetectionSettings shakeDetectionSettings, + HomePageSettings homePageSettings}); $ThemeSettingsCopyWith<$Res> get themeSettings; $PlayerSettingsCopyWith<$Res> get playerSettings; @@ -60,6 +62,7 @@ abstract class $AppSettingsCopyWith<$Res> { $DownloadSettingsCopyWith<$Res> get downloadSettings; $NotificationSettingsCopyWith<$Res> get notificationSettings; $ShakeDetectionSettingsCopyWith<$Res> get shakeDetectionSettings; + $HomePageSettingsCopyWith<$Res> get homePageSettings; } /// @nodoc @@ -83,6 +86,7 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings> Object? downloadSettings = null, Object? notificationSettings = null, Object? shakeDetectionSettings = null, + Object? homePageSettings = null, }) { return _then(_value.copyWith( themeSettings: null == themeSettings @@ -109,6 +113,10 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings> ? _value.shakeDetectionSettings : shakeDetectionSettings // ignore: cast_nullable_to_non_nullable as ShakeDetectionSettings, + homePageSettings: null == homePageSettings + ? _value.homePageSettings + : homePageSettings // ignore: cast_nullable_to_non_nullable + as HomePageSettings, ) as $Val); } @@ -174,6 +182,16 @@ class _$AppSettingsCopyWithImpl<$Res, $Val extends AppSettings> return _then(_value.copyWith(shakeDetectionSettings: value) as $Val); }); } + + /// 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>(_value.homePageSettings, (value) { + return _then(_value.copyWith(homePageSettings: value) as $Val); + }); + } } /// @nodoc @@ -190,7 +208,8 @@ abstract class _$$AppSettingsImplCopyWith<$Res> SleepTimerSettings sleepTimerSettings, DownloadSettings downloadSettings, NotificationSettings notificationSettings, - ShakeDetectionSettings shakeDetectionSettings}); + ShakeDetectionSettings shakeDetectionSettings, + HomePageSettings homePageSettings}); @override $ThemeSettingsCopyWith<$Res> get themeSettings; @@ -204,6 +223,8 @@ abstract class _$$AppSettingsImplCopyWith<$Res> $NotificationSettingsCopyWith<$Res> get notificationSettings; @override $ShakeDetectionSettingsCopyWith<$Res> get shakeDetectionSettings; + @override + $HomePageSettingsCopyWith<$Res> get homePageSettings; } /// @nodoc @@ -225,6 +246,7 @@ class __$$AppSettingsImplCopyWithImpl<$Res> Object? downloadSettings = null, Object? notificationSettings = null, Object? shakeDetectionSettings = null, + Object? homePageSettings = null, }) { return _then(_$AppSettingsImpl( themeSettings: null == themeSettings @@ -251,6 +273,10 @@ class __$$AppSettingsImplCopyWithImpl<$Res> ? _value.shakeDetectionSettings : shakeDetectionSettings // ignore: cast_nullable_to_non_nullable as ShakeDetectionSettings, + homePageSettings: null == homePageSettings + ? _value.homePageSettings + : homePageSettings // ignore: cast_nullable_to_non_nullable + as HomePageSettings, )); } } @@ -264,7 +290,8 @@ class _$AppSettingsImpl implements _AppSettings { this.sleepTimerSettings = const SleepTimerSettings(), this.downloadSettings = const DownloadSettings(), this.notificationSettings = const NotificationSettings(), - this.shakeDetectionSettings = const ShakeDetectionSettings()}); + this.shakeDetectionSettings = const ShakeDetectionSettings(), + this.homePageSettings = const HomePageSettings()}); factory _$AppSettingsImpl.fromJson(Map json) => _$$AppSettingsImplFromJson(json); @@ -287,10 +314,13 @@ class _$AppSettingsImpl implements _AppSettings { @override @JsonKey() final ShakeDetectionSettings shakeDetectionSettings; + @override + @JsonKey() + final HomePageSettings homePageSettings; @override String toString() { - return 'AppSettings(themeSettings: $themeSettings, playerSettings: $playerSettings, sleepTimerSettings: $sleepTimerSettings, downloadSettings: $downloadSettings, notificationSettings: $notificationSettings, shakeDetectionSettings: $shakeDetectionSettings)'; + return 'AppSettings(themeSettings: $themeSettings, playerSettings: $playerSettings, sleepTimerSettings: $sleepTimerSettings, downloadSettings: $downloadSettings, notificationSettings: $notificationSettings, shakeDetectionSettings: $shakeDetectionSettings, homePageSettings: $homePageSettings)'; } @override @@ -309,7 +339,9 @@ class _$AppSettingsImpl implements _AppSettings { (identical(other.notificationSettings, notificationSettings) || other.notificationSettings == notificationSettings) && (identical(other.shakeDetectionSettings, shakeDetectionSettings) || - other.shakeDetectionSettings == shakeDetectionSettings)); + other.shakeDetectionSettings == shakeDetectionSettings) && + (identical(other.homePageSettings, homePageSettings) || + other.homePageSettings == homePageSettings)); } @JsonKey(includeFromJson: false, includeToJson: false) @@ -321,7 +353,8 @@ class _$AppSettingsImpl implements _AppSettings { sleepTimerSettings, downloadSettings, notificationSettings, - shakeDetectionSettings); + shakeDetectionSettings, + homePageSettings); /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. @@ -346,7 +379,8 @@ abstract class _AppSettings implements AppSettings { final SleepTimerSettings sleepTimerSettings, final DownloadSettings downloadSettings, final NotificationSettings notificationSettings, - final ShakeDetectionSettings shakeDetectionSettings}) = _$AppSettingsImpl; + final ShakeDetectionSettings shakeDetectionSettings, + final HomePageSettings homePageSettings}) = _$AppSettingsImpl; factory _AppSettings.fromJson(Map json) = _$AppSettingsImpl.fromJson; @@ -363,6 +397,8 @@ abstract class _AppSettings implements AppSettings { NotificationSettings get notificationSettings; @override ShakeDetectionSettings get shakeDetectionSettings; + @override + HomePageSettings get homePageSettings; /// Create a copy of AppSettings /// with the given fields replaced by the non-null parameter values. @@ -378,7 +414,10 @@ ThemeSettings _$ThemeSettingsFromJson(Map json) { /// @nodoc mixin _$ThemeSettings { - bool get isDarkMode => throw _privateConstructorUsedError; + ThemeMode get themeMode => throw _privateConstructorUsedError; + bool get highContrast => throw _privateConstructorUsedError; + bool get useMaterialThemeFromSystem => throw _privateConstructorUsedError; + String get customThemeColor => throw _privateConstructorUsedError; bool get useMaterialThemeOnItemPage => throw _privateConstructorUsedError; bool get useCurrentPlayerThemeThroughoutApp => throw _privateConstructorUsedError; @@ -400,7 +439,10 @@ abstract class $ThemeSettingsCopyWith<$Res> { _$ThemeSettingsCopyWithImpl<$Res, ThemeSettings>; @useResult $Res call( - {bool isDarkMode, + {ThemeMode themeMode, + bool highContrast, + bool useMaterialThemeFromSystem, + String customThemeColor, bool useMaterialThemeOnItemPage, bool useCurrentPlayerThemeThroughoutApp}); } @@ -420,15 +462,30 @@ class _$ThemeSettingsCopyWithImpl<$Res, $Val extends ThemeSettings> @pragma('vm:prefer-inline') @override $Res call({ - Object? isDarkMode = null, + Object? themeMode = null, + Object? highContrast = null, + Object? useMaterialThemeFromSystem = null, + Object? customThemeColor = null, Object? useMaterialThemeOnItemPage = null, Object? useCurrentPlayerThemeThroughoutApp = null, }) { return _then(_value.copyWith( - isDarkMode: null == isDarkMode - ? _value.isDarkMode - : isDarkMode // ignore: cast_nullable_to_non_nullable + themeMode: null == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + highContrast: null == highContrast + ? _value.highContrast + : highContrast // ignore: cast_nullable_to_non_nullable as bool, + useMaterialThemeFromSystem: null == useMaterialThemeFromSystem + ? _value.useMaterialThemeFromSystem + : useMaterialThemeFromSystem // ignore: cast_nullable_to_non_nullable + as bool, + customThemeColor: null == customThemeColor + ? _value.customThemeColor + : customThemeColor // ignore: cast_nullable_to_non_nullable + as String, useMaterialThemeOnItemPage: null == useMaterialThemeOnItemPage ? _value.useMaterialThemeOnItemPage : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable @@ -451,7 +508,10 @@ abstract class _$$ThemeSettingsImplCopyWith<$Res> @override @useResult $Res call( - {bool isDarkMode, + {ThemeMode themeMode, + bool highContrast, + bool useMaterialThemeFromSystem, + String customThemeColor, bool useMaterialThemeOnItemPage, bool useCurrentPlayerThemeThroughoutApp}); } @@ -469,15 +529,30 @@ class __$$ThemeSettingsImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? isDarkMode = null, + Object? themeMode = null, + Object? highContrast = null, + Object? useMaterialThemeFromSystem = null, + Object? customThemeColor = null, Object? useMaterialThemeOnItemPage = null, Object? useCurrentPlayerThemeThroughoutApp = null, }) { return _then(_$ThemeSettingsImpl( - isDarkMode: null == isDarkMode - ? _value.isDarkMode - : isDarkMode // ignore: cast_nullable_to_non_nullable + themeMode: null == themeMode + ? _value.themeMode + : themeMode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + highContrast: null == highContrast + ? _value.highContrast + : highContrast // ignore: cast_nullable_to_non_nullable as bool, + useMaterialThemeFromSystem: null == useMaterialThemeFromSystem + ? _value.useMaterialThemeFromSystem + : useMaterialThemeFromSystem // ignore: cast_nullable_to_non_nullable + as bool, + customThemeColor: null == customThemeColor + ? _value.customThemeColor + : customThemeColor // ignore: cast_nullable_to_non_nullable + as String, useMaterialThemeOnItemPage: null == useMaterialThemeOnItemPage ? _value.useMaterialThemeOnItemPage : useMaterialThemeOnItemPage // ignore: cast_nullable_to_non_nullable @@ -495,7 +570,10 @@ class __$$ThemeSettingsImplCopyWithImpl<$Res> @JsonSerializable() class _$ThemeSettingsImpl implements _ThemeSettings { const _$ThemeSettingsImpl( - {this.isDarkMode = true, + {this.themeMode = ThemeMode.system, + this.highContrast = false, + this.useMaterialThemeFromSystem = false, + this.customThemeColor = '#FF311B92', this.useMaterialThemeOnItemPage = true, this.useCurrentPlayerThemeThroughoutApp = true}); @@ -504,7 +582,16 @@ class _$ThemeSettingsImpl implements _ThemeSettings { @override @JsonKey() - final bool isDarkMode; + final ThemeMode themeMode; + @override + @JsonKey() + final bool highContrast; + @override + @JsonKey() + final bool useMaterialThemeFromSystem; + @override + @JsonKey() + final String customThemeColor; @override @JsonKey() final bool useMaterialThemeOnItemPage; @@ -514,7 +601,7 @@ class _$ThemeSettingsImpl implements _ThemeSettings { @override String toString() { - return 'ThemeSettings(isDarkMode: $isDarkMode, useMaterialThemeOnItemPage: $useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp: $useCurrentPlayerThemeThroughoutApp)'; + return 'ThemeSettings(themeMode: $themeMode, highContrast: $highContrast, useMaterialThemeFromSystem: $useMaterialThemeFromSystem, customThemeColor: $customThemeColor, useMaterialThemeOnItemPage: $useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp: $useCurrentPlayerThemeThroughoutApp)'; } @override @@ -522,8 +609,16 @@ class _$ThemeSettingsImpl implements _ThemeSettings { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ThemeSettingsImpl && - (identical(other.isDarkMode, isDarkMode) || - other.isDarkMode == isDarkMode) && + (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 == @@ -536,8 +631,14 @@ class _$ThemeSettingsImpl implements _ThemeSettings { @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, isDarkMode, - useMaterialThemeOnItemPage, useCurrentPlayerThemeThroughoutApp); + int get hashCode => Object.hash( + runtimeType, + themeMode, + highContrast, + useMaterialThemeFromSystem, + customThemeColor, + useMaterialThemeOnItemPage, + useCurrentPlayerThemeThroughoutApp); /// Create a copy of ThemeSettings /// with the given fields replaced by the non-null parameter values. @@ -557,7 +658,10 @@ class _$ThemeSettingsImpl implements _ThemeSettings { abstract class _ThemeSettings implements ThemeSettings { const factory _ThemeSettings( - {final bool isDarkMode, + {final ThemeMode themeMode, + final bool highContrast, + final bool useMaterialThemeFromSystem, + final String customThemeColor, final bool useMaterialThemeOnItemPage, final bool useCurrentPlayerThemeThroughoutApp}) = _$ThemeSettingsImpl; @@ -565,7 +669,13 @@ abstract class _ThemeSettings implements ThemeSettings { _$ThemeSettingsImpl.fromJson; @override - bool get isDarkMode; + ThemeMode get themeMode; + @override + bool get highContrast; + @override + bool get useMaterialThemeFromSystem; + @override + String get customThemeColor; @override bool get useMaterialThemeOnItemPage; @override @@ -2753,3 +2863,248 @@ abstract class _ShakeDetectionSettings implements ShakeDetectionSettings { _$$ShakeDetectionSettingsImplCopyWith<_$ShakeDetectionSettingsImpl> get copyWith => throw _privateConstructorUsedError; } + +HomePageSettings _$HomePageSettingsFromJson(Map json) { + return _HomePageSettings.fromJson(json); +} + +/// @nodoc +mixin _$HomePageSettings { + bool get showPlayButtonOnContinueListeningShelf => + throw _privateConstructorUsedError; + bool get showPlayButtonOnContinueSeriesShelf => + throw _privateConstructorUsedError; + bool get showPlayButtonOnAllRemainingShelves => + throw _privateConstructorUsedError; + bool get showPlayButtonOnListenAgainShelf => + throw _privateConstructorUsedError; + + /// Serializes this HomePageSettings to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of HomePageSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $HomePageSettingsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HomePageSettingsCopyWith<$Res> { + factory $HomePageSettingsCopyWith( + HomePageSettings value, $Res Function(HomePageSettings) then) = + _$HomePageSettingsCopyWithImpl<$Res, HomePageSettings>; + @useResult + $Res call( + {bool showPlayButtonOnContinueListeningShelf, + bool showPlayButtonOnContinueSeriesShelf, + bool showPlayButtonOnAllRemainingShelves, + bool showPlayButtonOnListenAgainShelf}); +} + +/// @nodoc +class _$HomePageSettingsCopyWithImpl<$Res, $Val extends HomePageSettings> + implements $HomePageSettingsCopyWith<$Res> { + _$HomePageSettingsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _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(_value.copyWith( + showPlayButtonOnContinueListeningShelf: null == + showPlayButtonOnContinueListeningShelf + ? _value.showPlayButtonOnContinueListeningShelf + : showPlayButtonOnContinueListeningShelf // ignore: cast_nullable_to_non_nullable + as bool, + showPlayButtonOnContinueSeriesShelf: null == + showPlayButtonOnContinueSeriesShelf + ? _value.showPlayButtonOnContinueSeriesShelf + : showPlayButtonOnContinueSeriesShelf // ignore: cast_nullable_to_non_nullable + as bool, + showPlayButtonOnAllRemainingShelves: null == + showPlayButtonOnAllRemainingShelves + ? _value.showPlayButtonOnAllRemainingShelves + : showPlayButtonOnAllRemainingShelves // ignore: cast_nullable_to_non_nullable + as bool, + showPlayButtonOnListenAgainShelf: null == showPlayButtonOnListenAgainShelf + ? _value.showPlayButtonOnListenAgainShelf + : showPlayButtonOnListenAgainShelf // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HomePageSettingsImplCopyWith<$Res> + implements $HomePageSettingsCopyWith<$Res> { + factory _$$HomePageSettingsImplCopyWith(_$HomePageSettingsImpl value, + $Res Function(_$HomePageSettingsImpl) then) = + __$$HomePageSettingsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool showPlayButtonOnContinueListeningShelf, + bool showPlayButtonOnContinueSeriesShelf, + bool showPlayButtonOnAllRemainingShelves, + bool showPlayButtonOnListenAgainShelf}); +} + +/// @nodoc +class __$$HomePageSettingsImplCopyWithImpl<$Res> + extends _$HomePageSettingsCopyWithImpl<$Res, _$HomePageSettingsImpl> + implements _$$HomePageSettingsImplCopyWith<$Res> { + __$$HomePageSettingsImplCopyWithImpl(_$HomePageSettingsImpl _value, + $Res Function(_$HomePageSettingsImpl) _then) + : super(_value, _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(_$HomePageSettingsImpl( + showPlayButtonOnContinueListeningShelf: null == + showPlayButtonOnContinueListeningShelf + ? _value.showPlayButtonOnContinueListeningShelf + : showPlayButtonOnContinueListeningShelf // ignore: cast_nullable_to_non_nullable + as bool, + showPlayButtonOnContinueSeriesShelf: null == + showPlayButtonOnContinueSeriesShelf + ? _value.showPlayButtonOnContinueSeriesShelf + : showPlayButtonOnContinueSeriesShelf // ignore: cast_nullable_to_non_nullable + as bool, + showPlayButtonOnAllRemainingShelves: null == + showPlayButtonOnAllRemainingShelves + ? _value.showPlayButtonOnAllRemainingShelves + : showPlayButtonOnAllRemainingShelves // ignore: cast_nullable_to_non_nullable + as bool, + showPlayButtonOnListenAgainShelf: null == showPlayButtonOnListenAgainShelf + ? _value.showPlayButtonOnListenAgainShelf + : showPlayButtonOnListenAgainShelf // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$HomePageSettingsImpl implements _HomePageSettings { + const _$HomePageSettingsImpl( + {this.showPlayButtonOnContinueListeningShelf = true, + this.showPlayButtonOnContinueSeriesShelf = false, + this.showPlayButtonOnAllRemainingShelves = false, + this.showPlayButtonOnListenAgainShelf = false}); + + factory _$HomePageSettingsImpl.fromJson(Map json) => + _$$HomePageSettingsImplFromJson(json); + + @override + @JsonKey() + final bool showPlayButtonOnContinueListeningShelf; + @override + @JsonKey() + final bool showPlayButtonOnContinueSeriesShelf; + @override + @JsonKey() + final bool showPlayButtonOnAllRemainingShelves; + @override + @JsonKey() + final bool showPlayButtonOnListenAgainShelf; + + @override + String toString() { + return 'HomePageSettings(showPlayButtonOnContinueListeningShelf: $showPlayButtonOnContinueListeningShelf, showPlayButtonOnContinueSeriesShelf: $showPlayButtonOnContinueSeriesShelf, showPlayButtonOnAllRemainingShelves: $showPlayButtonOnAllRemainingShelves, showPlayButtonOnListenAgainShelf: $showPlayButtonOnListenAgainShelf)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HomePageSettingsImpl && + (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); + + /// Create a copy of HomePageSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$HomePageSettingsImplCopyWith<_$HomePageSettingsImpl> get copyWith => + __$$HomePageSettingsImplCopyWithImpl<_$HomePageSettingsImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$HomePageSettingsImplToJson( + this, + ); + } +} + +abstract class _HomePageSettings implements HomePageSettings { + const factory _HomePageSettings( + {final bool showPlayButtonOnContinueListeningShelf, + final bool showPlayButtonOnContinueSeriesShelf, + final bool showPlayButtonOnAllRemainingShelves, + final bool showPlayButtonOnListenAgainShelf}) = _$HomePageSettingsImpl; + + factory _HomePageSettings.fromJson(Map json) = + _$HomePageSettingsImpl.fromJson; + + @override + bool get showPlayButtonOnContinueListeningShelf; + @override + bool get showPlayButtonOnContinueSeriesShelf; + @override + bool get showPlayButtonOnAllRemainingShelves; + @override + bool get showPlayButtonOnListenAgainShelf; + + /// Create a copy of HomePageSettings + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$HomePageSettingsImplCopyWith<_$HomePageSettingsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index 90b317e..131458c 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -32,6 +32,10 @@ _$AppSettingsImpl _$$AppSettingsImplFromJson(Map json) => ? const ShakeDetectionSettings() : ShakeDetectionSettings.fromJson( json['shakeDetectionSettings'] as Map), + homePageSettings: json['homePageSettings'] == null + ? const HomePageSettings() + : HomePageSettings.fromJson( + json['homePageSettings'] as Map), ); Map _$$AppSettingsImplToJson(_$AppSettingsImpl instance) => @@ -42,11 +46,17 @@ Map _$$AppSettingsImplToJson(_$AppSettingsImpl instance) => 'downloadSettings': instance.downloadSettings, 'notificationSettings': instance.notificationSettings, 'shakeDetectionSettings': instance.shakeDetectionSettings, + 'homePageSettings': instance.homePageSettings, }; _$ThemeSettingsImpl _$$ThemeSettingsImplFromJson(Map json) => _$ThemeSettingsImpl( - isDarkMode: json['isDarkMode'] as bool? ?? true, + 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: @@ -55,12 +65,21 @@ _$ThemeSettingsImpl _$$ThemeSettingsImplFromJson(Map json) => Map _$$ThemeSettingsImplToJson(_$ThemeSettingsImpl instance) => { - 'isDarkMode': instance.isDarkMode, + 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, + 'highContrast': instance.highContrast, + 'useMaterialThemeFromSystem': instance.useMaterialThemeFromSystem, + 'customThemeColor': instance.customThemeColor, 'useMaterialThemeOnItemPage': instance.useMaterialThemeOnItemPage, 'useCurrentPlayerThemeThroughoutApp': instance.useCurrentPlayerThemeThroughoutApp, }; +const _$ThemeModeEnumMap = { + ThemeMode.system: 'system', + ThemeMode.light: 'light', + ThemeMode.dark: 'dark', +}; + _$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map json) => _$PlayerSettingsImpl( miniPlayerSettings: json['miniPlayerSettings'] == null @@ -337,3 +356,29 @@ const _$ShakeDetectedFeedbackEnumMap = { ShakeDetectedFeedback.vibrate: 'vibrate', ShakeDetectedFeedback.beep: 'beep', }; + +_$HomePageSettingsImpl _$$HomePageSettingsImplFromJson( + Map json) => + _$HomePageSettingsImpl( + 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 _$$HomePageSettingsImplToJson( + _$HomePageSettingsImpl instance) => + { + 'showPlayButtonOnContinueListeningShelf': + instance.showPlayButtonOnContinueListeningShelf, + 'showPlayButtonOnContinueSeriesShelf': + instance.showPlayButtonOnContinueSeriesShelf, + 'showPlayButtonOnAllRemainingShelves': + instance.showPlayButtonOnAllRemainingShelves, + 'showPlayButtonOnListenAgainShelf': + instance.showPlayButtonOnListenAgainShelf, + }; diff --git a/lib/settings/models/authenticated_user.dart b/lib/settings/models/authenticated_user.dart index 04f2bd0..321c885 100644 --- a/lib/settings/models/authenticated_user.dart +++ b/lib/settings/models/authenticated_user.dart @@ -10,9 +10,8 @@ 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..2a7ce53 100644 --- a/lib/settings/models/authenticated_user.freezed.dart +++ b/lib/settings/models/authenticated_user.freezed.dart @@ -22,9 +22,8 @@ AuthenticatedUser _$AuthenticatedUserFromJson(Map json) { mixin _$AuthenticatedUser { AudiobookShelfServer get server => throw _privateConstructorUsedError; String get authToken => throw _privateConstructorUsedError; - String? get id => throw _privateConstructorUsedError; + String get id => throw _privateConstructorUsedError; String? get username => throw _privateConstructorUsedError; - String? get password => throw _privateConstructorUsedError; /// Serializes this AuthenticatedUser to a JSON map. Map toJson() => throw _privateConstructorUsedError; @@ -45,9 +44,8 @@ abstract class $AuthenticatedUserCopyWith<$Res> { $Res call( {AudiobookShelfServer server, String authToken, - String? id, - String? username, - String? password}); + String id, + String? username}); $AudiobookShelfServerCopyWith<$Res> get server; } @@ -69,9 +67,8 @@ class _$AuthenticatedUserCopyWithImpl<$Res, $Val extends AuthenticatedUser> $Res call({ Object? server = null, Object? authToken = null, - Object? id = freezed, + Object? id = null, Object? username = freezed, - Object? password = freezed, }) { return _then(_value.copyWith( server: null == server @@ -82,18 +79,14 @@ class _$AuthenticatedUserCopyWithImpl<$Res, $Val extends AuthenticatedUser> ? _value.authToken : authToken // ignore: cast_nullable_to_non_nullable as String, - id: freezed == id + id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as String?, + 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); } @@ -119,9 +112,8 @@ abstract class _$$AuthenticatedUserImplCopyWith<$Res> $Res call( {AudiobookShelfServer server, String authToken, - String? id, - String? username, - String? password}); + String id, + String? username}); @override $AudiobookShelfServerCopyWith<$Res> get server; @@ -142,9 +134,8 @@ class __$$AuthenticatedUserImplCopyWithImpl<$Res> $Res call({ Object? server = null, Object? authToken = null, - Object? id = freezed, + Object? id = null, Object? username = freezed, - Object? password = freezed, }) { return _then(_$AuthenticatedUserImpl( server: null == server @@ -155,18 +146,14 @@ class __$$AuthenticatedUserImplCopyWithImpl<$Res> ? _value.authToken : authToken // ignore: cast_nullable_to_non_nullable as String, - id: freezed == id + id: null == id ? _value.id : id // ignore: cast_nullable_to_non_nullable - as String?, + 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?, )); } } @@ -177,9 +164,8 @@ class _$AuthenticatedUserImpl implements _AuthenticatedUser { const _$AuthenticatedUserImpl( {required this.server, required this.authToken, - this.id, - this.username, - this.password}); + required this.id, + this.username}); factory _$AuthenticatedUserImpl.fromJson(Map json) => _$$AuthenticatedUserImplFromJson(json); @@ -189,15 +175,13 @@ class _$AuthenticatedUserImpl implements _AuthenticatedUser { @override final String authToken; @override - final String? id; + final String id; @override final String? username; - @override - final String? password; @override String toString() { - return 'AuthenticatedUser(server: $server, authToken: $authToken, id: $id, username: $username, password: $password)'; + return 'AuthenticatedUser(server: $server, authToken: $authToken, id: $id, username: $username)'; } @override @@ -210,15 +194,12 @@ class _$AuthenticatedUserImpl implements _AuthenticatedUser { other.authToken == authToken) && (identical(other.id, id) || other.id == id) && (identical(other.username, username) || - other.username == username) && - (identical(other.password, password) || - other.password == password)); + other.username == username)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => - Object.hash(runtimeType, server, authToken, id, username, password); + int get hashCode => Object.hash(runtimeType, server, authToken, id, username); /// Create a copy of AuthenticatedUser /// with the given fields replaced by the non-null parameter values. @@ -241,9 +222,8 @@ 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; + required final String id, + final String? username}) = _$AuthenticatedUserImpl; factory _AuthenticatedUser.fromJson(Map json) = _$AuthenticatedUserImpl.fromJson; @@ -253,11 +233,9 @@ abstract class _AuthenticatedUser implements AuthenticatedUser { @override String get authToken; @override - String? get id; + 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. diff --git a/lib/settings/models/authenticated_user.g.dart b/lib/settings/models/authenticated_user.g.dart index 0752807..4ff5a06 100644 --- a/lib/settings/models/authenticated_user.g.dart +++ b/lib/settings/models/authenticated_user.g.dart @@ -12,9 +12,8 @@ _$AuthenticatedUserImpl _$$AuthenticatedUserImplFromJson( 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( @@ -24,5 +23,4 @@ Map _$$AuthenticatedUserImplToJson( '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 77afaae..15386fe 100644 --- a/lib/settings/view/app_settings_page.dart +++ b/lib/settings/view/app_settings_page.dart @@ -7,11 +7,10 @@ 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:material_symbols_icons/symbols.dart'; -import 'package:vaani/api/authenticated_user_provider.dart'; -import 'package:vaani/api/server_provider.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'; @@ -23,58 +22,11 @@ class AppSettingsPage extends HookConsumerWidget { @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.sleepTimerSettings; return SimpleSettingsPage( title: const Text('App Settings'), 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, - ), - ); - }, - ), - ], - ), - // General section SettingsSection( margin: const EdgeInsetsDirectional.symmetric( @@ -86,6 +38,16 @@ class AppSettingsPage extends HookConsumerWidget { 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); + }, + ), NavigationWithSwitchTile( title: const Text('Auto Turn On Sleep Timer'), description: const Text( @@ -106,26 +68,6 @@ class AppSettingsPage extends HookConsumerWidget { ); }, ), - 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( - 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); - }, - ), NavigationWithSwitchTile( title: const Text('Shake Detector'), leading: const Icon(Icons.vibration), @@ -146,6 +88,51 @@ class AppSettingsPage extends HookConsumerWidget { ), ], ), + + // 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( @@ -185,61 +172,11 @@ class AppSettingsPage extends HookConsumerWidget { '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'), - ), - ], - ); + return RestoreDialogue(); }, ); }, @@ -292,3 +229,83 @@ class AppSettingsPage extends HookConsumerWidget { ); } } + +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/buttons.dart b/lib/settings/view/buttons.dart index 0dea11f..4a15b41 100644 --- a/lib/settings/view/buttons.dart +++ b/lib/settings/view/buttons.dart @@ -20,12 +20,16 @@ class OkButton extends StatelessWidget { 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..327044d --- /dev/null +++ b/lib/settings/view/home_page_settings_page.dart @@ -0,0 +1,103 @@ +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/simple_settings_page.dart b/lib/settings/view/simple_settings_page.dart index 471b022..ea9107d 100644 --- a/lib/settings/view/simple_settings_page.dart +++ b/lib/settings/view/simple_settings_page.dart @@ -1,6 +1,7 @@ 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({ @@ -48,6 +49,7 @@ class SimpleSettingsPage extends HookConsumerWidget { ), // 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..315e4fa --- /dev/null +++ b/lib/settings/view/theme_settings_page.dart @@ -0,0 +1,209 @@ +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 index dbc8ecf..8851fa1 100644 --- a/lib/settings/view/widgets/navigation_with_switch_tile.dart +++ b/lib/settings/view/widgets/navigation_with_switch_tile.dart @@ -40,7 +40,7 @@ class NavigationWithSwitchTile extends AbstractSettingsTile { child: Row( children: [ VerticalDivider( - color: Theme.of(context).dividerColor.withOpacity(0.5), + color: Theme.of(context).dividerColor.withValues(alpha: 0.5), indent: 8.0, endIndent: 8.0, ), diff --git a/lib/shared/extensions/obfuscation.dart b/lib/shared/extensions/obfuscation.dart index c70715a..6ff85fe 100644 --- a/lib/shared/extensions/obfuscation.dart +++ b/lib/shared/extensions/obfuscation.dart @@ -1,5 +1,6 @@ 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'; @@ -67,7 +68,6 @@ extension ObfuscateAuthenticatedUser on AuthenticatedUser { return this; } return copyWith( - password: password == null ? null : 'passwordObfuscated', username: username == null ? null : 'usernameObfuscated', authToken: 'authTokenObfuscated', server: server.obfuscate(), @@ -116,10 +116,54 @@ extension ObfuscateResponse on http.Response { return this; } return http.Response( - body, + 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/hooks.dart b/lib/shared/hooks.dart index 62c6116..8e27b3a 100644 --- a/lib/shared/hooks.dart +++ b/lib/shared/hooks.dart @@ -28,64 +28,3 @@ void useTimer(VoidCallback callback, Duration delay) { [delay], ); } - -/// Creates [FixedExtentScrollController] that will be disposed automatically. -/// -/// See also: -/// - [FixedExtentScrollController] -FixedExtentScrollController useFixedExtentScrollController({ - String? debugLabel, - List? keys, - int initialItem = 0, - void Function(ScrollPosition)? onAttach, - void Function(ScrollPosition)? onDetach, -}) { - return use( - _FixedExtentScrollControllerHook( - debugLabel: debugLabel, - keys: keys, - initialItem: initialItem, - onAttach: onAttach, - onDetach: onDetach, - ), - ); -} - -class _FixedExtentScrollControllerHook - extends Hook { - const _FixedExtentScrollControllerHook({ - this.debugLabel, - super.keys, - required this.initialItem, - this.onAttach, - this.onDetach, - }); - - final int initialItem; - final void Function(ScrollPosition)? onAttach; - final void Function(ScrollPosition)? onDetach; - - final String? debugLabel; - - @override - HookState> - createState() => _FixedExtentScrollControllerHookState(); -} - -class _FixedExtentScrollControllerHookState extends HookState< - FixedExtentScrollController, _FixedExtentScrollControllerHook> { - late final controller = FixedExtentScrollController( - initialItem: hook.initialItem, - onAttach: hook.onAttach, - onDetach: hook.onDetach, - ); - - @override - FixedExtentScrollController build(BuildContext context) => controller; - - @override - void dispose() => controller.dispose(); - - @override - String get debugLabel => 'useFixedExtentScrollController'; -} diff --git a/lib/shared/icons/abs_icons.dart b/lib/shared/icons/abs_icons.dart new file mode 100644 index 0000000..eca3e1a --- /dev/null +++ b/lib/shared/icons/abs_icons.dart @@ -0,0 +1,103 @@ +/// 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..8c31fc1 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,42 @@ 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)), + myController.text.startsWith(httpUrlRegExp) ? '' : 'https://', + prefixIcon: ServerAliveIcon(server: parsedUri), // add server button suffixIcon: onPressed == null @@ -62,10 +87,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 ), ), diff --git a/lib/shared/widgets/drawer.dart b/lib/shared/widgets/drawer.dart index be224e9..12aa1c2 100644 --- a/lib/shared/widgets/drawer.dart +++ b/lib/shared/widgets/drawer.dart @@ -3,7 +3,6 @@ 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, diff --git a/lib/shared/widgets/expandable_description.dart b/lib/shared/widgets/expandable_description.dart index 06605e4..203c7b0 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) { diff --git a/lib/shared/widgets/shelves/book_shelf.dart b/lib/shared/widgets/shelves/book_shelf.dart index 692d3b3..4fad4be 100644 --- a/lib/shared/widgets/shelves/book_shelf.dart +++ b/lib/shared/widgets/shelves/book_shelf.dart @@ -17,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 { @@ -25,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) { @@ -41,6 +43,7 @@ class BookHomeShelf extends HookConsumerWidget { item: item, key: ValueKey(shelf.id + item.id), heroTagSuffix: shelf.id, + showPlayButton: showPlayButton, ), _ => Container(), }, @@ -114,38 +117,41 @@ class BookOnShelf extends HookConsumerWidget { 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); - }, + }, + ), ), ), ), @@ -197,7 +203,6 @@ class BookOnShelf extends HookConsumerWidget { class _BookOnShelfPlayButton extends HookConsumerWidget { const _BookOnShelfPlayButton({ - super.key, required this.libraryItemId, }); @@ -253,8 +258,10 @@ 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, ), @@ -272,7 +279,10 @@ class _BookOnShelfPlayButton extends HookConsumerWidget { 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 { @@ -314,9 +324,10 @@ class BookCoverSkeleton extends StatelessWidget { child: SizedBox( width: 150, child: Shimmer.fromColors( - baseColor: Theme.of(context).colorScheme.surface.withOpacity(0.3), + baseColor: + Theme.of(context).colorScheme.surface.withValues(alpha: 0.3), highlightColor: - Theme.of(context).colorScheme.onSurface.withOpacity(0.1), + 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..9a59a54 100644 --- a/lib/shared/widgets/shelves/home_shelf.dart +++ b/lib/shared/widgets/shelves/home_shelf.dart @@ -15,10 +15,12 @@ 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) { @@ -26,6 +28,7 @@ class HomeShelf extends HookConsumerWidget { ShelfType.book => BookHomeShelf( title: title, shelf: shelf.asLibraryItemShelf, + showPlayButton: showPlayButton, ), ShelfType.authors => AuthorHomeShelf( title: title, 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..529306e --- /dev/null +++ b/lib/theme/providers/system_theme_provider.dart @@ -0,0 +1,74 @@ +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..5685c95 --- /dev/null +++ b/lib/theme/providers/system_theme_provider.g.dart @@ -0,0 +1,179 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'system_theme_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$systemThemeHash() => r'c78d3d94683624a80b296594268c5fd4295e77a3'; + +/// 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)); + } +} + +/// copied from [DynamicColorBuilder] +/// +/// Copied from [systemTheme]. +@ProviderFor(systemTheme) +const systemThemeProvider = SystemThemeFamily(); + +/// copied from [DynamicColorBuilder] +/// +/// Copied from [systemTheme]. +class SystemThemeFamily + extends Family> { + /// copied from [DynamicColorBuilder] + /// + /// Copied from [systemTheme]. + const SystemThemeFamily(); + + /// copied from [DynamicColorBuilder] + /// + /// Copied from [systemTheme]. + SystemThemeProvider call({ + bool highContrast = false, + }) { + return SystemThemeProvider( + highContrast: highContrast, + ); + } + + @override + SystemThemeProvider getProviderOverride( + covariant SystemThemeProvider provider, + ) { + return call( + highContrast: provider.highContrast, + ); + } + + 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'systemThemeProvider'; +} + +/// copied from [DynamicColorBuilder] +/// +/// Copied from [systemTheme]. +class SystemThemeProvider + extends FutureProvider<(ColorScheme light, ColorScheme dark)?> { + /// copied from [DynamicColorBuilder] + /// + /// Copied from [systemTheme]. + SystemThemeProvider({ + bool highContrast = false, + }) : this._internal( + (ref) => systemTheme( + ref as SystemThemeRef, + highContrast: highContrast, + ), + from: systemThemeProvider, + name: r'systemThemeProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$systemThemeHash, + dependencies: SystemThemeFamily._dependencies, + allTransitiveDependencies: + SystemThemeFamily._allTransitiveDependencies, + highContrast: highContrast, + ); + + SystemThemeProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.highContrast, + }) : super.internal(); + + final bool highContrast; + + @override + Override overrideWith( + FutureOr<(ColorScheme light, ColorScheme dark)?> Function( + SystemThemeRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: SystemThemeProvider._internal( + (ref) => create(ref as SystemThemeRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + highContrast: highContrast, + ), + ); + } + + @override + FutureProviderElement<(ColorScheme light, ColorScheme dark)?> + createElement() { + return _SystemThemeProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SystemThemeProvider && other.highContrast == highContrast; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, highContrast.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin SystemThemeRef + on FutureProviderRef<(ColorScheme light, ColorScheme dark)?> { + /// The parameter `highContrast` of this provider. + bool get highContrast; +} + +class _SystemThemeProviderElement + extends FutureProviderElement<(ColorScheme light, ColorScheme dark)?> + with SystemThemeRef { + _SystemThemeProviderElement(super.provider); + + @override + bool get highContrast => (origin as SystemThemeProvider).highContrast; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/theme/theme_from_cover_provider.dart b/lib/theme/providers/theme_from_cover_provider.dart similarity index 78% rename from lib/theme/theme_from_cover_provider.dart rename to lib/theme/providers/theme_from_cover_provider.dart index a535f2f..3e9a667 100644 --- a/lib/theme/theme_from_cover_provider.dart +++ b/lib/theme/providers/theme_from_cover_provider.dart @@ -1,5 +1,7 @@ +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:vaani/api/image_provider.dart'; @@ -10,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 @@ -47,17 +59,21 @@ Future> themeFromCover( @Riverpod(keepAlive: true) FutureOr themeOfLibraryItem( - ThemeOfLibraryItemRef ref, + Ref ref, String? itemId, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) async { if (itemId == null) { return null; } 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/theme_from_cover_provider.g.dart b/lib/theme/providers/theme_from_cover_provider.g.dart similarity index 84% rename from lib/theme/theme_from_cover_provider.g.dart rename to lib/theme/providers/theme_from_cover_provider.g.dart index 61a86a9..1289765 100644 --- a/lib/theme/theme_from_cover_provider.g.dart +++ b/lib/theme/providers/theme_from_cover_provider.g.dart @@ -6,7 +6,7 @@ part of 'theme_from_cover_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$themeFromCoverHash() => r'a549513a0dcdff76be94488baf38a8b886ce63eb'; +String _$themeFromCoverHash() => r'afdeddc4bfe2fe46a4185143d3a88a23565e33f4'; /// Copied from Dart SDK class _SystemHash { @@ -42,10 +42,12 @@ class ThemeFromCoverFamily extends Family>> { ThemeFromCoverProvider call( ImageProvider img, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) { return ThemeFromCoverProvider( img, brightness: brightness, + highContrast: highContrast, ); } @@ -56,6 +58,7 @@ class ThemeFromCoverFamily extends Family>> { return call( provider.img, brightness: provider.brightness, + highContrast: provider.highContrast, ); } @@ -80,11 +83,13 @@ class ThemeFromCoverProvider extends FutureProvider> { ThemeFromCoverProvider( ImageProvider img, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) : this._internal( (ref) => themeFromCover( ref as ThemeFromCoverRef, img, brightness: brightness, + highContrast: highContrast, ), from: themeFromCoverProvider, name: r'themeFromCoverProvider', @@ -97,6 +102,7 @@ class ThemeFromCoverProvider extends FutureProvider> { ThemeFromCoverFamily._allTransitiveDependencies, img: img, brightness: brightness, + highContrast: highContrast, ); ThemeFromCoverProvider._internal( @@ -108,10 +114,12 @@ class ThemeFromCoverProvider extends FutureProvider> { required super.from, required this.img, required this.brightness, + required this.highContrast, }) : super.internal(); final ImageProvider img; final Brightness brightness; + final bool highContrast; @override Override overrideWith( @@ -129,6 +137,7 @@ class ThemeFromCoverProvider extends FutureProvider> { debugGetCreateSourceHash: null, img: img, brightness: brightness, + highContrast: highContrast, ), ); } @@ -142,7 +151,8 @@ class ThemeFromCoverProvider extends FutureProvider> { bool operator ==(Object other) { return other is ThemeFromCoverProvider && other.img == img && - other.brightness == brightness; + other.brightness == brightness && + other.highContrast == highContrast; } @override @@ -150,17 +160,23 @@ class ThemeFromCoverProvider extends FutureProvider> { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, img.hashCode); hash = _SystemHash.combine(hash, brightness.hashCode); + hash = _SystemHash.combine(hash, highContrast.hashCode); return _SystemHash.finish(hash); } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin ThemeFromCoverRef on FutureProviderRef> { /// The parameter `img` of this provider. ImageProvider get img; /// The parameter `brightness` of this provider. Brightness get brightness; + + /// The parameter `highContrast` of this provider. + bool get highContrast; } class _ThemeFromCoverProviderElement @@ -172,10 +188,12 @@ class _ThemeFromCoverProviderElement ImageProvider get img => (origin as ThemeFromCoverProvider).img; @override Brightness get brightness => (origin as ThemeFromCoverProvider).brightness; + @override + bool get highContrast => (origin as ThemeFromCoverProvider).highContrast; } String _$themeOfLibraryItemHash() => - r'a1d0e5d81f4debe88d5a6ce46c3af28623ad4273'; + r'0b2df397b2938003a9de6beb6d4204401a05370c'; /// See also [themeOfLibraryItem]. @ProviderFor(themeOfLibraryItem) @@ -190,10 +208,12 @@ class ThemeOfLibraryItemFamily extends Family> { ThemeOfLibraryItemProvider call( String? itemId, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) { return ThemeOfLibraryItemProvider( itemId, brightness: brightness, + highContrast: highContrast, ); } @@ -204,6 +224,7 @@ class ThemeOfLibraryItemFamily extends Family> { return call( provider.itemId, brightness: provider.brightness, + highContrast: provider.highContrast, ); } @@ -228,11 +249,13 @@ class ThemeOfLibraryItemProvider extends FutureProvider { ThemeOfLibraryItemProvider( String? itemId, { Brightness brightness = Brightness.dark, + bool highContrast = false, }) : this._internal( (ref) => themeOfLibraryItem( ref as ThemeOfLibraryItemRef, itemId, brightness: brightness, + highContrast: highContrast, ), from: themeOfLibraryItemProvider, name: r'themeOfLibraryItemProvider', @@ -245,6 +268,7 @@ class ThemeOfLibraryItemProvider extends FutureProvider { ThemeOfLibraryItemFamily._allTransitiveDependencies, itemId: itemId, brightness: brightness, + highContrast: highContrast, ); ThemeOfLibraryItemProvider._internal( @@ -256,10 +280,12 @@ class ThemeOfLibraryItemProvider extends FutureProvider { required super.from, required this.itemId, required this.brightness, + required this.highContrast, }) : super.internal(); final String? itemId; final Brightness brightness; + final bool highContrast; @override Override overrideWith( @@ -276,6 +302,7 @@ class ThemeOfLibraryItemProvider extends FutureProvider { debugGetCreateSourceHash: null, itemId: itemId, brightness: brightness, + highContrast: highContrast, ), ); } @@ -289,7 +316,8 @@ class ThemeOfLibraryItemProvider extends FutureProvider { bool operator ==(Object other) { return other is ThemeOfLibraryItemProvider && other.itemId == itemId && - other.brightness == brightness; + other.brightness == brightness && + other.highContrast == highContrast; } @override @@ -297,17 +325,23 @@ class ThemeOfLibraryItemProvider extends FutureProvider { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, itemId.hashCode); hash = _SystemHash.combine(hash, brightness.hashCode); + hash = _SystemHash.combine(hash, highContrast.hashCode); return _SystemHash.finish(hash); } } +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element mixin ThemeOfLibraryItemRef on FutureProviderRef { /// The parameter `itemId` of this provider. String? get itemId; /// The parameter `brightness` of this provider. Brightness get brightness; + + /// The parameter `highContrast` of this provider. + bool get highContrast; } class _ThemeOfLibraryItemProviderElement @@ -319,6 +353,8 @@ class _ThemeOfLibraryItemProviderElement @override Brightness get brightness => (origin as ThemeOfLibraryItemProvider).brightness; + @override + bool get highContrast => (origin as ThemeOfLibraryItemProvider).highContrast; } // ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/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/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 879195f..606c5a6 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + 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_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 026cbff..6023074 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color isar_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/pubspec.lock b/pubspec.lock index b6de7a9..216ce34 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "76.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.11.0" analyzer_plugin: dependency: transitive description: @@ -50,58 +50,58 @@ packages: 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: "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 +122,66 @@ packages: dependency: "direct main" description: name: background_downloader - sha256: "6b73fa5d20c47e855f6ef3ed6fb3e0d164141d8ae7d43ca0a42c78f90eaa15e7" + sha256: d3016a9eb584f6cb16384c8b4a008943c39119730d60046044349b5dbbda4ccb url: "https://pub.dev" source: hosted - version: "8.5.6" + 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: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.4" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.15" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -194,10 +194,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: "7193c909c8608d3e1a263093ff045f7140bcc1bf3f7de2c5ec7ad027891d2d22" url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.10.0" cached_network_image: dependency: "direct main" description: @@ -222,14 +222,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: @@ -250,18 +258,18 @@ packages: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" 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 +282,26 @@ 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" cross_file: dependency: transitive description: @@ -306,10 +314,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: @@ -322,66 +330,74 @@ packages: dependency: "direct dev" description: name: custom_lint - sha256: "6e1ec47427ca968f22bce734d00028ae7084361999b41673291138945c5baca0" + sha256: "3486c470bb93313a9417f926c7dd694a2e349220992d7b9d14534dc49c15bba9" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.0" custom_lint_builder: dependency: transitive description: name: custom_lint_builder - sha256: ba2f90fff4eff71d202d097eb14b14f87087eaaef742e956208c0eb9d3a40a21 + sha256: "42cdc41994eeeddab0d7a722c7093ec52bd0761921eeb2cbdbf33d192a234759" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.0" custom_lint_core: dependency: transitive description: name: custom_lint_core - sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d" + sha256: "02450c3e45e2a6e8b26c4d16687596ab3c4644dd5792e3313aa9ceba5a49b7f5" url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.7.0" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: bfe9b7a09c4775a587b58d10ebb871d4fe618237639b1e84d5ec62d7dfef25f9 + url: "https://pub.dev" + source: hosted + version: "1.0.0+6.11.0" dart_style: dependency: transitive description: name: dart_style - sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "2.3.7" + version: "2.3.8" 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: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.8.0+1" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.1" duration_picker: dependency: "direct main" description: @@ -390,6 +406,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: @@ -402,42 +426,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: "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 @@ -447,10 +471,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: @@ -459,22 +483,14 @@ 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: @@ -483,30 +499,22 @@ packages: url: "https://pub.dev" source: hosted version: "5.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" 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: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.6.1" flutter_settings_ui: dependency: "direct main" description: @@ -537,10 +545,10 @@ 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: @@ -569,18 +577,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: "5cf5fdcf853b0629deb35891c7af643be900c3dcaed7489009f9e7dbcfe55ab6" + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 url: "https://pub.dev" source: hosted - version: "14.2.8" + version: "14.8.1" graphs: dependency: transitive description: @@ -601,50 +609,50 @@ packages: dependency: "direct main" description: name: hooks_riverpod - sha256: "97266a91c994951a06ef0ff3a1c7fb261e52ec7f74e87f0614ea0b7411b859b2" + sha256: "70bba33cfc5670c84b796e6929c54b8bc5be7d0fe15bb28c2560500b9ad06966" url: "https://pub.dev" source: hosted - version: "2.5.2" + version: "2.6.1" hotreloader: dependency: transitive description: name: hotreloader - sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e + sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.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: @@ -657,18 +665,18 @@ 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" + version: "1.0.5" isar: dependency: "direct main" description: @@ -705,67 +713,67 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.0" 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: path: just_audio_background ref: media-notification-config - resolved-ref: "79ac48a7d322d5b8db8847b35ed0c8555fa249bc" + resolved-ref: fce45f334f0838cb6f630548efb65fec40ff17b4 url: "https://github.com/Dr-Blank/just_audio" source: git - version: "0.0.1-beta.13" + 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: "9a98035b8b24b40749507687520ec5ab404e291d2b0937823ff45d92cb18d448" + sha256: "6ba8a2a7e87d57d32f0f7b42856ade3d6a9fbe0f1a11fabae0a4f00bb73f0663" url: "https://pub.dev" source: hosted - version: "0.4.13" + version: "0.4.16" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -778,10 +786,10 @@ packages: dependency: transitive description: name: lints - sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.1" list_wheel_scroll_view_nls: dependency: "direct main" description: @@ -794,10 +802,10 @@ packages: 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: @@ -810,28 +818,28 @@ packages: dependency: "direct main" description: name: lottie - sha256: "6a24ade5d3d918c306bb1c21a6b9a04aab0489d51a2582522eea820b4093b62b" + sha256: c5fa04a80a620066c15cf19cc44773e19e9b38e989ff23ea32e5903ef1015950 url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.3.1" macros: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" 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 @@ -842,26 +850,26 @@ packages: dependency: "direct main" description: name: material_symbols_icons - sha256: "66416c4e30bd363508e12669634fc4f3250b83b69e862de67f4f9c480cf42414" + sha256: "7c50901b39d1ad645ee25d920aed008061e1fd541a897b4ebf2c01d966dbf16b" url: "https://pub.dev" source: hosted - version: "4.2785.1" + 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: @@ -874,18 +882,18 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.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: @@ -915,58 +923,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: @@ -991,22 +999,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: @@ -1023,62 +1079,70 @@ 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: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" url: "https://pub.dev" source: hosted - version: "2.5.1" + version: "2.6.1" riverpod_analyzer_utils: dependency: transitive description: name: riverpod_analyzer_utils - sha256: ac28d7bc678471ec986b42d88e5a0893513382ff7542c7ac9634463b044ac72c + sha256: c6b8222b2b483cb87ae77ad147d6408f400c64f060df7a225b127f4afef4f8c8 url: "https://pub.dev" source: hosted - version: "0.5.4" + version: "0.5.8" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.6.1" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: "63311e361ffc578d655dfc31b48dfa4ed3bc76fd06f9be845e9bf97c5c11a429" + sha256: "63546d70952015f0981361636bf8f356d9cfd9d7f6f0815e3c07789a41233188" url: "https://pub.dev" source: hosted - version: "2.4.3" + version: "2.6.3" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: a35a92f2c2a4b7a5d95671c96c5432b42c20f26bb3e985e83d0b186471b61a85 + sha256: "83e4caa337a9840469b7b9bd8c2351ce85abad80f570d84146911b32086fbd99" url: "https://pub.dev" source: hosted - version: "2.3.13" + version: "2.6.3" rxdart: dependency: transitive description: @@ -1091,10 +1155,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: @@ -1107,50 +1171,50 @@ 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: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52" + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da url: "https://pub.dev" source: hosted - version: "10.0.2" + version: "10.1.4" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b url: "https://pub.dev" source: hosted - version: "5.0.0" + 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_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: @@ -1170,23 +1234,23 @@ 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: "543842390db2c1d1b02e1ad0e6167db2f8872c7a6669051cb89e2559b11a5aeb" + sha256: c8471c2c6843cf308a5532ff653f2bcdb7fa9ae79d84d1179920578a06624f0d url: "https://pub.dev" source: hosted - version: "3.0.0-beta.4" + version: "3.1.2" socket_io_common: dependency: transitive description: name: socket_io_common - sha256: "392c3613c88ad3ee0e15911db2e7e9dbd675622c2589ce99383630603b097619" + sha256: "162fbaecbf4bf9a9372a62a341b3550b51dcef2f02f3e5830a297fd48203d45b" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.1" source_gen: dependency: transitive description: @@ -1199,18 +1263,18 @@ packages: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.5" 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: @@ -1223,26 +1287,50 @@ packages: dependency: transitive description: name: sqflite - sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788 + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 url: "https://pub.dev" source: hosted - version: "2.3.3+2" + 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: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4" + sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" url: "https://pub.dev" source: hosted - version: "2.5.4+4" + 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: @@ -1255,66 +1343,66 @@ 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: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" + sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" url: "https://pub.dev" source: hosted - version: "3.3.0+3" + 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_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" 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: @@ -1327,50 +1415,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: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1383,18 +1471,18 @@ 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: @@ -1415,82 +1503,82 @@ packages: dependency: "direct main" description: name: vibration - sha256: fe8f90e1827f86a4f722b819799ecac8a24789a39c6d562ea316bcaeb8b1ec61 + sha256: "804ee8f9628f31ee71fbe6137a2bc6206a64e101ec22cd9dd6d3a7dc0272591b" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.1.3" vibration_platform_interface: dependency: transitive description: name: vibration_platform_interface - sha256: "735a5fef0f284de0ad9449a5ed7d36ba017c6f59b5b20ac64418af4a6bd35ee7" + sha256: "03e9deaa4df48a1a6212e281bfee5f610d62e9247929dd2f26f4efd4fa5e225c" url: "https://pub.dev" source: hosted - version: "0.0.1" + 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: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.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" 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: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" url: "https://pub.dev" source: hosted - version: "1.1.5" + 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: @@ -1503,10 +1591,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.7.0 <4.0.0" + flutter: ">=3.32.0" diff --git a/pubspec.yaml b/pubspec.yaml index 3ce174f..daa3a2e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,10 +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.11+2 +version: 0.0.18+9 environment: sdk: ">=3.3.4 <4.0.0" + flutter: 3.32.0 isar_version: &isar_version ^4.0.0-dev.13 # define the version to be used @@ -32,26 +33,26 @@ 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 - archive: ^3.6.1 + archive: ^4.0.5 audio_service: ^0.18.15 - audio_session: ^0.1.19 + 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: ^8.1.2 + 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 @@ -73,6 +74,7 @@ dependencies: 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 @@ -84,6 +86,7 @@ dependencies: package_info_plus: ^8.0.0 path: ^1.9.0 path_provider: ^2.1.0 + permission_handler: ^11.3.1 riverpod_annotation: ^2.3.5 scroll_loop_auto_scroll: ^0.0.5 sensors_plus: ^6.0.1 @@ -92,10 +95,10 @@ dependencies: path: ./shelfsdk shimmer: ^3.0.0 url_launcher: ^6.2.6 - vibration: ^2.0.0 + vibration: ^3.1.3 dev_dependencies: build_runner: ^2.4.9 - custom_lint: ^0.6.4 + custom_lint: ^0.7.0 flutter_lints: ^5.0.0 flutter_test: sdk: flutter @@ -119,6 +122,7 @@ flutter: - 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 @@ -144,3 +148,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..e1848a4 --- /dev/null +++ b/shelfsdk @@ -0,0 +1 @@ +Subproject commit e1848a42c27257146015a33e9427f197f522fe03 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8d09818..de21556 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,16 +6,22 @@ #include "generated_plugin_registrant.h" +#include #include #include +#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DynamicColorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); IsarFlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 51689fc..13d504d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,8 +3,10 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color isar_flutter_libs media_kit_libs_windows_audio + permission_handler_windows share_plus url_launcher_windows )