Compare commits

...

57 commits

Author SHA1 Message Date
github-actions[bot]
e30e84ded1 chore(release): bump version to v0.0.19
Some checks failed
Flutter CI & Release / Test (push) Has been cancelled
Flutter CI & Release / Build Android APKs (push) Has been cancelled
Flutter CI & Release / build_linux (push) Has been cancelled
Flutter CI & Release / Create GitHub Release (push) Has been cancelled
2026-01-10 12:47:45 +00:00
Dr.Blank
e23c0b6c5f
chore: run dart format
Some checks are pending
Flutter CI & Release / Test (push) Waiting to run
Flutter CI & Release / Build Android APKs (push) Blocked by required conditions
Flutter CI & Release / build_linux (push) Blocked by required conditions
Flutter CI & Release / Create GitHub Release (push) Blocked by required conditions
2026-01-10 16:51:05 +05:30
Dr.Blank
a520136e01
chore: update flutter and dependencies 2026-01-10 16:46:06 +05:30
Dr.Blank
06694f5f0b
chore: target android api 36 2026-01-10 14:43:59 +05:30
Dr.Blank
07aea41c6e
fix: update build_runner command to use dart directly instead of fvm
Some checks failed
Flutter CI & Release / Test (push) Has been cancelled
Flutter CI & Release / Build Android APKs (push) Has been cancelled
Flutter CI & Release / build_linux (push) Has been cancelled
Flutter CI & Release / Create GitHub Release (push) Has been cancelled
2025-05-28 12:25:22 +05:30
Dr.Blank
8485a26f1a
chore: update Flutter to version 3.32.0 (#85) 2025-05-28 11:32:07 +05:30
Dr.Blank
19046d92d9
feat: add documentation for testing deeplinks on Linux
Some checks are pending
Flutter CI & Release / Test (push) Waiting to run
Flutter CI & Release / Build Android APKs (push) Blocked by required conditions
Flutter CI & Release / build_linux (push) Blocked by required conditions
Flutter CI & Release / Create GitHub Release (push) Blocked by required conditions
2025-05-27 23:15:52 +05:30
Dr.Blank
4619657f00
feat: deeplinks for linux (#84)
Some checks failed
Flutter CI & Release / Test (push) Has been cancelled
Flutter CI & Release / Build Android APKs (push) Has been cancelled
Flutter CI & Release / build_linux (push) Has been cancelled
Flutter CI & Release / Create GitHub Release (push) Has been cancelled
2025-05-23 21:03:02 +05:30
Dr.Blank
db20682004
feat: Add AppImage build and generic naming for Linux artifacts (#83)
Some checks are pending
Flutter CI & Release / Test (push) Waiting to run
Flutter CI & Release / Build Android APKs (push) Blocked by required conditions
Flutter CI & Release / build_linux (push) Blocked by required conditions
Flutter CI & Release / Create GitHub Release (push) Blocked by required conditions
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-05-23 10:53:24 +05:30
Dr.Blank
5c7be5cbe4
feat: Add settings to control play button visibility on home shelves (#81)
Some checks failed
Flutter CI & Release / Test (push) Has been cancelled
Flutter CI & Release / Build Android APKs (push) Has been cancelled
Flutter CI & Release / build_linux (push) Has been cancelled
Flutter CI & Release / Create GitHub Release (push) Has been cancelled
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-05-22 08:24:59 +05:30
Dr.Blank
25c3346941
feat: Replace theme dialog with segmented buttons (#82)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-05-22 07:26:25 +05:30
github-actions[bot]
23e5d73bea chore(release): bump version to v0.0.18
Some checks failed
Flutter CI & Release / Test (push) Has been cancelled
Flutter CI & Release / Build Android APKs (push) Has been cancelled
Flutter CI & Release / build_linux (push) Has been cancelled
Flutter CI & Release / Create GitHub Release (push) Has been cancelled
2025-04-23 11:01:25 +00:00
Dr.Blank
bae99292a2
feat: add PlayingIndicatorIcon widget for animated playback indication (#80) 2025-04-23 16:23:57 +05:30
Dr.Blank
25be7fda03
fix: keyboard not showing when adding new user (#79)
* feat: add fadeSlideTransitionBuilder for smoother transitions in user login

* fix: reuse onboarding components on server manager page

* fix: gaining focus rebuilt the widget

using memoized fixes this issue
2025-04-23 15:00:01 +05:30
Dr.Blank
c8767b4e1e
feat: enhance UserBar with user API details and improved text styling 2025-04-23 14:51:39 +05:30
Dr.Blank
ad0cd6e2ad
fix: run dart fix
Some checks are pending
Flutter CI & Release / Test (push) Waiting to run
Flutter CI & Release / Build Android APKs (push) Blocked by required conditions
Flutter CI & Release / build_linux (push) Blocked by required conditions
Flutter CI & Release / Create GitHub Release (push) Blocked by required conditions
2025-04-23 00:29:02 +05:30
Dr.Blank
2cb00c451e
feat: implement scroll-to-top FAB and enhance library item app bar with scroll listener 2025-04-23 00:22:32 +05:30
Dr.Blank
c3d3a3900d
refactor: remove transparent background color from app bars
Some checks failed
Flutter CI & Release / Test (push) Has been cancelled
Flutter CI & Release / Build Android APKs (push) Has been cancelled
Flutter CI & Release / build_linux (push) Has been cancelled
Flutter CI & Release / Create GitHub Release (push) Has been cancelled
2025-04-19 19:47:25 +05:30
Dr.Blank
5f85df4d19
add SVG conversion instructions and vaani_logo.svg file 2025-04-19 19:27:15 +05:30
Dr.Blank
5986482baf
feat: ability to change library (#77)
* feat: add AbsIcons font and update pubspec.yaml for font integration

* feat: implement library selection in YouPage

* fix: optimize authenticatedApi provider to not rebuild unnecessarily

* feat: add LibrarySwitchChip widget and integrate it into YouPage and ScaffoldWithNavBar

* feat: enhance library selection UI with refresh functionality and error handling

* fix: change library switcher activation from long press to double tap

* feat: show current library on nav bar

* feat: refactor LibraryBrowserPage to use CustomScrollView and enhance app bar with dynamic library icon and title
2025-04-19 19:17:31 +05:30
Dr.Blank
37c44f1c6b
add more ways to get app
Some checks failed
Flutter CI & Release / Test (push) Has been cancelled
Flutter CI & Release / Build Android APKs (push) Has been cancelled
Flutter CI & Release / build_linux (push) Has been cancelled
Flutter CI & Release / Create GitHub Release (push) Has been cancelled
2025-04-11 21:00:50 +05:30
github-actions[bot]
b0ea9e14d2 chore(release): bump version to v0.0.17
Some checks are pending
Flutter CI & Release / Test (push) Waiting to run
Flutter CI & Release / Build Android APKs (push) Blocked by required conditions
Flutter CI & Release / build_linux (push) Blocked by required conditions
Flutter CI & Release / Create GitHub Release (push) Blocked by required conditions
2025-04-11 06:49:50 +00:00
Dr.Blank
de7c3359f7
ci: use PAT instead of github token 2025-04-11 12:13:08 +05:30
Dr.Blank
39d051746b
update label regex 2025-04-10 19:46:57 +05:30
Dr.Blank
4ebf46d2fd
chore: remove deprecated apis (#74)
* dart format + remove withopacity

* remove unused imports
2025-04-10 19:42:08 +05:30
Dr.Blank
4af16ac5b4
ci: add format check for lib/ 2025-04-10 19:38:18 +05:30
Dr.Blank
28ceca5408
fix: bug login not shown for some languages (#73)
* fix language preventing logging in

* make eye blink once
2025-04-10 19:12:20 +05:30
Dr.Blank
4663ff9094
feat: add Linux packaging support (#70)
Some checks failed
Flutter CI & Release / Test (push) Has been cancelled
Flutter CI & Release / Build Android APKs (push) Has been cancelled
Flutter CI & Release / build_linux (push) Has been cancelled
Flutter CI & Release / Create GitHub Release (push) Has been cancelled
* Refactor CI workflow and add Linux packaging support; update app title to "Vaani"

* Refactor CI workflow to separate setup, testing, and building steps; add Linux AppImage packaging support

* use reusable workflow

* Make Flutter version input optional in setup-env action and rename step in workflow

* Replace setup-env action with reusable flutter-setup workflow; streamline CI configuration and enhance dependency management

* Add Flutter setup composite action for streamlined environment configuration

* Move repository checkout step to the main workflow for better control and clarity in the CI process

* Remove unnecessary shell specification for Flutter dependency setup to simplify action configuration

* Add shell specification for Flutter dependency command to enhance cross-platform compatibility

* Comment out static analysis step in Flutter test workflow to streamline CI process

* Add repository checkout and Flutter environment setup steps to CI workflow

* Add installation of Linux dependencies for Flutter test workflow

* Remove obsolete Flutter setup and release workflows to streamline CI configuration

* Fix formatting in make_config.yaml by ensuring newline at end of file
2025-04-05 10:31:07 +05:30
Dr.Blank
412c212118
bump version
Some checks failed
Flutter Test / test (push) Has been cancelled
2025-04-04 14:28:39 +05:30
Dr.Blank
edf7b2790f
upgrade to flutter 3.29.2
Some checks are pending
Flutter Test / test (push) Waiting to run
2025-04-03 23:07:00 +05:30
Dr.Blank
2fd4650bb8
upgrade to flutter 3.27.4 2025-03-25 22:01:16 +05:30
Dr.Blank
e7946feca1
Update full_description.txt
Co-Authored-By: Izzy <6781438+izzysoft@users.noreply.github.com>
2024-12-21 07:53:59 +05:30
Dr.Blank
997d3eb5e4
fix flutter version for reproducible builds 2024-12-08 03:58:03 +05:30
Dr.Blank
77f7a7e3b5
add instructions to add keystore 2024-12-08 03:43:46 +05:30
Dr.Blank
6c50821682
Make shelfsdk a submodule (#66)
* make shelfsdk a submodule

* add contributing.md

* add submodule info

* update ci

* add submodule
2024-12-08 01:52:30 +05:30
Dr.Blank
247413def0
fastlane init (#65) 2024-12-08 01:50:00 +05:30
Dr.Blank
2a715f6fa8
remove manage external storage permission 2024-12-07 21:07:48 +05:30
Dr.Blank
c2cf999398
dart fix 2024-12-07 12:26:59 +05:30
Dr-Blank
3488ae97fb
fix overflow error for onboarding 2024-10-28 05:45:28 +05:30
Dr-Blank
11b768d41c
chore: bump version to 0.0.15+6 in pubspec.yaml 2024-10-11 12:01:14 -04:00
Dr.Blank
630219dfbe
fix: disable dependency metadata in APKs and App Bundles (#57) 2024-10-11 11:25:41 -04:00
Dr-Blank
781e266a5f
docs: update README for clearer instructions on joining Playstore testing 2024-10-07 14:28:51 -04:00
Dr-Blank
5b896e8b09
refactor: fade in images on home screen 2024-10-06 05:52:29 -04:00
Dr.Blank
09eafb2c28
fix: content behind miniplayer not visible (#53) 2024-10-06 05:45:14 -04:00
Dr-Blank
e8903081b7
chore: bump version to 0.0.14+5 in pubspec.yaml 2024-10-06 03:02:44 -04:00
Dr-Blank
747dbdb46f
fix: bad state when reading player for theme and new login 2024-10-05 21:02:14 -04:00
Dr-Blank
e7205ed874
chore: bump version to 0.0.13+4 in pubspec.yaml 2024-10-05 10:05:26 -04:00
Dr.Blank
ff83c2cc63
feat: multiple theming options (#50)
* refactor: consolidate theme definitions by removing separate dark and light theme files

* feat: integrate dynamic color support and enhance theme settings management

* feat: add theme settings route and update theme management in app settings

* feat: enhance theme management by integrating high contrast support in various components

* feat: implement mode selection dialog for theme settings and enhance button functionality

* refactor: update theme import paths and consolidate theme provider files

* feat: enhance theme management by integrating theme selection based on audiobook playback

* refactor: update default value for useMaterialThemeFromSystem to false in theme settings

* refactor: adjust high contrast condition order in theme settings for consistency

* refactor: rename useMaterialThemeOfPlayingItem to useCurrentPlayerThemeThroughoutApp for clarity

* refactor: correct spelling in system theme provider and replace with updated implementation

* refactor: extract restore backup dialog into a separate widget for improved readability

* refactor: reorganize settings sections for clarity and improve restore dialog functionality
2024-10-05 10:01:08 -04:00
Dr-Blank
758e4cdc83
refactor: update personalized view handling, improve GitHub issue link, and adjust VaaniLogo size 2024-10-05 04:48:52 -04:00
Dr-Blank
fa815ae206
refactor: update AuthenticatedUser model to require id and remove password, enhance server URI handling in AddNewServer widget 2024-10-04 02:31:51 -04:00
Dr-Blank
eda45efbce
chore: bump version to 0.0.12+3 in pubspec.yaml 2024-10-03 19:05:29 -04:00
Dr-Blank
33c57da78f
hotfix: add permission handling for external storage and update log file naming 2024-10-03 19:04:36 -04:00
Dr-Blank
5d2b9fd43e
chore: bump version to 0.0.11+2 in pubspec.yaml 2024-10-03 06:38:07 -04:00
Dr-Blank
2175e6d06c
refactor: formatting 2024-10-03 06:34:50 -04:00
Dr-Blank
b81473998e
fix: reset activeLibraryId on 'Try again' button press in HomePage 2024-10-03 06:33:48 -04:00
Dr.Blank
19c5385b58
fix: enable cleartext traffic in AndroidManifest.xml (#47) 2024-10-03 06:31:55 -04:00
Dr.Blank
35a2d7cfce
feat: error reporting with logs (#45)
* feat: add ability to get logs file from ui

* test: add unit test for log line parsing in logs_provider

* refactor: update all logs to obfuscate sensitive information

* feat: generate dynamic zip file name for logs export

* feat: enhance logging in audiobook player and provider for better debugging

* refactor: extract user display logic into UserBar widget for offline access of settings and logs

* feat: add About section with app metadata and source code link in YouPage
2024-10-03 05:54:29 -04:00
208 changed files with 13837 additions and 11313 deletions

3
.fvmrc Normal file
View file

@ -0,0 +1,3 @@
{
"flutter": "3.38.6"
}

View file

@ -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

View file

@ -40,7 +40,7 @@ template: |
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
exclude-labels: exclude-labels:
- "skip-changelog" - "skip changelog"
exclude-contributors: exclude-contributors:
- "Dr-Blank" - "Dr-Blank"
@ -55,15 +55,15 @@ autolabeler:
branch: branch:
- '/feature\/.+/' - '/feature\/.+/'
title: title:
- "/feat(ure)?/i" - "/^feat(ure)?/i"
body: body:
- "/JIRA-[0-9]{1,4}/" - "/JIRA-[0-9]{1,4}/"
- label: "chore" - label: "chore"
title: title:
- "/chore/i" - "/^chore\b/i"
- label: "ui" - label: "ui"
title: title:
- "/^ui\b/i" - "/^ui\b/i"
- label: "refactor" - label: "refactor"
title: title:
- "/refactor/i" - "/^refactor/i"

218
.github/workflows/flutter-ci.yaml vendored Normal file
View file

@ -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

View file

@ -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 }}

View file

@ -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

130
.github/workflows/prepare-release.yaml vendored Normal file
View file

@ -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 }}"

9
.gitignore vendored
View file

@ -30,6 +30,7 @@ migrate_working_dir/
.pub-cache/ .pub-cache/
.pub/ .pub/
/build/ /build/
dist/
# Symbolication related # Symbolication related
app.*.symbols app.*.symbols
@ -41,6 +42,10 @@ app.*.map.json
/android/app/debug /android/app/debug
/android/app/profile /android/app/profile
/android/app/release /android/app/release
/android/app/.cxx/
# separate git repo for api sdk # secret keys
/shelfsdk /secrets
# FVM Version Cache
.fvm/

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "shelfsdk"]
path = shelfsdk
url = https://github.com/Dr-Blank/shelfsdk

1
.vscode/launch.json vendored
View file

@ -7,6 +7,7 @@
{ {
"name": "vaani", "name": "vaani",
"request": "launch", "request": "launch",
"program": "lib/main.dart",
"type": "dart" "type": "dart"
}, },
{ {

28
.vscode/settings.json vendored
View file

@ -1,27 +1,35 @@
{ {
"workbench.colorCustomizations": { "cmake.configureOnOpen": false,
"activityBar.background": "#5A1021",
"titleBar.activeBackground": "#7E162E",
"titleBar.activeForeground": "#FEFBFC"
},
"files.exclude": {
"**/*.freezed.dart": true,
"**/*.g.dart": true
},
"cSpell.words": [ "cSpell.words": [
"audioplayers", "audioplayers",
"autolabeler", "autolabeler",
"Autovalidate", "Autovalidate",
"Checkmark",
"Debounceable",
"deeplinking", "deeplinking",
"fullscreen", "fullscreen",
"Lerp", "Lerp",
"miniplayer", "miniplayer",
"mocktail", "mocktail",
"nodename",
"numberpicker",
"riverpod", "riverpod",
"Schyler",
"shelfsdk", "shelfsdk",
"sysname",
"tapable", "tapable",
"unfocus", "unfocus",
"utsname",
"Vaani" "Vaani"
], ],
"cmake.configureOnOpen": false "dart.flutterSdkPath": ".fvm/versions/3.38.6",
"files.exclude": {
"**/*.freezed.dart": true,
"**/*.g.dart": true
},
"workbench.colorCustomizations": {
"activityBar.background": "#5A1021",
"titleBar.activeBackground": "#7E162E",
"titleBar.activeForeground": "#FEFBFC"
}
} }

181
CONTRIBUTING.md Normal file
View file

@ -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:
`<type>(scope): <description>`
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! 🌟

3
Gemfile Normal file
View file

@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"

221
Gemfile.lock Normal file
View file

@ -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

View file

@ -18,18 +18,24 @@ Client for [Audiobookshelf](https://github.com/advplyr/audiobookshelf) server ma
### Android ### Android
<!-- a github image with link to releases for download --> <!-- a github image with link to releases for download -->
[<img src="https://github.com/NeoApplications/Neo-Backup/raw/main/badge_github.png" alt="Get it on GitHub" height="80">](https://github.com/Dr-Blank/Vaani/releases/latest/download/app-release-universal.apk) [<img src="https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png" alt="Get it on Obtainium" height="80">](http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/Dr-Blank/Vaani) [<img src="https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png" alt="Get it on Obtainium" height="80">](http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/Dr-Blank/Vaani)
[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" alt="Get it on Google Play" height="80">](https://play.google.com/store/apps/details?id=dr.blank.vaani)
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" alt="Get it on IzzyOnDroid" height="80">](https://apt.izzysoft.de/fdroid/index/apk/dr.blank.vaani)
[<img src="https://github.com/NeoApplications/Neo-Backup/raw/main/badge_github.png" alt="Get it on GitHub" height="80">](https://github.com/Dr-Blank/Vaani/releases/latest/download/app-universal-release.apk)
Playstore App is in closed testing. To join testing *<small>Play Store version is paid if you want to support the development.</small>*
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) ### Linux
[<img src="https://img.shields.io/badge/.deb-Download-blue" alt="Download Linux (.deb)" height="30">](https://github.com/Dr-Blank/Vaani/releases/latest/download/vaani-linux-amd64.deb)
[<img src="https://img.shields.io/badge/AppImage-Download-blue" alt="Download Linux (AppImage)" height="30">](https://github.com/Dr-Blank/Vaani/releases/latest/download/vaani-linux-amd64.AppImage)
## Screencaps ## Screencaps
https://github.com/user-attachments/assets/2ac9ace2-4a3c-40fc-adde-55914e4cf62d https://github.com/user-attachments/assets/2ac9ace2-4a3c-40fc-adde-55914e4cf62d
| <img src="images/screenshots/android/home.jpg" width="200" /> | <img src="images/screenshots/android/bookview.jpg" width="200" /> | <img src="images/screenshots/android/player.jpg" width="200" /> | | <img src="images/screenshots/android/home.jpg" width="200" /> | <img src="images/screenshots/android/bookview.jpg" width="200" /> | <img src="images/screenshots/android/player.jpg" width="200" /> |
|:---:|:---:|:---:| | :-----------------------------------------------------------: | :---------------------------------------------------------------: | :-------------------------------------------------------------: |
| Home | Book View | Player | | Home | Book View | Player |
Currently, the app is in development and is not ready for production use. Currently, the app is in development and is not ready for production use.

View file

@ -25,6 +25,10 @@ linter:
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
require_trailing_commas: true require_trailing_commas: true
analyzer: analyzer:
exclude:
- '**.freezed.dart'
- '**.g.dart'
- '**.gr.dart'
errors: errors:
invalid_annotation_target: ignore invalid_annotation_target: ignore
plugins: plugins:

View file

@ -32,6 +32,10 @@ android {
namespace "dr.blank.vaani" namespace "dr.blank.vaani"
compileSdk flutter.compileSdkVersion 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 { compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
@ -46,12 +50,21 @@ android {
main.java.srcDirs += 'src/main/kotlin' 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 { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "dr.blank.vaani" applicationId "dr.blank.vaani"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 23 minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
@ -80,3 +93,11 @@ flutter {
} }
dependencies {} 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"
}
}

View file

@ -7,9 +7,13 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<application <application
android:label="Vaani" android:label="Vaani"
android:name="${applicationName}" android:name="${applicationName}"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<!-- android:name=".MainActivity" --> <!-- android:name=".MainActivity" -->
<activity <activity

View file

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip

View file

@ -19,8 +19,8 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false id "com.android.application" version '8.10.0' apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false id "org.jetbrains.kotlin.android" version "2.1.10" apply false
} }
include ":app" include ":app"

BIN
assets/fonts/AbsIcons.ttf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

8
distribute_options.yaml Normal file
View file

@ -0,0 +1,8 @@
output: dist/
releases:
- name: dev
jobs:
- name: release-dev-linux-deb
package:
platform: linux
target: deb

8
docs/images_and_logos.md Normal file
View file

@ -0,0 +1,8 @@
this is how i converted my png to svg
`convert -background White vaani_logo.png vaani_logo.pbm`
`potrace -b svg -i vaani_logo.pbm -o vaani_logo.svg`
`-i` flag was needed so that it took white as the svgs and black as background

45
docs/linux_build_guide.md Normal file
View file

@ -0,0 +1,45 @@
# Linux Build Guide
## Determining Package Size
To determine the installed size for your Linux package configuration, you can use the following script:
```bash
#!/bin/bash
# Build the Linux app
flutter build linux
# Get size in KB and add 17% buffer for runtime dependencies
SIZE_KB=$(du -sk build/linux/x64/release/bundle | cut -f1)
BUFFER_SIZE_KB=$(($SIZE_KB + ($SIZE_KB * 17 / 100)))
echo "Actual bundle size: $SIZE_KB KB"
echo "Recommended installed_size (with 17% buffer): $BUFFER_SIZE_KB KB"
```
Save this as `get_package_size.sh` in your project root and make it executable:
```bash
chmod +x get_package_size.sh
```
### Usage
1. Run the script:
```bash
./get_package_size.sh
```
2. Use the output value for `installed_size` in your `linux/packaging/deb/make_config.yaml` file:
```yaml
installed_size: 75700 # Replace with the value from the script
```
### Why add a buffer?
The 17% buffer is added to account for:
- Runtime dependencies
- Future updates
- Potential additional assets
- Prevent installation issues on systems with limited space
### Notes
- The installed size should be specified in kilobytes (KB)
- Always round up the buffer size to be safe
- Re-run this script after significant changes to your app (new assets, dependencies, etc.)

2
docs/linux_deeplink.md Normal file
View file

@ -0,0 +1,2 @@
to test deeplink
`xdg-open vaani://test?code=123&state=abc`

2
fastlane/Appfile Normal file
View file

@ -0,0 +1,2 @@
json_key_file("./secrets/play-store-credentials.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
package_name("dr.blank.vaani") # e.g. com.krausefx.app

38
fastlane/Fastfile Normal file
View file

@ -0,0 +1,38 @@
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
# https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
# https://docs.fastlane.tools/plugins/available-plugins
#
# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane
default_platform(:android)
platform :android do
desc "Runs all the tests"
lane :test do
gradle(task: "test", project_dir: 'android/')
end
desc "Submit a new Beta Build to Crashlytics Beta"
lane :beta do
gradle(task: "clean assembleRelease", project_dir: 'android/')
crashlytics
# sh "your_script.sh"
# You can also use other beta testing services here
end
desc "Deploy a new version to the Google Play"
lane :deploy do
gradle(task: "clean assembleRelease", project_dir: 'android/')
upload_to_play_store
end
end

48
fastlane/README.md Normal file
View file

@ -0,0 +1,48 @@
fastlane documentation
----
# Installation
Make sure you have the latest version of the Xcode command line tools installed:
```sh
xcode-select --install
```
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
# Available Actions
## Android
### android test
```sh
[bundle exec] fastlane android test
```
Runs all the tests
### android beta
```sh
[bundle exec] fastlane android beta
```
Submit a new Beta Build to Crashlytics Beta
### android deploy
```sh
[bundle exec] fastlane android deploy
```
Deploy a new version to the Google Play
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).

View file

@ -0,0 +1,10 @@
<i>Vaani</i> is a client for your (self-hosted) <a href='https://github.com/advplyr/audiobookshelf'>Audiobookshelf</a> server.
<b>Features:</b>
- 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 KiB

View file

@ -0,0 +1 @@
Beautiful, Fast and Functional Audiobook Player for your Audiobookshelf server.

View file

@ -0,0 +1 @@
Vaani

18
fastlane/report.xml Normal file
View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="fastlane.lanes">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.0039338">
</testcase>
<testcase classname="fastlane.lanes" name="1: test" time="10.3552991">
</testcase>
</testsuite>
</testsuites>

36
images/vaani_logo.svg Normal file
View file

@ -0,0 +1,36 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="192.000000pt" height="192.000000pt"
viewBox="0 0 192.000000 192.000000" preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.16, written by Peter Selinger 2001-2019
</metadata>
<g transform="translate(0.000000,192.000000) scale(0.100000,-0.100000)" fill="#ffffff" stroke="none">
<path d="M602 1678 c-18 -18 -17 -689 1 -741 17 -48 30 -61 55 -53 25 8 38 57
23 85 -6 11 -11 75 -11 144 0 284 -13 548 -27 563 -18 17 -25 18 -41 2z" />
<path d="M897 1683 c-4 -3 -7 -176 -7 -383 l0 -377 23 -22 c30 -28 53 -19 67
25 10 29 9 39 -4 59 -14 21 -16 69 -16 353 0 242 -3 331 -12 340 -13 13 -41
16 -51 5z" />
<path d="M1329 1668 c-8 -15 -10 -104 -7 -311 4 -288 4 -289 27 -308 16 -13
28 -16 39 -10 13 8 15 46 13 312 -1 169 -7 311 -12 321 -13 25 -46 23 -60 -4z" />
<path d="M1035 1568 c-3 -7 -6 -123 -8 -258 l-2 -245 27 -3 c36 -4 36 -5 43
266 7 234 4 252 -37 252 -10 0 -21 -6 -23 -12z" />
<path d="M1474 1467 c-2 -7 -3 -60 -2 -118 3 -100 4 -104 26 -107 42 -6 49 10
45 105 -5 123 -8 133 -39 133 -13 0 -27 -6 -30 -13z" />
<path d="M443 1408 c-13 -23 -3 -251 13 -275 28 -45 88 -19 64 27 -6 10 -10
64 -10 120 0 56 -5 110 -10 121 -12 21 -45 25 -57 7z" />
<path d="M777 1407 c-12 -9 -16 -39 -19 -132 l-3 -120 30 0 c37 0 43 18 44
147 1 85 -1 99 -18 107 -13 7 -23 7 -34 -2z" />
<path d="M1182 1398 c-8 -8 -12 -48 -12 -115 0 -93 2 -103 21 -113 39 -21 49
2 49 111 0 75 -4 101 -16 113 -19 19 -26 20 -42 4z" />
<path d="M1505 1048 c-11 -6 -25 -14 -31 -17 -56 -30 -192 -131 -223 -164 -57
-62 -134 -166 -158 -214 -12 -23 -26 -50 -32 -60 -9 -17 -30 -66 -60 -138
l-12 -29 -21 34 c-114 189 -151 238 -243 316 -44 38 -107 95 -141 127 -33 31
-66 57 -73 57 -8 0 -30 8 -50 19 -20 10 -55 27 -79 37 -39 16 -44 16 -58 2
-29 -29 -8 -52 84 -93 51 -23 106 -57 132 -81 25 -23 65 -58 90 -78 59 -49
152 -147 186 -199 15 -23 35 -53 44 -67 10 -14 35 -63 56 -108 28 -61 45 -84
61 -88 28 -7 49 8 57 41 8 33 106 246 128 279 7 11 27 40 43 66 51 79 135 167
192 202 30 18 67 42 81 53 15 11 44 29 65 39 31 16 37 24 35 45 -3 29 -38 38
-73 19z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

@ -2,20 +2,22 @@
import 'dart:convert'; import 'dart:convert';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/db/cache_manager.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/api_settings_provider.dart';
import 'package:vaani/settings/models/authenticated_user.dart';
import 'package:vaani/shared/extensions/obfuscation.dart';
part 'api_provider.g.dart'; part 'api_provider.g.dart';
// TODO: workaround for https://github.com/rrousselGit/riverpod/issues/3718 // TODO: workaround for https://github.com/rrousselGit/riverpod/issues/3718
typedef ResponseErrorHandler = void Function( typedef ResponseErrorHandler =
Response response, [ void Function(Response response, [Object? error]);
Object? error,
]);
final _logger = Logger('api_provider'); final _logger = Logger('api_provider');
@ -31,23 +33,21 @@ Uri makeBaseUrl(String address) {
/// get the api instance for the given base url /// get the api instance for the given base url
@riverpod @riverpod
AudiobookshelfApi audiobookshelfApi(AudiobookshelfApiRef ref, Uri? baseUrl) { AudiobookshelfApi audiobookshelfApi(Ref ref, Uri? baseUrl) {
// try to get the base url from app settings // try to get the base url from app settings
final apiSettings = ref.watch(apiSettingsProvider); final apiSettings = ref.watch(apiSettingsProvider);
baseUrl ??= apiSettings.activeServer?.serverUrl; baseUrl ??= apiSettings.activeServer?.serverUrl;
return AudiobookshelfApi( return AudiobookshelfApi(baseUrl: makeBaseUrl(baseUrl.toString()));
baseUrl: makeBaseUrl(baseUrl.toString()),
);
} }
/// get the api instance for the authenticated user /// get the api instance for the authenticated user
/// ///
/// if the user is not authenticated throw an error /// if the user is not authenticated throw an error
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) { AudiobookshelfApi authenticatedApi(Ref ref) {
final apiSettings = ref.watch(apiSettingsProvider); final user = ref.watch(apiSettingsProvider.select((s) => s.activeUser));
final user = apiSettings.activeUser;
if (user == null) { if (user == null) {
_logger.severe('No active user can not provide authenticated api');
throw StateError('No active user'); throw StateError('No active user');
} }
return AudiobookshelfApi( return AudiobookshelfApi(
@ -58,15 +58,15 @@ AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) {
/// ping the server to check if it is reachable /// ping the server to check if it is reachable
@riverpod @riverpod
FutureOr<bool> isServerAlive(IsServerAliveRef ref, String address) async { FutureOr<bool> isServerAlive(Ref ref, String address) async {
if (address.isEmpty) { if (address.isEmpty) {
return false; return false;
} }
try { try {
return await AudiobookshelfApi(baseUrl: makeBaseUrl(address)) return await AudiobookshelfApi(
.server baseUrl: makeBaseUrl(address),
.ping() ?? ).server.ping() ??
false; false;
} catch (e) { } catch (e) {
return false; return false;
@ -76,14 +76,15 @@ FutureOr<bool> isServerAlive(IsServerAliveRef ref, String address) async {
/// fetch status of server /// fetch status of server
@riverpod @riverpod
FutureOr<ServerStatusResponse?> serverStatus( FutureOr<ServerStatusResponse?> serverStatus(
ServerStatusRef ref, Ref ref,
Uri baseUrl, [ Uri baseUrl, [
ResponseErrorHandler? responseErrorHandler, ResponseErrorHandler? responseErrorHandler,
]) async { ]) async {
_logger.fine('fetching server status: $baseUrl'); _logger.fine('fetching server status: ${baseUrl.obfuscate()}');
final api = ref.watch(audiobookshelfApiProvider(baseUrl)); final api = ref.watch(audiobookshelfApiProvider(baseUrl));
final res = final res = await api.server.status(
await api.server.status(responseErrorHandler: responseErrorHandler); responseErrorHandler: responseErrorHandler,
);
_logger.fine('server status: $res'); _logger.fine('server status: $res');
return res; return res;
} }
@ -96,20 +97,32 @@ class PersonalizedView extends _$PersonalizedView {
final api = ref.watch(authenticatedApiProvider); final api = ref.watch(authenticatedApiProvider);
final apiSettings = ref.watch(apiSettingsProvider); final apiSettings = ref.watch(apiSettingsProvider);
final user = apiSettings.activeUser; final user = apiSettings.activeUser;
if (user == null) {
_logger.warning('no active user');
yield [];
return;
}
if (apiSettings.activeLibraryId == null) { if (apiSettings.activeLibraryId == null) {
// set it to default user library by logging in and getting the library id // set it to default user library by logging in and getting the library id
final login = final login = await ref.read(loginProvider().future);
await api.login(username: user!.username!, password: user.password!); if (login == null) {
ref.read(apiSettingsProvider.notifier).updateState( _logger.shout('failed to login, not building personalized view');
apiSettings.copyWith(activeLibraryId: login!.userDefaultLibraryId), yield [];
return;
}
ref
.read(apiSettingsProvider.notifier)
.updateState(
apiSettings.copyWith(activeLibraryId: login.userDefaultLibraryId),
); );
yield [];
return;
} }
// try to find in cache // try to find in cache
// final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}'; // final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}';
var key = 'personalizedView:${apiSettings.activeLibraryId! + user!.id!}'; final key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}';
final cachedRes = await apiResponseCacheManager.getFileFromMemory( final cachedRes =
key, await apiResponseCacheManager.getFileFromMemory(key) ??
) ??
await apiResponseCacheManager.getFileFromCache(key); await apiResponseCacheManager.getFileFromCache(key);
if (cachedRes != null) { if (cachedRes != null) {
_logger.fine('reading from cache: $cachedRes for key: $key'); _logger.fine('reading from cache: $cachedRes for key: $key');
@ -126,10 +139,11 @@ class PersonalizedView extends _$PersonalizedView {
} }
} }
// ! exagerated delay // ! exaggerated delay
// await Future.delayed(const Duration(seconds: 2)); // await Future.delayed(const Duration(seconds: 2));
final res = await api.libraries final res = await api.libraries.getPersonalized(
.getPersonalized(libraryId: apiSettings.activeLibraryId!); libraryId: apiSettings.activeLibraryId!,
);
// debugPrint('personalizedView: ${res!.map((e) => e).toSet()}'); // debugPrint('personalizedView: ${res!.map((e) => e).toSet()}');
// save to cache // save to cache
if (res != null) { if (res != null) {
@ -145,21 +159,19 @@ class PersonalizedView extends _$PersonalizedView {
_logger.warning('failed to fetch personalized view'); _logger.warning('failed to fetch personalized view');
yield []; yield [];
} }
} }
// method to force refresh the view and ignore the cache // method to force refresh the view and ignore the cache
Future<void> forceRefresh() async { Future<void> forceRefresh() async {
// clear the cache // clear the cache
// TODO: find a better way to clear the cache for only personalized view key
return apiResponseCacheManager.emptyCache(); return apiResponseCacheManager.emptyCache();
} }
} }
/// fetch continue listening audiobooks /// fetch continue listening audiobooks
@riverpod @riverpod
FutureOr<GetUserSessionsResponse> fetchContinueListening( FutureOr<GetUserSessionsResponse> fetchContinueListening(Ref ref) async {
FetchContinueListeningRef ref,
) async {
final api = ref.watch(authenticatedApiProvider); final api = ref.watch(authenticatedApiProvider);
final res = await api.me.getSessions(); final res = await api.me.getSessions();
// debugPrint( // debugPrint(
@ -169,10 +181,46 @@ FutureOr<GetUserSessionsResponse> fetchContinueListening(
} }
@riverpod @riverpod
FutureOr<User> me( FutureOr<User> me(Ref ref) async {
MeRef ref,
) async {
final api = ref.watch(authenticatedApiProvider); final api = ref.watch(authenticatedApiProvider);
final res = await api.me.getUser(); final errorResponseHandler = ErrorResponseHandler();
return res!; 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<LoginResponse?> 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;
} }

View file

@ -6,538 +6,536 @@ part of 'api_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$audiobookshelfApiHash() => r'2c310ea77fea9918ccf96180a92075acd037bd95'; // GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// get the api instance for the given base url /// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
@ProviderFor(audiobookshelfApi) @ProviderFor(audiobookshelfApi)
const audiobookshelfApiProvider = AudiobookshelfApiFamily(); final audiobookshelfApiProvider = AudiobookshelfApiFamily._();
/// get the api instance for the given base url /// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi]. final class AudiobookshelfApiProvider
class AudiobookshelfApiFamily extends Family<AudiobookshelfApi> { extends
$FunctionalProvider<
AudiobookshelfApi,
AudiobookshelfApi,
AudiobookshelfApi
>
with $Provider<AudiobookshelfApi> {
/// get the api instance for the given base url /// get the api instance for the given base url
/// AudiobookshelfApiProvider._({
/// Copied from [audiobookshelfApi]. required AudiobookshelfApiFamily super.from,
const AudiobookshelfApiFamily(); required Uri? super.argument,
}) : super(
/// get the api instance for the given base url retry: null,
///
/// Copied from [audiobookshelfApi].
AudiobookshelfApiProvider call(
Uri? baseUrl,
) {
return AudiobookshelfApiProvider(
baseUrl,
);
}
@override
AudiobookshelfApiProvider getProviderOverride(
covariant AudiobookshelfApiProvider provider,
) {
return call(
provider.baseUrl,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'audiobookshelfApiProvider';
}
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
class AudiobookshelfApiProvider extends AutoDisposeProvider<AudiobookshelfApi> {
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
AudiobookshelfApiProvider(
Uri? baseUrl,
) : this._internal(
(ref) => audiobookshelfApi(
ref as AudiobookshelfApiRef,
baseUrl,
),
from: audiobookshelfApiProvider,
name: r'audiobookshelfApiProvider', name: r'audiobookshelfApiProvider',
debugGetCreateSourceHash: isAutoDispose: true,
const bool.fromEnvironment('dart.vm.product') dependencies: null,
? null $allTransitiveDependencies: null,
: _$audiobookshelfApiHash,
dependencies: AudiobookshelfApiFamily._dependencies,
allTransitiveDependencies:
AudiobookshelfApiFamily._allTransitiveDependencies,
baseUrl: baseUrl,
); );
AudiobookshelfApiProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.baseUrl,
}) : super.internal();
final Uri? baseUrl;
@override @override
Override overrideWith( String debugGetCreateSourceHash() => _$audiobookshelfApiHash();
AudiobookshelfApi Function(AudiobookshelfApiRef provider) create,
) { @override
return ProviderOverride( String toString() {
origin: this, return r'audiobookshelfApiProvider'
override: AudiobookshelfApiProvider._internal( ''
(ref) => create(ref as AudiobookshelfApiRef), '($argument)';
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
baseUrl: baseUrl,
),
);
} }
@$internal
@override @override
AutoDisposeProviderElement<AudiobookshelfApi> createElement() { $ProviderElement<AudiobookshelfApi> $createElement(
return _AudiobookshelfApiProviderElement(this); $ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
AudiobookshelfApi create(Ref ref) {
final argument = this.argument as Uri?;
return audiobookshelfApi(ref, argument);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AudiobookshelfApi value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AudiobookshelfApi>(value),
);
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is AudiobookshelfApiProvider && other.baseUrl == baseUrl; return other is AudiobookshelfApiProvider && other.argument == argument;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); return argument.hashCode;
hash = _SystemHash.combine(hash, baseUrl.hashCode);
return _SystemHash.finish(hash);
} }
} }
mixin AudiobookshelfApiRef on AutoDisposeProviderRef<AudiobookshelfApi> { String _$audiobookshelfApiHash() => r'f23a06c404e11867a7f796877eaca99b8ff25458';
/// The parameter `baseUrl` of this provider.
Uri? get baseUrl;
}
class _AudiobookshelfApiProviderElement /// get the api instance for the given base url
extends AutoDisposeProviderElement<AudiobookshelfApi>
with AudiobookshelfApiRef { final class AudiobookshelfApiFamily extends $Family
_AudiobookshelfApiProviderElement(super.provider); with $FunctionalFamilyOverride<AudiobookshelfApi, Uri?> {
AudiobookshelfApiFamily._()
: super(
retry: null,
name: r'audiobookshelfApiProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// get the api instance for the given base url
AudiobookshelfApiProvider call(Uri? baseUrl) =>
AudiobookshelfApiProvider._(argument: baseUrl, from: this);
@override @override
Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl; String toString() => r'audiobookshelfApiProvider';
} }
String _$authenticatedApiHash() => r'f555efb6eede590b5a8d60cad2e6bfc2847e2d14';
/// get the api instance for the authenticated user /// get the api instance for the authenticated user
/// ///
/// if the user is not authenticated throw an error /// if the user is not authenticated throw an error
///
/// Copied from [authenticatedApi].
@ProviderFor(authenticatedApi) @ProviderFor(authenticatedApi)
final authenticatedApiProvider = Provider<AudiobookshelfApi>.internal( final authenticatedApiProvider = AuthenticatedApiProvider._();
authenticatedApi,
/// get the api instance for the authenticated user
///
/// if the user is not authenticated throw an error
final class AuthenticatedApiProvider
extends
$FunctionalProvider<
AudiobookshelfApi,
AudiobookshelfApi,
AudiobookshelfApi
>
with $Provider<AudiobookshelfApi> {
/// get the api instance for the authenticated user
///
/// if the user is not authenticated throw an error
AuthenticatedApiProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'authenticatedApiProvider', name: r'authenticatedApiProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') isAutoDispose: false,
? null
: _$authenticatedApiHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, $allTransitiveDependencies: null,
); );
typedef AuthenticatedApiRef = ProviderRef<AudiobookshelfApi>; @override
String _$isServerAliveHash() => r'6ff90b6e0febd2cd4a4d3a5209a59afc778cd3b6'; String debugGetCreateSourceHash() => _$authenticatedApiHash();
/// ping the server to check if it is reachable @$internal
/// @override
/// Copied from [isServerAlive]. $ProviderElement<AudiobookshelfApi> $createElement(
@ProviderFor(isServerAlive) $ProviderPointer pointer,
const isServerAliveProvider = IsServerAliveFamily(); ) => $ProviderElement(pointer);
/// ping the server to check if it is reachable @override
/// AudiobookshelfApi create(Ref ref) {
/// Copied from [isServerAlive]. return authenticatedApi(ref);
class IsServerAliveFamily extends Family<AsyncValue<bool>> {
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
const IsServerAliveFamily();
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
IsServerAliveProvider call(
String address,
) {
return IsServerAliveProvider(
address,
);
} }
@override /// {@macro riverpod.override_with_value}
IsServerAliveProvider getProviderOverride( Override overrideWithValue(AudiobookshelfApi value) {
covariant IsServerAliveProvider provider, return $ProviderOverride(
) {
return call(
provider.address,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'isServerAliveProvider';
}
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
class IsServerAliveProvider extends AutoDisposeFutureProvider<bool> {
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
IsServerAliveProvider(
String address,
) : this._internal(
(ref) => isServerAlive(
ref as IsServerAliveRef,
address,
),
from: isServerAliveProvider,
name: r'isServerAliveProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$isServerAliveHash,
dependencies: IsServerAliveFamily._dependencies,
allTransitiveDependencies:
IsServerAliveFamily._allTransitiveDependencies,
address: address,
);
IsServerAliveProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.address,
}) : super.internal();
final String address;
@override
Override overrideWith(
FutureOr<bool> Function(IsServerAliveRef provider) create,
) {
return ProviderOverride(
origin: this, origin: this,
override: IsServerAliveProvider._internal( providerOverride: $SyncValueProvider<AudiobookshelfApi>(value),
(ref) => create(ref as IsServerAliveRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
address: address,
),
); );
} }
}
String _$authenticatedApiHash() => r'284be2c39823c20fb70035a136c430862c28fa27';
/// ping the server to check if it is reachable
@ProviderFor(isServerAlive)
final isServerAliveProvider = IsServerAliveFamily._();
/// ping the server to check if it is reachable
final class IsServerAliveProvider
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>>
with $FutureModifier<bool>, $FutureProvider<bool> {
/// ping the server to check if it is reachable
IsServerAliveProvider._({
required IsServerAliveFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'isServerAliveProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override @override
AutoDisposeFutureProviderElement<bool> createElement() { String debugGetCreateSourceHash() => _$isServerAliveHash();
return _IsServerAliveProviderElement(this);
@override
String toString() {
return r'isServerAliveProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<bool> create(Ref ref) {
final argument = this.argument as String;
return isServerAlive(ref, argument);
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is IsServerAliveProvider && other.address == address; return other is IsServerAliveProvider && other.argument == argument;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); return argument.hashCode;
hash = _SystemHash.combine(hash, address.hashCode);
return _SystemHash.finish(hash);
} }
} }
mixin IsServerAliveRef on AutoDisposeFutureProviderRef<bool> { String _$isServerAliveHash() => r'bb3a53cae1eb64b8760a56864feed47b7a3f1c29';
/// The parameter `address` of this provider.
String get address;
}
class _IsServerAliveProviderElement /// ping the server to check if it is reachable
extends AutoDisposeFutureProviderElement<bool> with IsServerAliveRef {
_IsServerAliveProviderElement(super.provider); final class IsServerAliveFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<bool>, String> {
IsServerAliveFamily._()
: super(
retry: null,
name: r'isServerAliveProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// ping the server to check if it is reachable
IsServerAliveProvider call(String address) =>
IsServerAliveProvider._(argument: address, from: this);
@override @override
String get address => (origin as IsServerAliveProvider).address; String toString() => r'isServerAliveProvider';
} }
String _$serverStatusHash() => r'2739906a1862d09b098588ebd16749a09032ee99';
/// fetch status of server /// fetch status of server
///
/// Copied from [serverStatus].
@ProviderFor(serverStatus) @ProviderFor(serverStatus)
const serverStatusProvider = ServerStatusFamily(); final serverStatusProvider = ServerStatusFamily._();
/// fetch status of server /// fetch status of server
///
/// Copied from [serverStatus]. final class ServerStatusProvider
class ServerStatusFamily extends Family<AsyncValue<ServerStatusResponse?>> { extends
$FunctionalProvider<
AsyncValue<ServerStatusResponse?>,
ServerStatusResponse?,
FutureOr<ServerStatusResponse?>
>
with
$FutureModifier<ServerStatusResponse?>,
$FutureProvider<ServerStatusResponse?> {
/// fetch status of server /// fetch status of server
/// ServerStatusProvider._({
/// Copied from [serverStatus]. required ServerStatusFamily super.from,
const ServerStatusFamily(); required (Uri, ResponseErrorHandler?) super.argument,
}) : super(
retry: null,
name: r'serverStatusProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$serverStatusHash();
@override
String toString() {
return r'serverStatusProvider'
''
'$argument';
}
@$internal
@override
$FutureProviderElement<ServerStatusResponse?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<ServerStatusResponse?> create(Ref ref) {
final argument = this.argument as (Uri, ResponseErrorHandler?);
return serverStatus(ref, argument.$1, argument.$2);
}
@override
bool operator ==(Object other) {
return other is ServerStatusProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$serverStatusHash() => r'2d9c5d6f970caec555e5322d43a388ea8572619f';
/// fetch status of server /// fetch status of server
///
/// Copied from [serverStatus]. final class ServerStatusFamily extends $Family
with
$FunctionalFamilyOverride<
FutureOr<ServerStatusResponse?>,
(Uri, ResponseErrorHandler?)
> {
ServerStatusFamily._()
: super(
retry: null,
name: r'serverStatusProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// fetch status of server
ServerStatusProvider call( ServerStatusProvider call(
Uri baseUrl, [ Uri baseUrl, [
void Function(Response, [Object?])? responseErrorHandler, ResponseErrorHandler? responseErrorHandler,
]) { ]) => ServerStatusProvider._(
return ServerStatusProvider( argument: (baseUrl, responseErrorHandler),
baseUrl, from: this,
responseErrorHandler,
);
}
@override
ServerStatusProvider getProviderOverride(
covariant ServerStatusProvider provider,
) {
return call(
provider.baseUrl,
provider.responseErrorHandler,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'serverStatusProvider';
}
/// fetch status of server
///
/// Copied from [serverStatus].
class ServerStatusProvider
extends AutoDisposeFutureProvider<ServerStatusResponse?> {
/// fetch status of server
///
/// Copied from [serverStatus].
ServerStatusProvider(
Uri baseUrl, [
void Function(Response, [Object?])? responseErrorHandler,
]) : this._internal(
(ref) => serverStatus(
ref as ServerStatusRef,
baseUrl,
responseErrorHandler,
),
from: serverStatusProvider,
name: r'serverStatusProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$serverStatusHash,
dependencies: ServerStatusFamily._dependencies,
allTransitiveDependencies:
ServerStatusFamily._allTransitiveDependencies,
baseUrl: baseUrl,
responseErrorHandler: responseErrorHandler,
); );
ServerStatusProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.baseUrl,
required this.responseErrorHandler,
}) : super.internal();
final Uri baseUrl;
final void Function(Response, [Object?])? responseErrorHandler;
@override @override
Override overrideWith( String toString() => r'serverStatusProvider';
FutureOr<ServerStatusResponse?> Function(ServerStatusRef provider) create, }
) {
return ProviderOverride( /// fetch the personalized view
origin: this,
override: ServerStatusProvider._internal( @ProviderFor(PersonalizedView)
(ref) => create(ref as ServerStatusRef), final personalizedViewProvider = PersonalizedViewProvider._();
from: from,
name: null, /// fetch the personalized view
final class PersonalizedViewProvider
extends $StreamNotifierProvider<PersonalizedView, List<Shelf>> {
/// fetch the personalized view
PersonalizedViewProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'personalizedViewProvider',
isAutoDispose: true,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, $allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
baseUrl: baseUrl,
responseErrorHandler: responseErrorHandler,
),
); );
}
@override @override
AutoDisposeFutureProviderElement<ServerStatusResponse?> createElement() { String debugGetCreateSourceHash() => _$personalizedViewHash();
return _ServerStatusProviderElement(this);
@$internal
@override
PersonalizedView create() => PersonalizedView();
} }
String _$personalizedViewHash() => r'425e89d99d7e4712b4d6a688f3a12442bd66584f';
/// fetch the personalized view
abstract class _$PersonalizedView extends $StreamNotifier<List<Shelf>> {
Stream<List<Shelf>> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<List<Shelf>>, List<Shelf>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<Shelf>>, List<Shelf>>,
AsyncValue<List<Shelf>>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
/// fetch continue listening audiobooks
@ProviderFor(fetchContinueListening)
final fetchContinueListeningProvider = FetchContinueListeningProvider._();
/// fetch continue listening audiobooks
final class FetchContinueListeningProvider
extends
$FunctionalProvider<
AsyncValue<GetUserSessionsResponse>,
GetUserSessionsResponse,
FutureOr<GetUserSessionsResponse>
>
with
$FutureModifier<GetUserSessionsResponse>,
$FutureProvider<GetUserSessionsResponse> {
/// fetch continue listening audiobooks
FetchContinueListeningProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'fetchContinueListeningProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override @override
bool operator ==(Object other) { String debugGetCreateSourceHash() => _$fetchContinueListeningHash();
return other is ServerStatusProvider &&
other.baseUrl == baseUrl && @$internal
other.responseErrorHandler == responseErrorHandler; @override
} $FutureProviderElement<GetUserSessionsResponse> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override @override
int get hashCode { FutureOr<GetUserSessionsResponse> create(Ref ref) {
var hash = _SystemHash.combine(0, runtimeType.hashCode); return fetchContinueListening(ref);
hash = _SystemHash.combine(hash, baseUrl.hashCode);
hash = _SystemHash.combine(hash, responseErrorHandler.hashCode);
return _SystemHash.finish(hash);
} }
} }
mixin ServerStatusRef on AutoDisposeFutureProviderRef<ServerStatusResponse?> {
/// The parameter `baseUrl` of this provider.
Uri get baseUrl;
/// The parameter `responseErrorHandler` of this provider.
void Function(Response, [Object?])? get responseErrorHandler;
}
class _ServerStatusProviderElement
extends AutoDisposeFutureProviderElement<ServerStatusResponse?>
with ServerStatusRef {
_ServerStatusProviderElement(super.provider);
@override
Uri get baseUrl => (origin as ServerStatusProvider).baseUrl;
@override
void Function(Response, [Object?])? get responseErrorHandler =>
(origin as ServerStatusProvider).responseErrorHandler;
}
String _$fetchContinueListeningHash() => String _$fetchContinueListeningHash() =>
r'f65fe3ac3a31b8ac074330525c5d2cc4b526802d'; r'50aeb77369eda38d496b2f56f3df2aea135dab45';
/// fetch continue listening audiobooks
///
/// Copied from [fetchContinueListening].
@ProviderFor(fetchContinueListening)
final fetchContinueListeningProvider =
AutoDisposeFutureProvider<GetUserSessionsResponse>.internal(
fetchContinueListening,
name: r'fetchContinueListeningProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$fetchContinueListeningHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef FetchContinueListeningRef
= AutoDisposeFutureProviderRef<GetUserSessionsResponse>;
String _$meHash() => r'bdc664c4fd867ad13018fa769ce7a6913248c44f';
/// See also [me].
@ProviderFor(me) @ProviderFor(me)
final meProvider = AutoDisposeFutureProvider<User>.internal( final meProvider = MeProvider._();
me,
final class MeProvider
extends $FunctionalProvider<AsyncValue<User>, User, FutureOr<User>>
with $FutureModifier<User>, $FutureProvider<User> {
MeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'meProvider', name: r'meProvider',
debugGetCreateSourceHash: isAutoDispose: true,
const bool.fromEnvironment('dart.vm.product') ? null : _$meHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, $allTransitiveDependencies: null,
); );
typedef MeRef = AutoDisposeFutureProviderRef<User>; @override
String _$personalizedViewHash() => r'4c392ece4650bdc36d7195a0ddb8810e8fe4caa9'; String debugGetCreateSourceHash() => _$meHash();
/// fetch the personalized view @$internal
/// @override
/// Copied from [PersonalizedView]. $FutureProviderElement<User> $createElement($ProviderPointer pointer) =>
@ProviderFor(PersonalizedView) $FutureProviderElement(pointer);
final personalizedViewProvider =
AutoDisposeStreamNotifierProvider<PersonalizedView, List<Shelf>>.internal( @override
PersonalizedView.new, FutureOr<User> create(Ref ref) {
name: r'personalizedViewProvider', return me(ref);
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') }
? null }
: _$personalizedViewHash,
String _$meHash() => r'b3b6d6d940b465c60d0c29cd6e81ba2fcccab186';
@ProviderFor(login)
final loginProvider = LoginFamily._();
final class LoginProvider
extends
$FunctionalProvider<
AsyncValue<LoginResponse?>,
LoginResponse?,
FutureOr<LoginResponse?>
>
with $FutureModifier<LoginResponse?>, $FutureProvider<LoginResponse?> {
LoginProvider._({
required LoginFamily super.from,
required AuthenticatedUser? super.argument,
}) : super(
retry: null,
name: r'loginProvider',
isAutoDispose: true,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, $allTransitiveDependencies: null,
); );
typedef _$PersonalizedView = AutoDisposeStreamNotifier<List<Shelf>>; @override
// ignore_for_file: type=lint String debugGetCreateSourceHash() => _$loginHash();
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@override
String toString() {
return r'loginProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<LoginResponse?> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<LoginResponse?> create(Ref ref) {
final argument = this.argument as AuthenticatedUser?;
return login(ref, user: argument);
}
@override
bool operator ==(Object other) {
return other is LoginProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$loginHash() => r'99410c2bed9c8f412c7b47c4e655db64e0054be2';
final class LoginFamily extends $Family
with
$FunctionalFamilyOverride<
FutureOr<LoginResponse?>,
AuthenticatedUser?
> {
LoginFamily._()
: super(
retry: null,
name: r'loginProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
LoginProvider call({AuthenticatedUser? user}) =>
LoginProvider._(argument: user, from: this);
@override
String toString() => r'loginProvider';
}

View file

@ -1,28 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'authenticated_user_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$authenticatedUserHash() => r'308f19b33ae04af6340fb83167fa64aa23400a09';
/// provides with a set of authenticated users
///
/// Copied from [AuthenticatedUser].
@ProviderFor(AuthenticatedUser)
final authenticatedUserProvider = AutoDisposeNotifierProvider<AuthenticatedUser,
Set<model.AuthenticatedUser>>.internal(
AuthenticatedUser.new,
name: r'authenticatedUserProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$authenticatedUserHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$AuthenticatedUser = AutoDisposeNotifier<Set<model.AuthenticatedUser>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -5,21 +5,21 @@ import 'package:vaani/api/server_provider.dart'
import 'package:vaani/db/storage.dart'; import 'package:vaani/db/storage.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/audiobookshelf_server.dart'; import 'package:vaani/settings/models/audiobookshelf_server.dart';
import 'package:vaani/settings/models/authenticated_user.dart' import 'package:vaani/settings/models/authenticated_user.dart' as model;
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 _box = AvailableHiveBoxes.authenticatedUserBox;
final _logger = Logger('authenticated_user_provider'); final _logger = Logger('authenticated_users_provider');
/// provides with a set of authenticated users /// provides with a set of authenticated users
@riverpod @riverpod
class AuthenticatedUser extends _$AuthenticatedUser { class AuthenticatedUsers extends _$AuthenticatedUsers {
@override @override
Set<model.AuthenticatedUser> build() { Set<model.AuthenticatedUser> build() {
ref.listenSelf((_, __) { listenSelf((_, __) {
writeStateToBox(); writeStateToBox();
}); });
// get the app settings // get the app settings
@ -35,7 +35,7 @@ class AuthenticatedUser extends _$AuthenticatedUser {
Set<model.AuthenticatedUser> readFromBoxOrCreate() { Set<model.AuthenticatedUser> readFromBoxOrCreate() {
if (_box.isNotEmpty) { if (_box.isNotEmpty) {
final foundData = _box.getRange(0, _box.length); final foundData = _box.getRange(0, _box.length);
_logger.fine('found users in box: $foundData'); _logger.fine('found users in box: ${foundData.obfuscate()}');
return foundData.toSet(); return foundData.toSet();
} else { } else {
_logger.fine('no settings found in box'); _logger.fine('no settings found in box');
@ -49,18 +49,17 @@ class AuthenticatedUser extends _$AuthenticatedUser {
return; return;
} }
_box.addAll(state); _box.addAll(state);
_logger.fine('writing state to box: $state'); _logger.fine('writing state to box: ${state.obfuscate()}');
} }
void addUser(model.AuthenticatedUser user, {bool setActive = false}) { void addUser(model.AuthenticatedUser user, {bool setActive = false}) {
state = state..add(user); state = state..add(user);
ref.invalidateSelf();
if (setActive) { if (setActive) {
final apiSettings = ref.read(apiSettingsProvider); final apiSettings = ref.read(apiSettingsProvider);
ref.read(apiSettingsProvider.notifier).updateState( ref
apiSettings.copyWith( .read(apiSettingsProvider.notifier)
activeUser: user, .updateState(apiSettings.copyWith(activeUser: user));
),
);
} }
} }
@ -80,11 +79,12 @@ class AuthenticatedUser extends _$AuthenticatedUser {
// also remove the user from the active user // also remove the user from the active user
final apiSettings = ref.read(apiSettingsProvider); final apiSettings = ref.read(apiSettingsProvider);
if (apiSettings.activeUser == user) { if (apiSettings.activeUser == user) {
ref.read(apiSettingsProvider.notifier).updateState( // replace the active user with the first user in the list
apiSettings.copyWith( // or null if there are no users left
activeUser: null, final newActiveUser = state.isNotEmpty ? state.first : null;
), ref
); .read(apiSettingsProvider.notifier)
.updateState(apiSettings.copyWith(activeUser: newActiveUser));
} }
} }
} }

View file

@ -0,0 +1,75 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'authenticated_users_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// provides with a set of authenticated users
@ProviderFor(AuthenticatedUsers)
final authenticatedUsersProvider = AuthenticatedUsersProvider._();
/// provides with a set of authenticated users
final class AuthenticatedUsersProvider
extends
$NotifierProvider<AuthenticatedUsers, Set<model.AuthenticatedUser>> {
/// provides with a set of authenticated users
AuthenticatedUsersProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'authenticatedUsersProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$authenticatedUsersHash();
@$internal
@override
AuthenticatedUsers create() => AuthenticatedUsers();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Set<model.AuthenticatedUser> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Set<model.AuthenticatedUser>>(value),
);
}
}
String _$authenticatedUsersHash() =>
r'c5e82cc70ffc31a0d315e3db9e07a141c583471e';
/// provides with a set of authenticated users
abstract class _$AuthenticatedUsers
extends $Notifier<Set<model.AuthenticatedUser>> {
Set<model.AuthenticatedUser> build();
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<Set<model.AuthenticatedUser>, Set<model.AuthenticatedUser>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
Set<model.AuthenticatedUser>,
Set<model.AuthenticatedUser>
>,
Set<model.AuthenticatedUser>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}

View file

@ -27,7 +27,8 @@ class CoverImage extends _$CoverImage {
// await Future.delayed(const Duration(seconds: 2)); // await Future.delayed(const Duration(seconds: 2));
// try to get the image from the cache // try to get the image from the cache
final file = await imageCacheManager.getFileFromMemory(itemId) ?? final file =
await imageCacheManager.getFileFromMemory(itemId) ??
await imageCacheManager.getFileFromCache(itemId); await imageCacheManager.getFileFromCache(itemId);
if (file != null) { if (file != null) {
@ -44,9 +45,7 @@ class CoverImage extends _$CoverImage {
); );
return; return;
} else { } else {
_logger.fine( _logger.fine('cover image stale for $itemId, fetching from the server');
'cover image stale for $itemId, fetching from the server',
);
} }
} else { } else {
_logger.fine('cover image not found in cache for $itemId'); _logger.fine('cover image not found in cache for $itemId');

View file

@ -6,167 +6,94 @@ part of 'image_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$coverImageHash() => r'89cc4783cbc76bb41beae34384d92fb277135c75'; // GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$CoverImage extends BuildlessStreamNotifier<Uint8List> {
late final String itemId;
Stream<Uint8List> build(
String itemId,
);
}
/// See also [CoverImage].
@ProviderFor(CoverImage) @ProviderFor(CoverImage)
const coverImageProvider = CoverImageFamily(); final coverImageProvider = CoverImageFamily._();
/// See also [CoverImage]. final class CoverImageProvider
class CoverImageFamily extends Family<AsyncValue<Uint8List>> { extends $StreamNotifierProvider<CoverImage, Uint8List> {
/// See also [CoverImage]. CoverImageProvider._({
const CoverImageFamily(); required CoverImageFamily super.from,
required String super.argument,
/// See also [CoverImage]. }) : super(
CoverImageProvider call( retry: null,
String itemId,
) {
return CoverImageProvider(
itemId,
);
}
@override
CoverImageProvider getProviderOverride(
covariant CoverImageProvider provider,
) {
return call(
provider.itemId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'coverImageProvider';
}
/// See also [CoverImage].
class CoverImageProvider
extends StreamNotifierProviderImpl<CoverImage, Uint8List> {
/// See also [CoverImage].
CoverImageProvider(
String itemId,
) : this._internal(
() => CoverImage()..itemId = itemId,
from: coverImageProvider,
name: r'coverImageProvider', name: r'coverImageProvider',
debugGetCreateSourceHash: isAutoDispose: false,
const bool.fromEnvironment('dart.vm.product')
? null
: _$coverImageHash,
dependencies: CoverImageFamily._dependencies,
allTransitiveDependencies:
CoverImageFamily._allTransitiveDependencies,
itemId: itemId,
);
CoverImageProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.itemId,
}) : super.internal();
final String itemId;
@override
Stream<Uint8List> runNotifierBuild(
covariant CoverImage notifier,
) {
return notifier.build(
itemId,
);
}
@override
Override overrideWith(CoverImage Function() create) {
return ProviderOverride(
origin: this,
override: CoverImageProvider._internal(
() => create()..itemId = itemId,
from: from,
name: null,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, $allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
itemId: itemId,
),
); );
}
@override @override
StreamNotifierProviderElement<CoverImage, Uint8List> createElement() { String debugGetCreateSourceHash() => _$coverImageHash();
return _CoverImageProviderElement(this);
@override
String toString() {
return r'coverImageProvider'
''
'($argument)';
} }
@$internal
@override
CoverImage create() => CoverImage();
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is CoverImageProvider && other.itemId == itemId; return other is CoverImageProvider && other.argument == argument;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); return argument.hashCode;
hash = _SystemHash.combine(hash, itemId.hashCode);
return _SystemHash.finish(hash);
} }
} }
mixin CoverImageRef on StreamNotifierProviderRef<Uint8List> { String _$coverImageHash() => r'89cc4783cbc76bb41beae34384d92fb277135c75';
/// The parameter `itemId` of this provider.
String get itemId;
}
class _CoverImageProviderElement final class CoverImageFamily extends $Family
extends StreamNotifierProviderElement<CoverImage, Uint8List> with
with CoverImageRef { $ClassFamilyOverride<
_CoverImageProviderElement(super.provider); CoverImage,
AsyncValue<Uint8List>,
Uint8List,
Stream<Uint8List>,
String
> {
CoverImageFamily._()
: super(
retry: null,
name: r'coverImageProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: false,
);
CoverImageProvider call(String itemId) =>
CoverImageProvider._(argument: itemId, from: this);
@override @override
String get itemId => (origin as CoverImageProvider).itemId; String toString() => r'coverImageProvider';
}
abstract class _$CoverImage extends $StreamNotifier<Uint8List> {
late final _$args = ref.$arg as String;
String get itemId => _$args;
Stream<Uint8List> build(String itemId);
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<Uint8List>, Uint8List>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<Uint8List>, Uint8List>,
AsyncValue<Uint8List>,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
} }
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -26,7 +26,8 @@ class LibraryItem extends _$LibraryItem {
// look for the item in the cache // look for the item in the cache
final key = CacheKey.libraryItem(id); final key = CacheKey.libraryItem(id);
final cachedFile = await apiResponseCacheManager.getFileFromMemory(key) ?? final cachedFile =
await apiResponseCacheManager.getFileFromMemory(key) ??
await apiResponseCacheManager.getFileFromCache(key); await apiResponseCacheManager.getFileFromCache(key);
if (cachedFile != null) { if (cachedFile != null) {
_logger.fine( _logger.fine(

View file

@ -6,182 +6,112 @@ part of 'library_item_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$libraryItemHash() => r'a3cfa7f912e9498a70b5782899018b6964d6445c'; // GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$LibraryItem
extends BuildlessStreamNotifier<shelfsdk.LibraryItemExpanded> {
late final String id;
Stream<shelfsdk.LibraryItemExpanded> build(
String id,
);
}
/// provides the library item for the given id /// provides the library item for the given id
///
/// Copied from [LibraryItem].
@ProviderFor(LibraryItem) @ProviderFor(LibraryItem)
const libraryItemProvider = LibraryItemFamily(); final libraryItemProvider = LibraryItemFamily._();
/// provides the library item for the given id /// provides the library item for the given id
/// final class LibraryItemProvider
/// Copied from [LibraryItem]. extends $StreamNotifierProvider<LibraryItem, shelfsdk.LibraryItemExpanded> {
class LibraryItemFamily
extends Family<AsyncValue<shelfsdk.LibraryItemExpanded>> {
/// provides the library item for the given id /// provides the library item for the given id
/// LibraryItemProvider._({
/// Copied from [LibraryItem]. required LibraryItemFamily super.from,
const LibraryItemFamily(); required String super.argument,
}) : super(
/// provides the library item for the given id retry: null,
///
/// Copied from [LibraryItem].
LibraryItemProvider call(
String id,
) {
return LibraryItemProvider(
id,
);
}
@override
LibraryItemProvider getProviderOverride(
covariant LibraryItemProvider provider,
) {
return call(
provider.id,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'libraryItemProvider';
}
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
class LibraryItemProvider extends StreamNotifierProviderImpl<LibraryItem,
shelfsdk.LibraryItemExpanded> {
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
LibraryItemProvider(
String id,
) : this._internal(
() => LibraryItem()..id = id,
from: libraryItemProvider,
name: r'libraryItemProvider', name: r'libraryItemProvider',
debugGetCreateSourceHash: isAutoDispose: false,
const bool.fromEnvironment('dart.vm.product')
? null
: _$libraryItemHash,
dependencies: LibraryItemFamily._dependencies,
allTransitiveDependencies:
LibraryItemFamily._allTransitiveDependencies,
id: id,
);
LibraryItemProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.id,
}) : super.internal();
final String id;
@override
Stream<shelfsdk.LibraryItemExpanded> runNotifierBuild(
covariant LibraryItem notifier,
) {
return notifier.build(
id,
);
}
@override
Override overrideWith(LibraryItem Function() create) {
return ProviderOverride(
origin: this,
override: LibraryItemProvider._internal(
() => create()..id = id,
from: from,
name: null,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, $allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
id: id,
),
); );
}
@override @override
StreamNotifierProviderElement<LibraryItem, shelfsdk.LibraryItemExpanded> String debugGetCreateSourceHash() => _$libraryItemHash();
createElement() {
return _LibraryItemProviderElement(this); @override
String toString() {
return r'libraryItemProvider'
''
'($argument)';
} }
@$internal
@override
LibraryItem create() => LibraryItem();
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is LibraryItemProvider && other.id == id; return other is LibraryItemProvider && other.argument == argument;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); return argument.hashCode;
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
} }
} }
mixin LibraryItemRef String _$libraryItemHash() => r'a3cfa7f912e9498a70b5782899018b6964d6445c';
on StreamNotifierProviderRef<shelfsdk.LibraryItemExpanded> {
/// The parameter `id` of this provider.
String get id;
}
class _LibraryItemProviderElement extends StreamNotifierProviderElement< /// provides the library item for the given id
LibraryItem, shelfsdk.LibraryItemExpanded> with LibraryItemRef {
_LibraryItemProviderElement(super.provider); final class LibraryItemFamily extends $Family
with
$ClassFamilyOverride<
LibraryItem,
AsyncValue<shelfsdk.LibraryItemExpanded>,
shelfsdk.LibraryItemExpanded,
Stream<shelfsdk.LibraryItemExpanded>,
String
> {
LibraryItemFamily._()
: super(
retry: null,
name: r'libraryItemProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: false,
);
/// provides the library item for the given id
LibraryItemProvider call(String id) =>
LibraryItemProvider._(argument: id, from: this);
@override @override
String get id => (origin as LibraryItemProvider).id; String toString() => r'libraryItemProvider';
}
/// provides the library item for the given id
abstract class _$LibraryItem
extends $StreamNotifier<shelfsdk.LibraryItemExpanded> {
late final _$args = ref.$arg as String;
String get id => _$args;
Stream<shelfsdk.LibraryItemExpanded> build(String id);
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<
AsyncValue<shelfsdk.LibraryItemExpanded>,
shelfsdk.LibraryItemExpanded
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
AsyncValue<shelfsdk.LibraryItemExpanded>,
shelfsdk.LibraryItemExpanded
>,
AsyncValue<shelfsdk.LibraryItemExpanded>,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
} }
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -0,0 +1,60 @@
import 'package:hooks_riverpod/hooks_riverpod.dart'
show Ref, ProviderListenableSelect;
import 'package:logging/logging.dart' show Logger;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' show Library;
import 'package:vaani/api/api_provider.dart' show authenticatedApiProvider;
import 'package:vaani/settings/api_settings_provider.dart'
show apiSettingsProvider;
part 'library_provider.g.dart';
final _logger = Logger('LibraryProvider');
@riverpod
Future<Library?> 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<Library?> 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<List<Library>> 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;
}
}

View file

@ -0,0 +1,158 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'library_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(library)
final libraryProvider = LibraryFamily._();
final class LibraryProvider
extends
$FunctionalProvider<AsyncValue<Library?>, Library?, FutureOr<Library?>>
with $FutureModifier<Library?>, $FutureProvider<Library?> {
LibraryProvider._({
required LibraryFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'libraryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$libraryHash();
@override
String toString() {
return r'libraryProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<Library?> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<Library?> create(Ref ref) {
final argument = this.argument as String;
return library(ref, argument);
}
@override
bool operator ==(Object other) {
return other is LibraryProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$libraryHash() => r'f8a34100acb58f02fa958c71a629577bf815710e';
final class LibraryFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<Library?>, String> {
LibraryFamily._()
: super(
retry: null,
name: r'libraryProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
LibraryProvider call(String id) =>
LibraryProvider._(argument: id, from: this);
@override
String toString() => r'libraryProvider';
}
@ProviderFor(currentLibrary)
final currentLibraryProvider = CurrentLibraryProvider._();
final class CurrentLibraryProvider
extends
$FunctionalProvider<AsyncValue<Library?>, Library?, FutureOr<Library?>>
with $FutureModifier<Library?>, $FutureProvider<Library?> {
CurrentLibraryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'currentLibraryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$currentLibraryHash();
@$internal
@override
$FutureProviderElement<Library?> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<Library?> create(Ref ref) {
return currentLibrary(ref);
}
}
String _$currentLibraryHash() => r'658498a531e04a01e2b3915a3319101285601118';
@ProviderFor(Libraries)
final librariesProvider = LibrariesProvider._();
final class LibrariesProvider
extends $AsyncNotifierProvider<Libraries, List<Library>> {
LibrariesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'librariesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$librariesHash();
@$internal
@override
Libraries create() => Libraries();
}
String _$librariesHash() => r'95ebd4d1ac0cc2acf7617dc22895eff0ca30600f';
abstract class _$Libraries extends $AsyncNotifier<List<Library>> {
FutureOr<List<Library>> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<List<Library>>, List<Library>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<Library>>, List<Library>>,
AsyncValue<List<Library>>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}

View file

@ -1,16 +1,17 @@
import 'package:collection/collection.dart'; import 'package:logging/logging.dart';
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.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/db/storage.dart';
import 'package:vaani/settings/api_settings_provider.dart'; import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/audiobookshelf_server.dart' import 'package:vaani/settings/models/audiobookshelf_server.dart' as model;
as model; import 'package:vaani/shared/extensions/obfuscation.dart';
part 'server_provider.g.dart'; part 'server_provider.g.dart';
final _box = AvailableHiveBoxes.serverBox; final _box = AvailableHiveBoxes.serverBox;
final _logger = Logger('AudiobookShelfServerProvider');
class ServerAlreadyExistsException implements Exception { class ServerAlreadyExistsException implements Exception {
final model.AudiobookShelfServer server; final model.AudiobookShelfServer server;
@ -27,7 +28,7 @@ class ServerAlreadyExistsException implements Exception {
class AudiobookShelfServer extends _$AudiobookShelfServer { class AudiobookShelfServer extends _$AudiobookShelfServer {
@override @override
Set<model.AudiobookShelfServer> build() { Set<model.AudiobookShelfServer> build() {
ref.listenSelf((_, __) { listenSelf((_, __) {
writeStateToBox(); writeStateToBox();
}); });
// get the app settings // get the app settings
@ -47,10 +48,10 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
Set<model.AudiobookShelfServer> readFromBoxOrCreate() { Set<model.AudiobookShelfServer> readFromBoxOrCreate() {
if (_box.isNotEmpty) { if (_box.isNotEmpty) {
final foundServers = _box.getRange(0, _box.length); final foundServers = _box.getRange(0, _box.length);
debugPrint('found servers in box: $foundServers'); _logger.info('found servers in box: ${foundServers.obfuscate()}');
return foundServers.whereNotNull().toSet(); return foundServers.nonNulls.toSet();
} else { } else {
debugPrint('no settings found in box'); _logger.info('no settings found in box');
return {}; return {};
} }
} }
@ -61,7 +62,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
return; return;
} }
_box.addAll(state); _box.addAll(state);
debugPrint('writing state to box: $state'); _logger.info('writing state to box: ${state.obfuscate()}');
} }
void addServer(model.AudiobookShelfServer server) { void addServer(model.AudiobookShelfServer server) {
@ -71,23 +72,21 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
state = {...state, server}; state = {...state, server};
} }
void removeServer(model.AudiobookShelfServer server, void removeServer(
{ model.AudiobookShelfServer server, {
bool removeUsers = false, bool removeUsers = false,
}) { }) {
state = state.where((s) => s != server).toSet(); state = state.where((s) => s != server).toSet();
// remove the server from the active server // remove the server from the active server
final apiSettings = ref.read(apiSettingsProvider); final apiSettings = ref.read(apiSettingsProvider);
if (apiSettings.activeServer == server) { if (apiSettings.activeServer == server) {
ref.read(apiSettingsProvider.notifier).updateState( ref
apiSettings.copyWith( .read(apiSettingsProvider.notifier)
activeServer: null, .updateState(apiSettings.copyWith(activeServer: null));
),
);
} }
// remove the users of this server // remove the users of this server
if (removeUsers) { if (removeUsers) {
ref.read(authenticatedUserProvider.notifier).removeUsersOfServer(server); ref.read(authenticatedUsersProvider.notifier).removeUsersOfServer(server);
} }
} }

View file

@ -6,25 +6,78 @@ part of 'server_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$audiobookShelfServerHash() => // GENERATED CODE - DO NOT MODIFY BY HAND
r'f0d645bb42233c59886bc43fdc473897484ceca1'; // ignore_for_file: type=lint, type=warning
/// provides with a set of servers added by the user
@ProviderFor(AudiobookShelfServer)
final audiobookShelfServerProvider = AudiobookShelfServerProvider._();
/// provides with a set of servers added by the user /// provides with a set of servers added by the user
/// final class AudiobookShelfServerProvider
/// Copied from [AudiobookShelfServer]. extends
@ProviderFor(AudiobookShelfServer) $NotifierProvider<
final audiobookShelfServerProvider = AutoDisposeNotifierProvider< AudiobookShelfServer,
AudiobookShelfServer, Set<model.AudiobookShelfServer>>.internal( Set<model.AudiobookShelfServer>
AudiobookShelfServer.new, > {
/// provides with a set of servers added by the user
AudiobookShelfServerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'audiobookShelfServerProvider', name: r'audiobookShelfServerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') isAutoDispose: true,
? null
: _$audiobookShelfServerHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, $allTransitiveDependencies: null,
); );
typedef _$AudiobookShelfServer @override
= AutoDisposeNotifier<Set<model.AudiobookShelfServer>>; String debugGetCreateSourceHash() => _$audiobookShelfServerHash();
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member @$internal
@override
AudiobookShelfServer create() => AudiobookShelfServer();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Set<model.AudiobookShelfServer> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Set<model.AudiobookShelfServer>>(
value,
),
);
}
}
String _$audiobookShelfServerHash() =>
r'144817dcb3704b80c5b60763167fcf932f00c29c';
/// provides with a set of servers added by the user
abstract class _$AudiobookShelfServer
extends $Notifier<Set<model.AudiobookShelfServer>> {
Set<model.AudiobookShelfServer> build();
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<
Set<model.AudiobookShelfServer>,
Set<model.AudiobookShelfServer>
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
Set<model.AudiobookShelfServer>,
Set<model.AudiobookShelfServer>
>,
Set<model.AudiobookShelfServer>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}

View file

@ -10,5 +10,4 @@ class HeroTagPrefixes {
static const String bookTitle = 'book_title_'; static const String bookTitle = 'book_title_';
static const String narratorName = 'narrator_name_'; static const String narratorName = 'narrator_name_';
static const String libraryItemPlayButton = 'library_item_play_button_'; static const String libraryItemPlayButton = 'library_item_play_button_';
} }

View file

@ -1,5 +1,5 @@
import 'package:flutter/foundation.dart' show immutable; import 'package:flutter/foundation.dart' show immutable;
import 'package:hive/hive.dart'; import 'package:hive_plus_secure/hive_plus_secure.dart';
import 'package:vaani/features/per_book_settings/models/book_settings.dart'; import 'package:vaani/features/per_book_settings/models/book_settings.dart';
import 'package:vaani/settings/models/models.dart'; import 'package:vaani/settings/models/models.dart';
@ -14,14 +14,17 @@ class AvailableHiveBoxes {
static final apiSettingsBox = Hive.box<ApiSettings>(name: 'apiSettings'); static final apiSettingsBox = Hive.box<ApiSettings>(name: 'apiSettings');
/// stores the a list of [AudiobookShelfServer] /// stores the a list of [AudiobookShelfServer]
static final serverBox = static final serverBox = Hive.box<AudiobookShelfServer>(
Hive.box<AudiobookShelfServer>(name: 'audiobookShelfServer'); name: 'audiobookShelfServer',
);
/// stores the a list of [AuthenticatedUser] /// stores the a list of [AuthenticatedUser]
static final authenticatedUserBox = static final authenticatedUserBox = Hive.box<AuthenticatedUser>(
Hive.box<AuthenticatedUser>(name: 'authenticatedUser'); name: 'authenticatedUser',
);
/// stores the a list of [BookSettings] /// stores the a list of [BookSettings]
static final individualBookSettingsBox = static final individualBookSettingsBox = Hive.box<BookSettings>(
Hive.box<BookSettings>(name: 'bookSettings'); name: 'bookSettings',
);
} }

View file

@ -1,39 +1,39 @@
import 'package:isar/isar.dart'; // import 'package:isar/isar.dart';
part 'image.g.dart'; // part 'image.g.dart';
/// Represents a cover image for a library item // /// Represents a cover image for a library item
/// // ///
/// stores 2 paths, one is thumbnail and the other is the full size image // /// stores 2 paths, one is thumbnail and the other is the full size image
/// both are optional // /// both are optional
/// also stores last fetched date for the image // /// also stores last fetched date for the image
/// Id is passed as a parameter to the collection annotation (the lib_item_id) // /// Id is passed as a parameter to the collection annotation (the lib_item_id)
/// also index the id // /// also index the id
/// This is because the image is a part of the library item and the library item // /// This is because the image is a part of the library item and the library item
/// is the parent of the image // /// is the parent of the image
@Collection(ignore: {'path'}) // @Collection(ignore: {'path'})
@Name('CacheImage') // @Name('CacheImage')
class Image { // class Image {
@Id() // @Id()
int id; // int id;
String? thumbnailPath; // String? thumbnailPath;
String? imagePath; // String? imagePath;
DateTime lastSaved; // DateTime lastSaved;
Image({ // Image({
required this.id, // required this.id,
this.thumbnailPath, // this.thumbnailPath,
this.imagePath, // this.imagePath,
}) : lastSaved = DateTime.now(); // }) : lastSaved = DateTime.now();
/// returns the path to the image // /// returns the path to the image
String? get path => thumbnailPath ?? imagePath; // String? get path => thumbnailPath ?? imagePath;
/// automatically updates the last fetched date when saving a new path // /// automatically updates the last fetched date when saving a new path
void updatePath(String? thumbnailPath, String? imagePath) async { // void updatePath(String? thumbnailPath, String? imagePath) async {
this.thumbnailPath = thumbnailPath; // this.thumbnailPath = thumbnailPath;
this.imagePath = imagePath; // this.imagePath = imagePath;
lastSaved = DateTime.now(); // lastSaved = DateTime.now();
} // }
} // }

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,23 @@
// does the initial setup of the storage
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:hive_plus_secure/hive_plus_secure.dart';
import 'package:hive/hive.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:vaani/main.dart';
import 'package:vaani/settings/constants.dart'; import 'package:vaani/settings/constants.dart';
import 'register_models.dart'; import 'register_models.dart';
// does the initial setup of the storage
Future initStorage() async { Future initStorage() async {
final dir = await getApplicationDocumentsDirectory(); final dir = await getApplicationDocumentsDirectory();
// use vaani as the directory for hive // use vaani as the directory for hive
final storageDir = Directory(p.join( final storageDir = Directory(p.join(dir.path, AppMetadata.appNameLowerCase));
dir.path,
AppMetadata.appNameLowerCase,
),
);
await storageDir.create(recursive: true); await storageDir.create(recursive: true);
Hive.defaultDirectory = storageDir.path; Hive.defaultDirectory = storageDir.path;
debugPrint('Hive storage directory init: ${Hive.defaultDirectory}'); appLogger.config('Hive storage directory init: ${Hive.defaultDirectory}');
await registerModels(); await registerModels();
} }

View file

@ -1,29 +1,29 @@
// a table to track preferences of player for each book // // a table to track preferences of player for each book
import 'package:isar/isar.dart'; // import 'package:isar/isar.dart';
part 'book_prefs.g.dart'; // part 'book_prefs.g.dart';
/// stores the preferences of the player for a book // /// stores the preferences of the player for a book
@Collection() // @Collection()
@Name('BookPrefs') // @Name('BookPrefs')
class BookPrefs { // class BookPrefs {
@Id() // @Id()
int libItemId; // int libItemId;
double? speed; // double? speed;
// double? volume; // // double? volume;
// Duration? sleepTimer; // // Duration? sleepTimer;
// bool? showTotalProgress; // // bool? showTotalProgress;
// bool? showChapterProgress; // // bool? showChapterProgress;
// bool? useChapterInfo; // // bool? useChapterInfo;
BookPrefs({ // BookPrefs({
required this.libItemId, // required this.libItemId,
this.speed, // this.speed,
// this.volume, // // this.volume,
// this.sleepTimer, // // this.sleepTimer,
// this.showTotalProgress, // // this.showTotalProgress,
// this.showChapterProgress, // // this.showChapterProgress,
// this.useChapterInfo, // // this.useChapterInfo,
}); // });
} // }

View file

@ -1,496 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'book_prefs.dart';
// **************************************************************************
// _IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, invalid_use_of_protected_member, lines_longer_than_80_chars, constant_identifier_names, avoid_js_rounded_ints, no_leading_underscores_for_local_identifiers, require_trailing_commas, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_in_if_null_operators, library_private_types_in_public_api, prefer_const_constructors
// ignore_for_file: type=lint
extension GetBookPrefsCollection on Isar {
IsarCollection<int, BookPrefs> get bookPrefs => this.collection();
}
const BookPrefsSchema = IsarGeneratedSchema(
schema: IsarSchema(
name: 'BookPrefs',
idName: 'libItemId',
embedded: false,
properties: [
IsarPropertySchema(
name: 'speed',
type: IsarType.double,
),
],
indexes: [],
),
converter: IsarObjectConverter<int, BookPrefs>(
serialize: serializeBookPrefs,
deserialize: deserializeBookPrefs,
deserializeProperty: deserializeBookPrefsProp,
),
embeddedSchemas: [],
);
@isarProtected
int serializeBookPrefs(IsarWriter writer, BookPrefs object) {
IsarCore.writeDouble(writer, 1, object.speed ?? double.nan);
return object.libItemId;
}
@isarProtected
BookPrefs deserializeBookPrefs(IsarReader reader) {
final int _libItemId;
_libItemId = IsarCore.readId(reader);
final double? _speed;
{
final value = IsarCore.readDouble(reader, 1);
if (value.isNaN) {
_speed = null;
} else {
_speed = value;
}
}
final object = BookPrefs(
libItemId: _libItemId,
speed: _speed,
);
return object;
}
@isarProtected
dynamic deserializeBookPrefsProp(IsarReader reader, int property) {
switch (property) {
case 0:
return IsarCore.readId(reader);
case 1:
{
final value = IsarCore.readDouble(reader, 1);
if (value.isNaN) {
return null;
} else {
return value;
}
}
default:
throw ArgumentError('Unknown property: $property');
}
}
sealed class _BookPrefsUpdate {
bool call({
required int libItemId,
double? speed,
});
}
class _BookPrefsUpdateImpl implements _BookPrefsUpdate {
const _BookPrefsUpdateImpl(this.collection);
final IsarCollection<int, BookPrefs> collection;
@override
bool call({
required int libItemId,
Object? speed = ignore,
}) {
return collection.updateProperties([
libItemId
], {
if (speed != ignore) 1: speed as double?,
}) >
0;
}
}
sealed class _BookPrefsUpdateAll {
int call({
required List<int> libItemId,
double? speed,
});
}
class _BookPrefsUpdateAllImpl implements _BookPrefsUpdateAll {
const _BookPrefsUpdateAllImpl(this.collection);
final IsarCollection<int, BookPrefs> collection;
@override
int call({
required List<int> libItemId,
Object? speed = ignore,
}) {
return collection.updateProperties(libItemId, {
if (speed != ignore) 1: speed as double?,
});
}
}
extension BookPrefsUpdate on IsarCollection<int, BookPrefs> {
_BookPrefsUpdate get update => _BookPrefsUpdateImpl(this);
_BookPrefsUpdateAll get updateAll => _BookPrefsUpdateAllImpl(this);
}
sealed class _BookPrefsQueryUpdate {
int call({
double? speed,
});
}
class _BookPrefsQueryUpdateImpl implements _BookPrefsQueryUpdate {
const _BookPrefsQueryUpdateImpl(this.query, {this.limit});
final IsarQuery<BookPrefs> query;
final int? limit;
@override
int call({
Object? speed = ignore,
}) {
return query.updateProperties(limit: limit, {
if (speed != ignore) 1: speed as double?,
});
}
}
extension BookPrefsQueryUpdate on IsarQuery<BookPrefs> {
_BookPrefsQueryUpdate get updateFirst =>
_BookPrefsQueryUpdateImpl(this, limit: 1);
_BookPrefsQueryUpdate get updateAll => _BookPrefsQueryUpdateImpl(this);
}
class _BookPrefsQueryBuilderUpdateImpl implements _BookPrefsQueryUpdate {
const _BookPrefsQueryBuilderUpdateImpl(this.query, {this.limit});
final QueryBuilder<BookPrefs, BookPrefs, QOperations> query;
final int? limit;
@override
int call({
Object? speed = ignore,
}) {
final q = query.build();
try {
return q.updateProperties(limit: limit, {
if (speed != ignore) 1: speed as double?,
});
} finally {
q.close();
}
}
}
extension BookPrefsQueryBuilderUpdate
on QueryBuilder<BookPrefs, BookPrefs, QOperations> {
_BookPrefsQueryUpdate get updateFirst =>
_BookPrefsQueryBuilderUpdateImpl(this, limit: 1);
_BookPrefsQueryUpdate get updateAll => _BookPrefsQueryBuilderUpdateImpl(this);
}
extension BookPrefsQueryFilter
on QueryBuilder<BookPrefs, BookPrefs, QFilterCondition> {
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> libItemIdEqualTo(
int value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
EqualCondition(
property: 0,
value: value,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition>
libItemIdGreaterThan(
int value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
GreaterCondition(
property: 0,
value: value,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition>
libItemIdGreaterThanOrEqualTo(
int value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
GreaterOrEqualCondition(
property: 0,
value: value,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> libItemIdLessThan(
int value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
LessCondition(
property: 0,
value: value,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition>
libItemIdLessThanOrEqualTo(
int value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
LessOrEqualCondition(
property: 0,
value: value,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> libItemIdBetween(
int lower,
int upper,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
BetweenCondition(
property: 0,
lower: lower,
upper: upper,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(const IsNullCondition(property: 1));
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedIsNotNull() {
return QueryBuilder.apply(not(), (query) {
return query.addFilterCondition(const IsNullCondition(property: 1));
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedEqualTo(
double? value, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
EqualCondition(
property: 1,
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedGreaterThan(
double? value, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
GreaterCondition(
property: 1,
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition>
speedGreaterThanOrEqualTo(
double? value, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
GreaterOrEqualCondition(
property: 1,
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedLessThan(
double? value, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
LessCondition(
property: 1,
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition>
speedLessThanOrEqualTo(
double? value, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
LessOrEqualCondition(
property: 1,
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterFilterCondition> speedBetween(
double? lower,
double? upper, {
double epsilon = Filter.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
BetweenCondition(
property: 1,
lower: lower,
upper: upper,
epsilon: epsilon,
),
);
});
}
}
extension BookPrefsQueryObject
on QueryBuilder<BookPrefs, BookPrefs, QFilterCondition> {}
extension BookPrefsQuerySortBy on QueryBuilder<BookPrefs, BookPrefs, QSortBy> {
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> sortByLibItemId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(0);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> sortByLibItemIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(0, sort: Sort.desc);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> sortBySpeed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(1);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> sortBySpeedDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(1, sort: Sort.desc);
});
}
}
extension BookPrefsQuerySortThenBy
on QueryBuilder<BookPrefs, BookPrefs, QSortThenBy> {
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> thenByLibItemId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(0);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> thenByLibItemIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(0, sort: Sort.desc);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> thenBySpeed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(1);
});
}
QueryBuilder<BookPrefs, BookPrefs, QAfterSortBy> thenBySpeedDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(1, sort: Sort.desc);
});
}
}
extension BookPrefsQueryWhereDistinct
on QueryBuilder<BookPrefs, BookPrefs, QDistinct> {
QueryBuilder<BookPrefs, BookPrefs, QAfterDistinct> distinctBySpeed() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(1);
});
}
}
extension BookPrefsQueryProperty1
on QueryBuilder<BookPrefs, BookPrefs, QProperty> {
QueryBuilder<BookPrefs, int, QAfterProperty> libItemIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(0);
});
}
QueryBuilder<BookPrefs, double?, QAfterProperty> speedProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(1);
});
}
}
extension BookPrefsQueryProperty2<R>
on QueryBuilder<BookPrefs, R, QAfterProperty> {
QueryBuilder<BookPrefs, (R, int), QAfterProperty> libItemIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(0);
});
}
QueryBuilder<BookPrefs, (R, double?), QAfterProperty> speedProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(1);
});
}
}
extension BookPrefsQueryProperty3<R1, R2>
on QueryBuilder<BookPrefs, (R1, R2), QAfterProperty> {
QueryBuilder<BookPrefs, (R1, R2, int), QOperations> libItemIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(0);
});
}
QueryBuilder<BookPrefs, (R1, R2, double?), QOperations> speedProperty() {
return QueryBuilder.apply(this, (query) {
return query.addProperty(1);
});
}
}

View file

@ -1,4 +1,4 @@
import 'package:hive/hive.dart'; import 'package:hive_plus_secure/hive_plus_secure.dart';
import 'package:vaani/features/per_book_settings/models/book_settings.dart'; import 'package:vaani/features/per_book_settings/models/book_settings.dart';
import 'package:vaani/settings/models/models.dart'; import 'package:vaani/settings/models/models.dart';

View file

@ -8,6 +8,7 @@ import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/extensions/obfuscation.dart';
final _logger = Logger('AudiobookDownloadManager'); final _logger = Logger('AudiobookDownloadManager');
final tq = MemoryTaskQueue(); final tq = MemoryTaskQueue();
@ -35,7 +36,9 @@ class AudiobookDownloadManager {
FileDownloader().addTaskQueue(tq); FileDownloader().addTaskQueue(tq);
_logger.fine('initialized with baseUrl: $baseUrl, token: $token'); _logger.fine(
'initialized with baseUrl: ${Uri.parse(baseUrl).obfuscate()} and token: ${token.obfuscate()}',
);
_logger.fine( _logger.fine(
'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause', 'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause',
); );
@ -64,17 +67,13 @@ class AudiobookDownloadManager {
late StreamSubscription<TaskUpdate> _updatesSubscription; late StreamSubscription<TaskUpdate> _updatesSubscription;
Future<void> queueAudioBookDownload( Future<void> queueAudioBookDownload(LibraryItemExpanded item) async {
LibraryItemExpanded item,
) async {
_logger.info('queuing download for item: ${item.id}'); _logger.info('queuing download for item: ${item.id}');
// create a download task for each file in the item // create a download task for each file in the item
final directory = await getApplicationSupportDirectory(); final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) { for (final file in item.libraryFiles) {
// check if the file is already downloaded // check if the file is already downloaded
if (isFileDownloaded( if (isFileDownloaded(constructFilePath(directory, item, file))) {
constructFilePath(directory, item, file),
)) {
_logger.info('file already downloaded: ${file.metadata.filename}'); _logger.info('file already downloaded: ${file.metadata.filename}');
continue; continue;
} }
@ -102,8 +101,7 @@ class AudiobookDownloadManager {
Directory directory, Directory directory,
LibraryItemExpanded item, LibraryItemExpanded item,
LibraryFile file, LibraryFile file,
) => ) => '${directory.path}/${item.relPath}/${file.metadata.filename}';
'${directory.path}/${item.relPath}/${file.metadata.filename}';
void dispose() { void dispose() {
_updatesSubscription.cancel(); _updatesSubscription.cancel();

View file

@ -1,4 +1,5 @@
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
@ -31,9 +32,11 @@ class SimpleDownloadManager extends _$SimpleDownloadManager {
core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup; core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup;
ref.onDispose(() { ref.onDispose(() {
_logger.info('disposing download manager');
manager.dispose(); manager.dispose();
}); });
_logger.config('initialized download manager');
return manager; return manager;
} }
} }
@ -49,15 +52,13 @@ class DownloadManager extends _$DownloadManager {
return manager; return manager;
} }
Future<void> queueAudioBookDownload( Future<void> queueAudioBookDownload(LibraryItemExpanded item) async {
LibraryItemExpanded item, _logger.fine('queueing download for ${item.id}');
) async { await state.queueAudioBookDownload(item);
await state.queueAudioBookDownload(
item,
);
} }
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async { Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
_logger.fine('deleting downloaded item ${item.id}');
await state.deleteDownloadedItem(item); await state.deleteDownloadedItem(item);
ref.notifyListeners(); ref.notifyListeners();
} }
@ -78,14 +79,16 @@ class ItemDownloadProgress extends _$ItemDownloadProgress {
Future<double?> build(String id) async { Future<double?> build(String id) async {
final item = await ref.watch(libraryItemProvider(id).future); final item = await ref.watch(libraryItemProvider(id).future);
final manager = ref.read(downloadManagerProvider); final manager = ref.read(downloadManagerProvider);
manager.taskUpdateStream.map((taskUpdate) { manager.taskUpdateStream
.map((taskUpdate) {
if (taskUpdate is! TaskProgressUpdate) { if (taskUpdate is! TaskProgressUpdate) {
return null; return null;
} }
if (taskUpdate.task.group == id) { if (taskUpdate.task.group == id) {
return taskUpdate; return taskUpdate;
} }
}).listen((task) async { })
.listen((task) async {
if (task != null) { if (task != null) {
final totalSize = item.totalSize; final totalSize = item.totalSize;
// if total size is 0, return 0 // if total size is 0, return 0
@ -93,7 +96,9 @@ class ItemDownloadProgress extends _$ItemDownloadProgress {
state = const AsyncValue.data(0.0); state = const AsyncValue.data(0.0);
return; return;
} }
final downloadedFiles = await manager.getDownloadedFilesMetadata(item); final downloadedFiles = await manager.getDownloadedFilesMetadata(
item,
);
// calculate total size of downloaded files and total size of item, then divide // calculate total size of downloaded files and total size of item, then divide
// to get percentage // to get percentage
final downloadedSize = downloadedFiles.fold<int>( final downloadedSize = downloadedFiles.fold<int>(
@ -105,7 +110,7 @@ class ItemDownloadProgress extends _$ItemDownloadProgress {
final totalDownloadedSize = downloadedSize + inProgressFileSize; final totalDownloadedSize = downloadedSize + inProgressFileSize;
final progress = totalDownloadedSize / totalSize; final progress = totalDownloadedSize / totalSize;
// if current progress is more than calculated progress, do not update // if current progress is more than calculated progress, do not update
if (progress < (state.valueOrNull ?? 0.0)) { if (progress < (state.value ?? 0.0)) {
return; return;
} }
@ -117,19 +122,14 @@ class ItemDownloadProgress extends _$ItemDownloadProgress {
} }
@riverpod @riverpod
FutureOr<List<TaskRecord>> downloadHistory( FutureOr<List<TaskRecord>> downloadHistory(Ref ref, {String? group}) async {
DownloadHistoryRef ref, {
String? group,
}) async {
return await FileDownloader().database.allRecords(group: group); return await FileDownloader().database.allRecords(group: group);
} }
@riverpod @riverpod
class IsItemDownloaded extends _$IsItemDownloaded { class IsItemDownloaded extends _$IsItemDownloaded {
@override @override
FutureOr<bool> build( FutureOr<bool> build(LibraryItemExpanded item) {
LibraryItemExpanded item,
) {
final manager = ref.watch(downloadManagerProvider); final manager = ref.watch(downloadManagerProvider);
return manager.isItemDownloaded(item); return manager.isItemDownloaded(item);
} }

File diff suppressed because it is too large Load diff

View file

@ -11,10 +11,7 @@ class DownloadsPage extends HookConsumerWidget {
final downloadHistory = ref.watch(downloadHistoryProvider()); final downloadHistory = ref.watch(downloadHistoryProvider());
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: const Text('Downloads')),
title: const Text('Downloads'),
backgroundColor: Colors.transparent,
),
body: Center( body: Center(
// history of downloads // history of downloads
child: downloadHistory.when( child: downloadHistory.when(

View file

@ -6,24 +6,64 @@ part of 'search_controller.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// The controller for the search bar.
@ProviderFor(GlobalSearchController)
final globalSearchControllerProvider = GlobalSearchControllerProvider._();
/// The controller for the search bar.
final class GlobalSearchControllerProvider
extends $NotifierProvider<GlobalSearchController, Raw<SearchController>> {
/// The controller for the search bar.
GlobalSearchControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'globalSearchControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$globalSearchControllerHash();
@$internal
@override
GlobalSearchController create() => GlobalSearchController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Raw<SearchController> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Raw<SearchController>>(value),
);
}
}
String _$globalSearchControllerHash() => String _$globalSearchControllerHash() =>
r'd854ace6f2e00a10fc33aba63051375f82ad1b10'; r'd854ace6f2e00a10fc33aba63051375f82ad1b10';
/// The controller for the search bar. /// The controller for the search bar.
///
/// Copied from [GlobalSearchController].
@ProviderFor(GlobalSearchController)
final globalSearchControllerProvider =
NotifierProvider<GlobalSearchController, Raw<SearchController>>.internal(
GlobalSearchController.new,
name: r'globalSearchControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$globalSearchControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$GlobalSearchController = Notifier<Raw<SearchController>>; abstract class _$GlobalSearchController
// ignore_for_file: type=lint extends $Notifier<Raw<SearchController>> {
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member Raw<SearchController> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<Raw<SearchController>, Raw<SearchController>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Raw<SearchController>, Raw<SearchController>>,
Raw<SearchController>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}

View file

@ -1,3 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
@ -8,7 +9,7 @@ part 'search_result_provider.g.dart';
/// The provider for the search result. /// The provider for the search result.
@riverpod @riverpod
FutureOr<LibrarySearchResponse?> searchResult( FutureOr<LibrarySearchResponse?> searchResult(
SearchResultRef ref, Ref ref,
String query, { String query, {
int limit = 25, int limit = 25,
}) async { }) async {

View file

@ -6,184 +6,94 @@ part of 'search_result_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$searchResultHash() => r'9baa643cce24f3a5e022f42202e423373939ef95'; // GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// The provider for the search result. /// The provider for the search result.
///
/// Copied from [searchResult].
@ProviderFor(searchResult) @ProviderFor(searchResult)
const searchResultProvider = SearchResultFamily(); final searchResultProvider = SearchResultFamily._();
/// The provider for the search result. /// The provider for the search result.
///
/// Copied from [searchResult]. final class SearchResultProvider
class SearchResultFamily extends Family<AsyncValue<LibrarySearchResponse?>> { extends
$FunctionalProvider<
AsyncValue<LibrarySearchResponse?>,
LibrarySearchResponse?,
FutureOr<LibrarySearchResponse?>
>
with
$FutureModifier<LibrarySearchResponse?>,
$FutureProvider<LibrarySearchResponse?> {
/// The provider for the search result. /// The provider for the search result.
/// SearchResultProvider._({
/// Copied from [searchResult]. required SearchResultFamily super.from,
const SearchResultFamily(); required (String, {int limit}) super.argument,
}) : super(
/// The provider for the search result. retry: null,
///
/// Copied from [searchResult].
SearchResultProvider call(
String query, {
int limit = 25,
}) {
return SearchResultProvider(
query,
limit: limit,
);
}
@override
SearchResultProvider getProviderOverride(
covariant SearchResultProvider provider,
) {
return call(
provider.query,
limit: provider.limit,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'searchResultProvider';
}
/// The provider for the search result.
///
/// Copied from [searchResult].
class SearchResultProvider
extends AutoDisposeFutureProvider<LibrarySearchResponse?> {
/// The provider for the search result.
///
/// Copied from [searchResult].
SearchResultProvider(
String query, {
int limit = 25,
}) : this._internal(
(ref) => searchResult(
ref as SearchResultRef,
query,
limit: limit,
),
from: searchResultProvider,
name: r'searchResultProvider', name: r'searchResultProvider',
debugGetCreateSourceHash: isAutoDispose: true,
const bool.fromEnvironment('dart.vm.product') dependencies: null,
? null $allTransitiveDependencies: null,
: _$searchResultHash,
dependencies: SearchResultFamily._dependencies,
allTransitiveDependencies:
SearchResultFamily._allTransitiveDependencies,
query: query,
limit: limit,
); );
SearchResultProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.query,
required this.limit,
}) : super.internal();
final String query;
final int limit;
@override @override
Override overrideWith( String debugGetCreateSourceHash() => _$searchResultHash();
FutureOr<LibrarySearchResponse?> Function(SearchResultRef provider) create,
) { @override
return ProviderOverride( String toString() {
origin: this, return r'searchResultProvider'
override: SearchResultProvider._internal( ''
(ref) => create(ref as SearchResultRef), '$argument';
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
query: query,
limit: limit,
),
);
} }
@$internal
@override @override
AutoDisposeFutureProviderElement<LibrarySearchResponse?> createElement() { $FutureProviderElement<LibrarySearchResponse?> $createElement(
return _SearchResultProviderElement(this); $ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<LibrarySearchResponse?> create(Ref ref) {
final argument = this.argument as (String, {int limit});
return searchResult(ref, argument.$1, limit: argument.limit);
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is SearchResultProvider && return other is SearchResultProvider && other.argument == argument;
other.query == query &&
other.limit == limit;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); return argument.hashCode;
hash = _SystemHash.combine(hash, query.hashCode);
hash = _SystemHash.combine(hash, limit.hashCode);
return _SystemHash.finish(hash);
} }
} }
mixin SearchResultRef on AutoDisposeFutureProviderRef<LibrarySearchResponse?> { String _$searchResultHash() => r'33785de298ad0d53c9d21e8fec88ba2f22f1363f';
/// The parameter `query` of this provider.
String get query;
/// The parameter `limit` of this provider. /// The provider for the search result.
int get limit;
}
class _SearchResultProviderElement final class SearchResultFamily extends $Family
extends AutoDisposeFutureProviderElement<LibrarySearchResponse?> with
with SearchResultRef { $FunctionalFamilyOverride<
_SearchResultProviderElement(super.provider); FutureOr<LibrarySearchResponse?>,
(String, {int limit})
> {
SearchResultFamily._()
: super(
retry: null,
name: r'searchResultProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// The provider for the search result.
SearchResultProvider call(String query, {int limit = 25}) =>
SearchResultProvider._(argument: (query, limit: limit), from: this);
@override @override
String get query => (origin as SearchResultProvider).query; String toString() => r'searchResultProvider';
@override
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

View file

@ -28,19 +28,14 @@ class ExplorePage extends HookConsumerWidget {
final settings = ref.watch(appSettingsProvider); final settings = ref.watch(appSettingsProvider);
final api = ref.watch(authenticatedApiProvider); final api = ref.watch(authenticatedApiProvider);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: const Text('Explore')),
title: const Text('Explore'),
backgroundColor: Colors.transparent,
),
body: const MySearchBar(), body: const MySearchBar(),
); );
} }
} }
class MySearchBar extends HookConsumerWidget { class MySearchBar extends HookConsumerWidget {
const MySearchBar({ const MySearchBar({super.key});
super.key,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -62,8 +57,11 @@ class MySearchBar extends HookConsumerWidget {
currentQuery = query; currentQuery = query;
// In a real application, there should be some error handling here. // In a real application, there should be some error handling here.
final options = await api.libraries final options = await api.libraries.search(
.search(libraryId: settings.activeLibraryId!, query: query, limit: 3); libraryId: settings.activeLibraryId!,
query: query,
limit: 3,
);
// If another search happened after this one, throw away these options. // If another search happened after this one, throw away these options.
if (currentQuery != query) { if (currentQuery != query) {
@ -98,8 +96,9 @@ class MySearchBar extends HookConsumerWidget {
// opacity: 0.5 for the hint text // opacity: 0.5 for the hint text
hintStyle: WidgetStatePropertyAll( hintStyle: WidgetStatePropertyAll(
Theme.of(context).textTheme.bodyMedium!.copyWith( Theme.of(context).textTheme.bodyMedium!.copyWith(
color: color: Theme.of(
Theme.of(context).colorScheme.onSurface.withOpacity(0.5), context,
).colorScheme.onSurface.withValues(alpha: 0.5),
), ),
), ),
textInputAction: TextInputAction.search, textInputAction: TextInputAction.search,
@ -119,12 +118,7 @@ class MySearchBar extends HookConsumerWidget {
); );
}, },
viewOnSubmitted: (value) { viewOnSubmitted: (value) {
context.pushNamed( context.pushNamed(Routes.search.name, queryParameters: {'q': value});
Routes.search.name,
queryParameters: {
'q': value,
},
);
}, },
suggestionsBuilder: (context, controller) async { suggestionsBuilder: (context, controller) async {
// check if the search controller is empty // check if the search controller is empty
@ -190,14 +184,12 @@ List<Widget> buildBookSearchResult(
SearchResultMiniSection( SearchResultMiniSection(
// title: 'Books', // title: 'Books',
category: SearchResultCategory.books, category: SearchResultCategory.books,
options: options.book.map( options: options.book.map((result) {
(result) {
// convert result to a book object // convert result to a book object
final book = result.libraryItem.media.asBookExpanded; final book = result.libraryItem.media.asBookExpanded;
final metadata = book.metadata.asBookMetadataExpanded; final metadata = book.metadata.asBookMetadataExpanded;
return BookSearchResultMini(book: book, metadata: metadata); return BookSearchResultMini(book: book, metadata: metadata);
}, }),
),
), ),
); );
} }
@ -206,11 +198,9 @@ List<Widget> buildBookSearchResult(
SearchResultMiniSection( SearchResultMiniSection(
// title: 'Authors', // title: 'Authors',
category: SearchResultCategory.authors, category: SearchResultCategory.authors,
options: options.authors.map( options: options.authors.map((result) {
(result) {
return ListTile(title: Text(result.name)); return ListTile(title: Text(result.name));
}, }),
),
), ),
); );
} }
@ -231,7 +221,7 @@ class BookSearchResultMini extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final item = ref.watch(libraryItemProvider(book.libraryItemId)).valueOrNull; final item = ref.watch(libraryItemProvider(book.libraryItemId)).value;
final image = item == null final image = item == null
? const AsyncValue.loading() ? const AsyncValue.loading()
: ref.watch(coverImageProvider(item.id)); : ref.watch(coverImageProvider(item.id));
@ -244,10 +234,7 @@ class BookSearchResultMini extends HookConsumerWidget {
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(5), borderRadius: BorderRadius.circular(5),
child: image.when( child: image.when(
data: (bytes) => Image.memory( data: (bytes) => Image.memory(bytes, fit: BoxFit.cover),
bytes,
fit: BoxFit.cover,
),
loading: () => const BookCoverSkeleton(), loading: () => const BookCoverSkeleton(),
error: (error, _) => const Icon(Icons.error), error: (error, _) => const Icon(Icons.error),
), ),
@ -258,11 +245,7 @@ class BookSearchResultMini extends HookConsumerWidget {
subtitle: Text( subtitle: Text(
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
metadata.authors metadata.authors.map((author) => author.name).join(', '),
.map(
(author) => author.name,
)
.join(', '),
), ),
onTap: () { onTap: () {
// navigate to the book details page // navigate to the book details page

View file

@ -5,13 +5,7 @@ import 'package:vaani/features/explore/providers/search_result_provider.dart';
import 'package:vaani/features/explore/view/explore_page.dart'; import 'package:vaani/features/explore/view/explore_page.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
enum SearchResultCategory { enum SearchResultCategory { books, authors, series, tags, narrators }
books,
authors,
series,
tags,
narrators,
}
class SearchResultPage extends HookConsumerWidget { class SearchResultPage extends HookConsumerWidget {
const SearchResultPage({ const SearchResultPage({
@ -41,9 +35,7 @@ class SearchResultPage extends HookConsumerWidget {
body: results.when( body: results.when(
data: (options) { data: (options) {
if (options == null) { if (options == null) {
return Container( return Container(child: const Text('No data found'));
child: const Text('No data found'),
);
} }
if (options is BookLibrarySearchResponse) { if (options is BookLibrarySearchResponse) {
if (category == null) { if (category == null) {
@ -57,10 +49,7 @@ class SearchResultPage extends HookConsumerWidget {
options.book[index].libraryItem.media.asBookExpanded; options.book[index].libraryItem.media.asBookExpanded;
final metadata = book.metadata.asBookMetadataExpanded; final metadata = book.metadata.asBookMetadataExpanded;
return BookSearchResultMini( return BookSearchResultMini(book: book, metadata: metadata);
book: book,
metadata: metadata,
);
}, },
), ),
SearchResultCategory.authors => Container(), SearchResultCategory.authors => Container(),
@ -71,12 +60,8 @@ class SearchResultPage extends HookConsumerWidget {
} }
return null; return null;
}, },
loading: () => const Center( loading: () => const Center(child: CircularProgressIndicator()),
child: CircularProgressIndicator(), error: (error, stackTrace) => Center(child: Text('Error: $error')),
),
error: (error, stackTrace) => Center(
child: Text('Error: $error'),
),
), ),
); );
} }

View file

@ -26,16 +26,13 @@ import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/utils.dart';
class LibraryItemActions extends HookConsumerWidget { class LibraryItemActions extends HookConsumerWidget {
const LibraryItemActions({ const LibraryItemActions({super.key, required this.id});
super.key,
required this.id,
});
final String id; final String id;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final item = ref.watch(libraryItemProvider(id)).valueOrNull; final item = ref.watch(libraryItemProvider(id)).value;
if (item == null) { if (item == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -68,9 +65,7 @@ class LibraryItemActions extends HookConsumerWidget {
// read list button // read list button
IconButton( IconButton(
onPressed: () {}, onPressed: () {},
icon: const Icon( icon: const Icon(Icons.playlist_add_rounded),
Icons.playlist_add_rounded,
),
), ),
// share button // share button
IconButton( IconButton(
@ -79,8 +74,9 @@ class LibraryItemActions extends HookConsumerWidget {
var currentServerUrl = var currentServerUrl =
apiSettings.activeServer!.serverUrl; apiSettings.activeServer!.serverUrl;
if (!currentServerUrl.hasScheme) { if (!currentServerUrl.hasScheme) {
currentServerUrl = currentServerUrl = Uri.https(
Uri.https(currentServerUrl.toString()); currentServerUrl.toString(),
);
} }
handleLaunchUrl( handleLaunchUrl(
Uri.parse( Uri.parse(
@ -140,7 +136,8 @@ class LibraryItemActions extends HookConsumerWidget {
.database .database
.deleteRecordWithId( .deleteRecordWithId(
record record
.task.taskId, .task
.taskId,
); );
Navigator.pop(context); Navigator.pop(context);
}, },
@ -182,16 +179,13 @@ class LibraryItemActions extends HookConsumerWidget {
loading: () => const Center( loading: () => const Center(
child: CircularProgressIndicator(), child: CircularProgressIndicator(),
), ),
error: (error, stackTrace) => Center( error: (error, stackTrace) =>
child: Text('Error: $error'), Center(child: Text('Error: $error')),
),
); );
}, },
); );
}, },
icon: const Icon( icon: const Icon(Icons.more_vert_rounded),
Icons.more_vert_rounded,
),
), ),
], ],
), ),
@ -206,25 +200,20 @@ class LibraryItemActions extends HookConsumerWidget {
} }
class LibItemDownloadButton extends HookConsumerWidget { class LibItemDownloadButton extends HookConsumerWidget {
const LibItemDownloadButton({ const LibItemDownloadButton({super.key, required this.item});
super.key,
required this.item,
});
final shelfsdk.LibraryItemExpanded item; final shelfsdk.LibraryItemExpanded item;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final isItemDownloaded = ref.watch(isItemDownloadedProvider(item)); final isItemDownloaded = ref.watch(isItemDownloadedProvider(item));
if (isItemDownloaded.valueOrNull ?? false) { if (isItemDownloaded.value ?? false) {
return AlreadyItemDownloadedButton(item: item); return AlreadyItemDownloadedButton(item: item);
} }
final isItemDownloading = ref.watch(isItemDownloadingProvider(item.id)); final isItemDownloading = ref.watch(isItemDownloadingProvider(item.id));
return isItemDownloading return isItemDownloading
? ItemCurrentlyInDownloadQueue( ? ItemCurrentlyInDownloadQueue(item: item)
item: item,
)
: IconButton( : IconButton(
onPressed: () { onPressed: () {
appLogger.fine('Pressed download button'); appLogger.fine('Pressed download button');
@ -233,18 +222,13 @@ class LibItemDownloadButton extends HookConsumerWidget {
.read(downloadManagerProvider.notifier) .read(downloadManagerProvider.notifier)
.queueAudioBookDownload(item); .queueAudioBookDownload(item);
}, },
icon: const Icon( icon: const Icon(Icons.download_rounded),
Icons.download_rounded,
),
); );
} }
} }
class ItemCurrentlyInDownloadQueue extends HookConsumerWidget { class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
const ItemCurrentlyInDownloadQueue({ const ItemCurrentlyInDownloadQueue({super.key, required this.item});
super.key,
required this.item,
});
final shelfsdk.LibraryItemExpanded item; final shelfsdk.LibraryItemExpanded item;
@ -252,7 +236,7 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final progress = ref final progress = ref
.watch(itemDownloadProgressProvider(item.id)) .watch(itemDownloadProgressProvider(item.id))
.valueOrNull .value
?.clamp(0.05, 1.0); ?.clamp(0.05, 1.0);
if (progress == 1) { if (progress == 1) {
@ -263,17 +247,12 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
return Stack( return Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
CircularProgressIndicator( CircularProgressIndicator(value: progress, strokeWidth: 2),
value: progress,
strokeWidth: 2,
),
const Icon( const Icon(
Icons.download, Icons.download,
// color: Theme.of(context).progressIndicatorTheme.color, // color: Theme.of(context).progressIndicatorTheme.color,
) )
.animate( .animate(onPlay: (controller) => controller.repeat())
onPlay: (controller) => controller.repeat(),
)
.fade( .fade(
duration: shimmerDuration, duration: shimmerDuration,
end: 1, end: 1,
@ -292,10 +271,7 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
} }
class AlreadyItemDownloadedButton extends HookConsumerWidget { class AlreadyItemDownloadedButton extends HookConsumerWidget {
const AlreadyItemDownloadedButton({ const AlreadyItemDownloadedButton({super.key, required this.item});
super.key,
required this.item,
});
final shelfsdk.LibraryItemExpanded item; final shelfsdk.LibraryItemExpanded item;
@ -317,25 +293,18 @@ class AlreadyItemDownloadedButton extends HookConsumerWidget {
top: 8.0, top: 8.0,
bottom: (isBookPlaying ? playerMinHeight : 0) + 8, bottom: (isBookPlaying ? playerMinHeight : 0) + 8,
), ),
child: DownloadSheet( child: DownloadSheet(item: item),
item: item,
),
); );
}, },
); );
}, },
icon: const Icon( icon: const Icon(Icons.download_done_rounded),
Icons.download_done_rounded,
),
); );
} }
} }
class DownloadSheet extends HookConsumerWidget { class DownloadSheet extends HookConsumerWidget {
const DownloadSheet({ const DownloadSheet({super.key, required this.item});
super.key,
required this.item,
});
final shelfsdk.LibraryItemExpanded item; final shelfsdk.LibraryItemExpanded item;
@ -367,9 +336,7 @@ class DownloadSheet extends HookConsumerWidget {
// ), // ),
ListTile( ListTile(
title: const Text('Delete'), title: const Text('Delete'),
leading: const Icon( leading: const Icon(Icons.delete_rounded),
Icons.delete_rounded,
),
onTap: () async { onTap: () async {
// show the delete dialog // show the delete dialog
final wasDeleted = await showDialog<bool>( final wasDeleted = await showDialog<bool>(
@ -387,9 +354,7 @@ class DownloadSheet extends HookConsumerWidget {
// delete the file // delete the file
ref ref
.read(downloadManagerProvider.notifier) .read(downloadManagerProvider.notifier)
.deleteDownloadedItem( .deleteDownloadedItem(item);
item,
);
GoRouter.of(context).pop(true); GoRouter.of(context).pop(true);
}, },
child: const Text('Yes'), child: const Text('Yes'),
@ -409,11 +374,7 @@ class DownloadSheet extends HookConsumerWidget {
appLogger.fine('Deleted ${item.media.metadata.title}'); appLogger.fine('Deleted ${item.media.metadata.title}');
GoRouter.of(context).pop(); GoRouter.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(content: Text('Deleted ${item.media.metadata.title}')),
content: Text(
'Deleted ${item.media.metadata.title}',
),
),
); );
} }
}, },
@ -424,10 +385,7 @@ class DownloadSheet extends HookConsumerWidget {
} }
class _LibraryItemPlayButton extends HookConsumerWidget { class _LibraryItemPlayButton extends HookConsumerWidget {
const _LibraryItemPlayButton({ const _LibraryItemPlayButton({required this.item});
super.key,
required this.item,
});
final shelfsdk.LibraryItemExpanded item; final shelfsdk.LibraryItemExpanded item;
@ -478,9 +436,7 @@ class _LibraryItemPlayButton extends HookConsumerWidget {
), ),
label: Text(getPlayDisplayText()), label: Text(getPlayDisplayText()),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
borderRadius: BorderRadius.circular(4),
),
), ),
); );
} }
@ -518,7 +474,7 @@ Future<void> libraryItemPlayButtonOnPressed({
required shelfsdk.BookExpanded book, required shelfsdk.BookExpanded book,
shelfsdk.MediaProgress? userMediaProgress, shelfsdk.MediaProgress? userMediaProgress,
}) async { }) async {
debugPrint('Pressed play/resume button'); appLogger.info('Pressed play/resume button');
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(audiobookPlayerProvider);
final isCurrentBookSetInPlayer = player.book == book; final isCurrentBookSetInPlayer = player.book == book;
@ -527,11 +483,12 @@ Future<void> libraryItemPlayButtonOnPressed({
Future<void>? setSourceFuture; Future<void>? setSourceFuture;
// set the book to the player if not already set // set the book to the player if not already set
if (!isCurrentBookSetInPlayer) { if (!isCurrentBookSetInPlayer) {
debugPrint('Setting the book ${book.libraryItemId}'); appLogger.info('Setting the book ${book.libraryItemId}');
debugPrint('Initial position: ${userMediaProgress?.currentTime}'); appLogger.info('Initial position: ${userMediaProgress?.currentTime}');
final downloadManager = ref.watch(simpleDownloadManagerProvider); final downloadManager = ref.watch(simpleDownloadManagerProvider);
final libItem = final libItem = await ref.read(
await ref.read(libraryItemProvider(book.libraryItemId).future); libraryItemProvider(book.libraryItemId).future,
);
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
setSourceFuture = player.setSourceAudiobook( setSourceFuture = player.setSourceAudiobook(
book, book,
@ -539,16 +496,17 @@ Future<void> libraryItemPlayButtonOnPressed({
downloadedUris: downloadedUris, downloadedUris: downloadedUris,
); );
} else { } else {
debugPrint('Book was already set'); appLogger.info('Book was already set');
if (isPlayingThisBook) { if (isPlayingThisBook) {
debugPrint('Pausing the book'); appLogger.info('Pausing the book');
await player.pause(); await player.pause();
return; return;
} }
} }
// set the volume as this is the first time playing and dismissing causes the volume to go to 0 // set the volume as this is the first time playing and dismissing causes the volume to go to 0
var bookPlayerSettings = var bookPlayerSettings = ref
ref.read(bookSettingsProvider(book.libraryItemId)).playerSettings; .read(bookSettingsProvider(book.libraryItemId))
.playerSettings;
var appPlayerSettings = ref.read(appSettingsProvider).playerSettings; var appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
var configurePlayerForEveryBook = var configurePlayerForEveryBook =

View file

@ -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/duration_format.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.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 { class LibraryItemHeroSection extends HookConsumerWidget {
const LibraryItemHeroSection({ const LibraryItemHeroSection({
@ -42,14 +42,13 @@ class LibraryItemHeroSection extends HookConsumerWidget {
child: Column( child: Column(
children: [ children: [
Hero( Hero(
tag: HeroTagPrefixes.bookCover + tag:
HeroTagPrefixes.bookCover +
itemId + itemId +
(extraMap?.heroTagSuffix ?? ''), (extraMap?.heroTagSuffix ?? ''),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: _BookCover( child: _BookCover(itemId: itemId),
itemId: itemId,
),
), ),
), ),
// a progress bar // a progress bar
@ -59,9 +58,7 @@ class LibraryItemHeroSection extends HookConsumerWidget {
right: 8.0, right: 8.0,
left: 8.0, left: 8.0,
), ),
child: _LibraryItemProgressIndicator( child: _LibraryItemProgressIndicator(id: itemId),
id: itemId,
),
), ),
], ],
), ),
@ -77,11 +74,7 @@ class LibraryItemHeroSection extends HookConsumerWidget {
} }
class _BookDetails extends HookConsumerWidget { class _BookDetails extends HookConsumerWidget {
const _BookDetails({ const _BookDetails({required this.id, this.extraMap});
super.key,
required this.id,
this.extraMap,
});
final String id; final String id;
final LibraryItemExtras? extraMap; final LibraryItemExtras? extraMap;
@ -91,7 +84,7 @@ class _BookDetails extends HookConsumerWidget {
final itemFromApi = ref.watch(libraryItemProvider(id)); final itemFromApi = ref.watch(libraryItemProvider(id));
final itemBookMetadata = final itemBookMetadata =
itemFromApi.valueOrNull?.media.metadata.asBookMetadataExpanded; itemFromApi.value?.media.metadata.asBookMetadataExpanded;
return Expanded( return Expanded(
child: Padding( child: Padding(
@ -100,10 +93,7 @@ class _BookDetails extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: [ children: [
_BookTitle( _BookTitle(extraMap: extraMap, itemBookMetadata: itemBookMetadata),
extraMap: extraMap,
itemBookMetadata: itemBookMetadata,
),
Container( Container(
margin: const EdgeInsets.symmetric(vertical: 16), margin: const EdgeInsets.symmetric(vertical: 16),
child: Column( child: Column(
@ -135,17 +125,14 @@ class _BookDetails extends HookConsumerWidget {
} }
class _LibraryItemProgressIndicator extends HookConsumerWidget { class _LibraryItemProgressIndicator extends HookConsumerWidget {
const _LibraryItemProgressIndicator({ const _LibraryItemProgressIndicator({required this.id});
super.key,
required this.id,
});
final String id; final String id;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider); final player = ref.watch(audiobookPlayerProvider);
final libraryItem = ref.watch(libraryItemProvider(id)).valueOrNull; final libraryItem = ref.watch(libraryItemProvider(id)).value;
if (libraryItem == null) { if (libraryItem == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@ -159,13 +146,15 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
Duration remainingTime; Duration remainingTime;
if (player.book?.libraryItemId == libraryItem.id) { if (player.book?.libraryItemId == libraryItem.id) {
// final positionStream = useStream(player.slowPositionStream); // final positionStream = useStream(player.slowPositionStream);
progress = (player.positionInBook).inSeconds / progress =
(player.positionInBook).inSeconds /
libraryItem.media.asBookExpanded.duration.inSeconds; libraryItem.media.asBookExpanded.duration.inSeconds;
remainingTime = remainingTime =
libraryItem.media.asBookExpanded.duration - player.positionInBook; libraryItem.media.asBookExpanded.duration - player.positionInBook;
} else { } else {
progress = mediaProgress?.progress ?? 0; progress = mediaProgress?.progress ?? 0;
remainingTime = (libraryItem.media.asBookExpanded.duration - remainingTime =
(libraryItem.media.asBookExpanded.duration -
mediaProgress!.currentTime); mediaProgress!.currentTime);
} }
@ -192,17 +181,16 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
semanticsLabel: 'Book progress', semanticsLabel: 'Book progress',
semanticsValue: '${progressInPercent.toStringAsFixed(2)}%', semanticsValue: '${progressInPercent.toStringAsFixed(2)}%',
), ),
const SizedBox.square( const SizedBox.square(dimension: 4.0),
dimension: 4.0,
),
// time remaining // time remaining
Text( Text(
// only show 2 decimal places // only show 2 decimal places
'${remainingTime.smartBinaryFormat} left', '${remainingTime.smartBinaryFormat} left',
style: Theme.of(context).textTheme.bodySmall?.copyWith( style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: color: Theme.of(
Theme.of(context).colorScheme.onSurface.withOpacity(0.75), context,
).colorScheme.onSurface.withValues(alpha: 0.75),
), ),
), ),
], ],
@ -212,11 +200,7 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
} }
class _HeroSectionSubLabelWithIcon extends HookConsumerWidget { class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
const _HeroSectionSubLabelWithIcon({ const _HeroSectionSubLabelWithIcon({required this.icon, required this.text});
super.key,
required this.icon,
required this.text,
});
final IconData icon; final IconData icon;
final Widget text; final Widget text;
@ -226,11 +210,13 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
final themeData = Theme.of(context); final themeData = Theme.of(context);
final useFontAwesome = final useFontAwesome =
icon.runtimeType == FontAwesomeIcons.book.runtimeType; icon.runtimeType == FontAwesomeIcons.book.runtimeType;
final useMaterialThemeOnItemPage = final useMaterialThemeOnItemPage = ref
ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage; .watch(appSettingsProvider)
.themeSettings
.useMaterialThemeOnItemPage;
final color = useMaterialThemeOnItemPage final color = useMaterialThemeOnItemPage
? themeData.colorScheme.primary ? themeData.colorScheme.primary
: themeData.colorScheme.onSurface.withOpacity(0.75); : themeData.colorScheme.onSurface.withValues(alpha: 0.75);
return Padding( return Padding(
padding: const EdgeInsets.only(bottom: 8.0), padding: const EdgeInsets.only(bottom: 8.0),
child: Row( child: Row(
@ -238,20 +224,10 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
Container( Container(
margin: const EdgeInsets.only(right: 8, top: 2), margin: const EdgeInsets.only(right: 8, top: 2),
child: useFontAwesome child: useFontAwesome
? FaIcon( ? FaIcon(icon, size: 16, color: color)
icon, : Icon(icon, size: 16, color: color),
size: 16,
color: color,
)
: Icon(
icon,
size: 16,
color: color,
),
),
Expanded(
child: text,
), ),
Expanded(child: text),
], ],
), ),
); );
@ -260,7 +236,6 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
class _BookSeries extends StatelessWidget { class _BookSeries extends StatelessWidget {
const _BookSeries({ const _BookSeries({
super.key,
required this.itemBookMetadata, required this.itemBookMetadata,
required this.bookDetailsCached, required this.bookDetailsCached,
}); });
@ -306,7 +281,6 @@ class _BookSeries extends StatelessWidget {
class _BookNarrators extends StatelessWidget { class _BookNarrators extends StatelessWidget {
const _BookNarrators({ const _BookNarrators({
super.key,
required this.itemBookMetadata, required this.itemBookMetadata,
required this.bookDetailsCached, required this.bookDetailsCached,
}); });
@ -341,10 +315,7 @@ class _BookNarrators extends StatelessWidget {
} }
class _BookCover extends HookConsumerWidget { class _BookCover extends HookConsumerWidget {
const _BookCover({ const _BookCover({required this.itemId});
super.key,
required this.itemId,
});
final String itemId; final String itemId;
@ -353,25 +324,27 @@ class _BookCover extends HookConsumerWidget {
final coverImage = ref.watch(coverImageProvider(itemId)); final coverImage = ref.watch(coverImageProvider(itemId));
final themeData = Theme.of(context); final themeData = Theme.of(context);
// final item = ref.watch(libraryItemProvider(itemId)); // final item = ref.watch(libraryItemProvider(itemId));
final useMaterialThemeOnItemPage = final themeSettings = ref.watch(appSettingsProvider).themeSettings;
ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage;
ColorScheme? coverColorScheme; ColorScheme? coverColorScheme;
if (useMaterialThemeOnItemPage) { if (themeSettings.useMaterialThemeOnItemPage) {
coverColorScheme = ref coverColorScheme = ref
.watch( .watch(
themeOfLibraryItemProvider( themeOfLibraryItemProvider(
itemId, itemId,
brightness: Theme.of(context).brightness, brightness: Theme.of(context).brightness,
highContrast:
themeSettings.highContrast ||
MediaQuery.of(context).highContrast,
), ),
) )
.valueOrNull; .value;
} }
return ThemeSwitcher( return ThemeSwitcher(
builder: (context) { builder: (context) {
// change theme after 2 seconds // change theme after 2 seconds
if (useMaterialThemeOnItemPage) { if (themeSettings.useMaterialThemeOnItemPage) {
Future.delayed(150.ms, () { Future.delayed(150.ms, () {
try { try {
ThemeSwitcher.of(context).changeTheme( ThemeSwitcher.of(context).changeTheme(
@ -383,7 +356,7 @@ class _BookCover extends HookConsumerWidget {
: themeData, : themeData,
); );
} catch (e) { } catch (e) {
appLogger.shout('Error changing theme: $e'); appLogger.severe('Error changing theme: $e');
} }
}); });
} }
@ -394,15 +367,10 @@ class _BookCover extends HookConsumerWidget {
return const Icon(Icons.error); return const Icon(Icons.error);
} }
return Image.memory( return Image.memory(image, fit: BoxFit.cover);
image,
fit: BoxFit.cover,
);
}, },
loading: () { loading: () {
return const Center( return const Center(child: BookCoverSkeleton());
child: BookCoverSkeleton(),
);
}, },
error: (error, stack) { error: (error, stack) {
return const Center(child: Icon(Icons.error)); return const Center(child: Icon(Icons.error));
@ -414,11 +382,7 @@ class _BookCover extends HookConsumerWidget {
} }
class _BookTitle extends StatelessWidget { class _BookTitle extends StatelessWidget {
const _BookTitle({ const _BookTitle({required this.extraMap, required this.itemBookMetadata});
super.key,
required this.extraMap,
required this.itemBookMetadata,
});
final LibraryItemExtras? extraMap; final LibraryItemExtras? extraMap;
final shelfsdk.BookMetadataExpanded? itemBookMetadata; final shelfsdk.BookMetadataExpanded? itemBookMetadata;
@ -430,7 +394,8 @@ class _BookTitle extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Hero( Hero(
tag: HeroTagPrefixes.bookTitle + tag:
HeroTagPrefixes.bookTitle +
// itemId + // itemId +
(extraMap?.heroTagSuffix ?? ''), (extraMap?.heroTagSuffix ?? ''),
child: Text( child: Text(
@ -449,7 +414,7 @@ class _BookTitle extends StatelessWidget {
? const SizedBox.shrink() ? const SizedBox.shrink()
: Text( : Text(
style: themeData.textTheme.titleSmall?.copyWith( style: themeData.textTheme.titleSmall?.copyWith(
color: themeData.colorScheme.onSurface.withOpacity(0.8), color: themeData.colorScheme.onSurface.withValues(alpha: 0.8),
), ),
itemBookMetadata?.subtitle ?? '', itemBookMetadata?.subtitle ?? '',
), ),
@ -460,7 +425,6 @@ class _BookTitle extends StatelessWidget {
class _BookAuthors extends StatelessWidget { class _BookAuthors extends StatelessWidget {
const _BookAuthors({ const _BookAuthors({
super.key,
required this.itemBookMetadata, required this.itemBookMetadata,
required this.bookDetailsCached, required this.bookDetailsCached,
}); });

View file

@ -4,16 +4,13 @@ import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
class LibraryItemMetadata extends HookConsumerWidget { class LibraryItemMetadata extends HookConsumerWidget {
const LibraryItemMetadata({ const LibraryItemMetadata({super.key, required this.id});
super.key,
required this.id,
});
final String id; final String id;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final item = ref.watch(libraryItemProvider(id)).valueOrNull; final item = ref.watch(libraryItemProvider(id)).value;
/// formats the duration of the book as `10h 30m` /// formats the duration of the book as `10h 30m`
/// ///
@ -72,7 +69,8 @@ class LibraryItemMetadata extends HookConsumerWidget {
), ),
_MetadataItem( _MetadataItem(
title: 'Published', title: 'Published',
value: itemBookMetadata?.publishedDate ?? value:
itemBookMetadata?.publishedDate ??
itemBookMetadata?.publishedYear ?? itemBookMetadata?.publishedYear ??
'Unknown', 'Unknown',
), ),
@ -87,19 +85,18 @@ class LibraryItemMetadata extends HookConsumerWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// alternate between metadata and vertical divider // alternate between metadata and vertical divider
children: List.generate( children: List.generate(children.length * 2 - 1, (index) {
children.length * 2 - 1,
(index) {
if (index.isEven) { if (index.isEven) {
return children[index ~/ 2]; return children[index ~/ 2];
} }
return VerticalDivider( return VerticalDivider(
indent: 6, indent: 6,
endIndent: 6, endIndent: 6,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6), color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
); );
}, }),
),
), ),
), ),
); );
@ -108,11 +105,7 @@ class LibraryItemMetadata extends HookConsumerWidget {
/// key-value pair to display as column /// key-value pair to display as column
class _MetadataItem extends StatelessWidget { class _MetadataItem extends StatelessWidget {
const _MetadataItem({ const _MetadataItem({required this.title, required this.value});
super.key,
required this.title,
required this.value,
});
final String title; final String title;
final String value; final String value;
@ -126,7 +119,7 @@ class _MetadataItem extends StatelessWidget {
children: [ children: [
Text( Text(
style: themeData.textTheme.titleMedium?.copyWith( style: themeData.textTheme.titleMedium?.copyWith(
color: themeData.colorScheme.onSurface.withOpacity(0.90), color: themeData.colorScheme.onSurface.withValues(alpha: 0.90),
), ),
value, value,
maxLines: 1, maxLines: 1,
@ -134,7 +127,7 @@ class _MetadataItem extends StatelessWidget {
), ),
Text( Text(
style: themeData.textTheme.bodySmall?.copyWith( style: themeData.textTheme.bodySmall?.copyWith(
color: themeData.colorScheme.onSurface.withOpacity(0.7), color: themeData.colorScheme.onSurface.withValues(alpha: 0.7),
), ),
title, title,
maxLines: 1, maxLines: 1,

View file

@ -3,10 +3,11 @@ import 'dart:math';
import 'package:animated_theme_switcher/animated_theme_switcher.dart'; import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/library_item_provider.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/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/router/models/library_item_extras.dart';
import 'package:vaani/shared/widgets/expandable_description.dart'; import 'package:vaani/shared/widgets/expandable_description.dart';
@ -15,27 +16,86 @@ import 'library_item_hero_section.dart';
import 'library_item_metadata.dart'; import 'library_item_metadata.dart';
class LibraryItemPage extends HookConsumerWidget { class LibraryItemPage extends HookConsumerWidget {
const LibraryItemPage({ const LibraryItemPage({super.key, required this.itemId, this.extra});
super.key,
required this.itemId,
this.extra,
});
final String itemId; final String itemId;
final Object? extra; final Object? extra;
static const double _showFabThreshold = 300.0;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final additionalItemData = final additionalItemData = extra is LibraryItemExtras
extra is LibraryItemExtras ? extra as LibraryItemExtras : null; ? 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( return ThemeProvider(
initTheme: Theme.of(context), initTheme: Theme.of(context),
duration: 200.ms, duration: 200.ms,
child: ThemeSwitchingArea( child: ThemeSwitchingArea(
child: Scaffold( child: Scaffold(
floatingActionButton: AnimatedSwitcher(
duration: 250.ms,
// A common transition for FABs (fade + scale)
transitionBuilder: (Widget child, Animation<double> 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( body: CustomScrollView(
controller: scrollController,
slivers: [ slivers: [
const LibraryItemSliverAppBar(), LibraryItemSliverAppBar(
id: itemId,
scrollController: scrollController,
),
SliverPadding( SliverPadding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
sliver: LibraryItemHeroSection( sliver: LibraryItemHeroSection(
@ -44,21 +104,13 @@ class LibraryItemPage extends HookConsumerWidget {
), ),
), ),
// a horizontal display with dividers of metadata // a horizontal display with dividers of metadata
SliverToBoxAdapter( SliverToBoxAdapter(child: LibraryItemMetadata(id: itemId)),
child: LibraryItemMetadata(id: itemId),
),
// a row of actions like play, download, share, etc // a row of actions like play, download, share, etc
SliverToBoxAdapter( SliverToBoxAdapter(child: LibraryItemActions(id: itemId)),
child: LibraryItemActions(id: itemId),
),
// a expandable section for book description // a expandable section for book description
SliverToBoxAdapter( SliverToBoxAdapter(child: LibraryItemDescription(id: itemId)),
child: LibraryItemDescription(id: itemId),
),
// a padding at the bottom to make sure the last item is not hidden by mini player // a padding at the bottom to make sure the last item is not hidden by mini player
const SliverToBoxAdapter( const SliverToBoxAdapter(child: MiniPlayerBottomPadding()),
child: SizedBox(height: playerMinHeight),
),
], ],
), ),
), ),
@ -68,15 +120,12 @@ class LibraryItemPage extends HookConsumerWidget {
} }
class LibraryItemDescription extends HookConsumerWidget { class LibraryItemDescription extends HookConsumerWidget {
const LibraryItemDescription({ const LibraryItemDescription({super.key, required this.id});
super.key,
required this.id,
});
final String id; final String id;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final item = ref.watch(libraryItemProvider(id)).valueOrNull; final item = ref.watch(libraryItemProvider(id)).value;
if (item == null) { if (item == null) {
return const SizedBox(); return const SizedBox();
} }
@ -91,16 +140,21 @@ class LibraryItemDescription extends HookConsumerWidget {
double calculateWidth( double calculateWidth(
BuildContext context, BuildContext context,
BoxConstraints constraints, { BoxConstraints constraints, {
/// width ratio of the cover image to the available width /// width ratio of the cover image to the available width
double widthRatio = 0.4, double widthRatio = 0.4,
/// height ratio of the cover image to the available height /// height ratio of the cover image to the available height
double maxHeightToUse = 0.25, double maxHeightToUse = 0.25,
}) { }) {
final availHeight = final availHeight = min(
min(constraints.maxHeight, MediaQuery.of(context).size.height); constraints.maxHeight,
final availWidth = MediaQuery.of(context).size.height,
min(constraints.maxWidth, MediaQuery.of(context).size.width); );
final availWidth = min(
constraints.maxWidth,
MediaQuery.of(context).size.width,
);
// make the width widthRatio of the available width // make the width widthRatio of the available width
var width = availWidth * widthRatio; var width = availWidth * widthRatio;

View file

@ -1,23 +1,78 @@
import 'package:flutter/material.dart'; 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({ const LibraryItemSliverAppBar({
super.key, super.key,
required this.id,
required this.scrollController,
}); });
final String id;
final ScrollController scrollController;
static const double _showTitleThreshold = kToolbarHeight * 0.5;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final item = ref.watch(libraryItemProvider(id)).value;
final showTitle = useState(false);
useEffect(() {
void listener() {
final shouldShow =
scrollController.hasClients &&
scrollController.offset > _showTitleThreshold;
if (showTitle.value != shouldShow) {
showTitle.value = shouldShow;
}
}
scrollController.addListener(listener);
// Trigger listener once initially in case the view starts scrolled
// (though unlikely for this specific use case, it's good practice)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
listener();
}
});
return () => scrollController.removeListener(listener);
}, [scrollController]);
return SliverAppBar( return SliverAppBar(
backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
floating: true, floating: false,
pinned: true,
primary: true, primary: true,
snap: true,
actions: [ actions: [
// cast button // IconButton(
IconButton(onPressed: () {}, icon: const Icon(Icons.cast)), // icon: const Icon(Icons.cast),
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)), // 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,
); );
} }
} }

View file

@ -1,38 +1,70 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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 { class LibraryBrowserPage extends HookConsumerWidget {
const LibraryBrowserPage({super.key}); const LibraryBrowserPage({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final currentLibrary = ref.watch(currentLibraryProvider).value;
// Determine the icon to use, with a fallback
final IconData libraryIconData =
AbsIcons.getIconByName(currentLibrary?.icon) ?? Icons.library_books;
// Determine the title text
final String appBarTitle = '${currentLibrary?.name ?? 'Your'} Library';
return Scaffold( return Scaffold(
appBar: AppBar( // Use CustomScrollView to enable slivers
title: const Text('Library'), body: CustomScrollView(
backgroundColor: Colors.transparent, slivers: <Widget>[
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);
},
), ),
// a list redirecting to authors, genres, and series pages title: Text(appBarTitle),
body: ListView( ),
children: [ SliverList(
delegate: SliverChildListDelegate([
ListTile( ListTile(
title: const Text('Authors'), title: const Text('Authors'),
leading: const Icon(Icons.person), leading: const Icon(Icons.person),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () {}, onTap: () {
showNotImplementedToast(context);
},
), ),
ListTile( ListTile(
title: const Text('Genres'), title: const Text('Genres'),
leading: const Icon(Icons.category), leading: const Icon(Icons.category),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () {}, onTap: () {
showNotImplementedToast(context);
},
), ),
ListTile( ListTile(
title: const Text('Series'), title: const Text('Series'),
leading: const Icon(Icons.list), leading: const Icon(Icons.list),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () {}, onTap: () {
showNotImplementedToast(context);
},
), ),
// Downloads // Downloads
ListTile( ListTile(
@ -43,6 +75,8 @@ class LibraryBrowserPage extends HookConsumerWidget {
GoRouter.of(context).pushNamed(Routes.downloads.name); GoRouter.of(context).pushNamed(Routes.downloads.name);
}, },
), ),
]),
),
], ],
), ),
); );

View file

@ -0,0 +1,36 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:logging_appenders/logging_appenders.dart';
import 'package:path_provider/path_provider.dart';
import 'package:vaani/shared/extensions/duration_format.dart';
Future<String> getLoggingFilePath() async {
final Directory directory = await getApplicationDocumentsDirectory();
return '${directory.path}/vaani.log';
}
Future<void> initLogging() async {
final formatter = const DefaultLogRecordFormatter();
if (kReleaseMode) {
Logger.root.level = Level.INFO; // is also the default
// Write to a file
RotatingFileAppender(
baseFilePath: await getLoggingFilePath(),
formatter: formatter,
).attachToLogger(Logger.root);
} else {
Logger.root.level = Level.FINE; // Capture all logs
RotatingFileAppender(
baseFilePath: await getLoggingFilePath(),
formatter: formatter,
).attachToLogger(Logger.root);
Logger.root.onRecord.listen((record) {
// Print log records to the console
debugPrint(
'${record.loggerName}: ${record.level.name}: ${record.time.time}: ${record.message}',
);
});
}
}

View file

@ -0,0 +1,90 @@
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/features/logging/core/logger.dart';
part 'logs_provider.g.dart';
@riverpod
class Logs extends _$Logs {
@override
Future<List<LogRecord>> build() async {
final path = await getLoggingFilePath();
final file = File(path);
if (!file.existsSync()) {
return [];
}
final lines = await file.readAsLines();
return lines.map(parseLogLine).toList();
}
Future<void> clear() async {
final path = await getLoggingFilePath();
final file = File(path);
await file.writeAsString('');
state = AsyncData([]);
}
Future<String> getZipFilePath() async {
final String targetZipPath = await generateZipFilePath();
var encoder = ZipFileEncoder();
encoder.create(targetZipPath);
final logFilePath = await getLoggingFilePath();
final logFile = File(logFilePath);
if (await logFile.exists()) {
// Check if log file exists before adding
await encoder.addFile(logFile);
} else {
// Handle case where log file doesn't exist? Maybe log a warning?
// Or create an empty file inside the zip? For now, just don't add.
debugPrint(
'Warning: Log file not found at $logFilePath, creating potentially empty zip.',
);
}
await encoder.close();
return targetZipPath;
}
}
Future<String> generateZipFilePath() async {
Directory appDocDirectory = await getTemporaryDirectory();
return '${appDocDirectory.path}/${generateZipFileName()}';
}
String generateZipFileName() {
return 'vaani-${DateTime.now().microsecondsSinceEpoch}.zip';
}
Level parseLevel(String level) {
return Level.LEVELS.firstWhere(
(l) => l.name == level,
orElse: () => Level.ALL,
);
}
LogRecord parseLogLine(String line) {
// 2024-10-03 00:48:58.012400 INFO GoRouter - getting location for name: "logs"
final RegExp logLineRegExp = RegExp(
r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) (\w+) (\w+) - (.+)',
);
final match = logLineRegExp.firstMatch(line);
if (match == null) {
// return as is
return LogRecord(Level.ALL, line, 'Unknown');
}
final timeString = match.group(1)!;
final levelString = match.group(2)!;
final loggerName = match.group(3)!;
final message = match.group(4)!;
final time = DateTime.parse(timeString);
final level = parseLevel(levelString);
return LogRecord(level, message, loggerName, time);
}

View file

@ -0,0 +1,53 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'logs_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(Logs)
final logsProvider = LogsProvider._();
final class LogsProvider extends $AsyncNotifierProvider<Logs, List<LogRecord>> {
LogsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'logsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$logsHash();
@$internal
@override
Logs create() => Logs();
}
String _$logsHash() => r'aa9d3d56586cba6ddf69615320ea605d071ea5e2';
abstract class _$Logs extends $AsyncNotifier<List<LogRecord>> {
FutureOr<List<LogRecord>> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<List<LogRecord>>, List<LogRecord>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<LogRecord>>, List<LogRecord>>,
AsyncValue<List<LogRecord>>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}

View file

@ -0,0 +1,306 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:share_plus/share_plus.dart';
import 'package:vaani/features/logging/providers/logs_provider.dart';
import 'package:vaani/main.dart';
class LogsPage extends HookConsumerWidget {
const LogsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final logs = ref.watch(logsProvider);
final theme = Theme.of(context);
final scrollController = useScrollController();
return Scaffold(
appBar: AppBar(
title: const Text('Logs'),
actions: [
IconButton(
tooltip: 'Clear logs',
icon: const Icon(Icons.delete_forever),
onPressed: () async {
// ask for confirmation
final shouldClear = await showDialog<bool>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Clear logs?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(false);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: const Text('Clear'),
),
],
);
},
);
if (shouldClear == true) {
ref.read(logsProvider.notifier).clear();
}
},
),
IconButton(
tooltip: 'Share logs',
icon: const Icon(Icons.share),
onPressed: () async {
appLogger.info('Preparing logs for sharing');
final zipLogFilePath = await ref
.read(logsProvider.notifier)
.getZipFilePath();
// submit logs
final result = await Share.shareXFiles([XFile(zipLogFilePath)]);
switch (result.status) {
case ShareResultStatus.success:
appLogger.info('Share success');
break;
case ShareResultStatus.dismissed:
appLogger.info('Share dismissed');
break;
case ShareResultStatus.unavailable:
appLogger.severe('Share unavailable');
break;
}
},
),
// downloads disabled since manage external storage permission was removed
// see https://gitlab.com/IzzyOnDroid/repo/-/issues/623#note_2240386369
// IconButton(
// tooltip: 'Download logs',
// icon: const Icon(Icons.download),
// onPressed: () async {
// appLogger.info('Preparing logs for download');
// if (Platform.isAndroid) {
// final androidVersion =
// await ref.watch(deviceSdkVersionProvider.future);
// if ((int.parse(androidVersion)) > 29) {
// final status = await Permission.storage.status;
// if (!status.isGranted) {
// appLogger
// .info('Requesting storage permission');
// final newStatus =
// await Permission.storage.request();
// if (!newStatus.isGranted) {
// appLogger
// .warning('storage permission denied');
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text('Storage permission denied'),
// ),
// );
// return;
// }
// }
// } else {
// final status = await Permission.storage.status;
// if (!status.isGranted) {
// appLogger.info('Requesting storage permission');
// final newStatus = await Permission.storage.request();
// if (!newStatus.isGranted) {
// appLogger.warning('Storage permission denied');
// ScaffoldMessenger.of(context).showSnackBar(
// const SnackBar(
// content: Text('Storage permission denied'),
// ),
// );
// return;
// }
// }
// }
// }
// final zipLogFilePath =
// await ref.read(logsProvider.notifier).getZipFilePath();
// // save to folder
// String? outputFile = await FilePicker.platform.saveFile(
// dialogTitle: 'Please select an output file:',
// fileName: zipLogFilePath.split('/').last,
// bytes: await File(zipLogFilePath).readAsBytes(),
// );
// if (outputFile != null) {
// try {
// final file = File(outputFile);
// final zipFile = File(zipLogFilePath);
// await zipFile.copy(file.path);
// appLogger.info('File saved to: $outputFile');
// } catch (e) {
// appLogger.severe('Error saving file: $e');
// }
// } else {
// appLogger.info('Download cancelled');
// }
// },
// ),
IconButton(
tooltip: 'Refresh logs',
icon: const Icon(Icons.refresh),
onPressed: () {
ref.invalidate(logsProvider);
},
),
IconButton(
tooltip: 'Scroll to top',
icon: const Icon(Icons.arrow_upward),
onPressed: () {
scrollController.animateTo(
0,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
},
),
],
),
// a column with ListView.builder and a scrollable list of logs
body: Column(
children: [
// a filter for log levels, loggers, and search
// TODO: implement filters and search
Expanded(
child: logs.when(
data: (logRecords) {
return Scrollbar(
controller: scrollController,
child: ListView.builder(
controller: scrollController,
itemCount: logRecords.length,
itemBuilder: (context, index) {
final logRecord = logRecords[index];
return LogRecordTile(logRecord: logRecord, theme: theme);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Error loading logs'),
ElevatedButton(
onPressed: () {
ref.invalidate(logsProvider);
},
child: const Text('Retry'),
),
],
),
),
),
),
],
),
);
}
}
class LogRecordTile extends StatelessWidget {
final LogRecord logRecord;
final ThemeData theme;
const LogRecordTile({
required this.logRecord,
required this.theme,
super.key,
});
@override
Widget build(BuildContext context) {
return ListTile(
leading: CircleAvatar(
backgroundColor: getLogLevelColor(logRecord.level, theme),
child: Icon(
getLogLevelIcon(logRecord.level),
color: getLogLevelTextColor(logRecord.level, theme),
),
),
title: Text(logRecord.loggerName),
subtitle: Text(logRecord.message),
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
icon: Icon(getLogLevelIcon(logRecord.level)),
title: Text(logRecord.loggerName),
content: Text.rich(
TextSpan(
children: [
TextSpan(
text: logRecord.time.toIso8601String(),
style: const TextStyle(fontStyle: FontStyle.italic),
),
const TextSpan(text: '\n\n'),
TextSpan(text: logRecord.message),
],
),
),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('Close'),
),
],
);
},
);
},
);
}
}
IconData getLogLevelIcon(Level level) {
switch (level) {
case Level.INFO:
return (Icons.info);
case Level.WARNING:
return (Icons.warning);
case Level.SEVERE:
case Level.SHOUT:
return (Icons.error);
default:
return (Icons.bug_report);
}
}
Color? getLogLevelColor(Level level, ThemeData theme) {
switch (level) {
case Level.INFO:
return theme.colorScheme.surfaceContainerLow;
case Level.WARNING:
return theme.colorScheme.surfaceBright;
case Level.SEVERE:
case Level.SHOUT:
return theme.colorScheme.errorContainer;
default:
return theme.colorScheme.primaryContainer;
}
}
Color? getLogLevelTextColor(Level level, ThemeData theme) {
switch (level) {
case Level.INFO:
return theme.colorScheme.onSurface;
case Level.WARNING:
return theme.colorScheme.onSurface;
case Level.SEVERE:
case Level.SHOUT:
return theme.colorScheme.onErrorContainer;
default:
return theme.colorScheme.onPrimaryContainer;
}
}

View file

@ -5,7 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'flow.freezed.dart'; part 'flow.freezed.dart';
@freezed @freezed
class Flow with _$Flow { sealed class Flow with _$Flow {
const factory Flow({ const factory Flow({
required Uri serverUri, required Uri serverUri,
required String state, required String state,

View file

@ -1,5 +1,5 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
@ -9,241 +9,272 @@ part of 'flow.dart';
// FreezedGenerator // FreezedGenerator
// ************************************************************************** // **************************************************************************
// dart format off
T _$identity<T>(T value) => value; T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc /// @nodoc
mixin _$Flow { mixin _$Flow {
Uri get serverUri => throw _privateConstructorUsedError;
String get state => throw _privateConstructorUsedError;
String get verifier => throw _privateConstructorUsedError;
Cookie get cookie => throw _privateConstructorUsedError;
bool get isFlowComplete => throw _privateConstructorUsedError;
String? get authToken => throw _privateConstructorUsedError;
Uri get serverUri; String get state; String get verifier; Cookie get cookie; bool get isFlowComplete; String? get authToken;
/// Create a copy of Flow /// Create a copy of Flow
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
$FlowCopyWith<Flow> get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $FlowCopyWith<$Res> {
factory $FlowCopyWith(Flow value, $Res Function(Flow) then) =
_$FlowCopyWithImpl<$Res, Flow>;
@useResult
$Res call(
{Uri serverUri,
String state,
String verifier,
Cookie cookie,
bool isFlowComplete,
String? authToken});
}
/// @nodoc
class _$FlowCopyWithImpl<$Res, $Val extends Flow>
implements $FlowCopyWith<$Res> {
_$FlowCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Flow
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$FlowCopyWith<Flow> get copyWith => _$FlowCopyWithImpl<Flow>(this as Flow, _$identity);
@override @override
$Res call({ bool operator ==(Object other) {
Object? serverUri = null, return identical(this, other) || (other.runtimeType == runtimeType&&other is Flow&&(identical(other.serverUri, serverUri) || other.serverUri == serverUri)&&(identical(other.state, state) || other.state == state)&&(identical(other.verifier, verifier) || other.verifier == verifier)&&(identical(other.cookie, cookie) || other.cookie == cookie)&&(identical(other.isFlowComplete, isFlowComplete) || other.isFlowComplete == isFlowComplete)&&(identical(other.authToken, authToken) || other.authToken == authToken));
Object? state = null,
Object? verifier = null,
Object? cookie = null,
Object? isFlowComplete = null,
Object? authToken = freezed,
}) {
return _then(_value.copyWith(
serverUri: null == serverUri
? _value.serverUri
: serverUri // ignore: cast_nullable_to_non_nullable
as Uri,
state: null == state
? _value.state
: state // ignore: cast_nullable_to_non_nullable
as String,
verifier: null == verifier
? _value.verifier
: verifier // ignore: cast_nullable_to_non_nullable
as String,
cookie: null == cookie
? _value.cookie
: cookie // ignore: cast_nullable_to_non_nullable
as Cookie,
isFlowComplete: null == isFlowComplete
? _value.isFlowComplete
: isFlowComplete // ignore: cast_nullable_to_non_nullable
as bool,
authToken: freezed == authToken
? _value.authToken
: authToken // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
} }
/// @nodoc
abstract class _$$FlowImplCopyWith<$Res> implements $FlowCopyWith<$Res> {
factory _$$FlowImplCopyWith(
_$FlowImpl value, $Res Function(_$FlowImpl) then) =
__$$FlowImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Uri serverUri,
String state,
String verifier,
Cookie cookie,
bool isFlowComplete,
String? authToken});
}
/// @nodoc
class __$$FlowImplCopyWithImpl<$Res>
extends _$FlowCopyWithImpl<$Res, _$FlowImpl>
implements _$$FlowImplCopyWith<$Res> {
__$$FlowImplCopyWithImpl(_$FlowImpl _value, $Res Function(_$FlowImpl) _then)
: super(_value, _then);
/// Create a copy of Flow
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? serverUri = null,
Object? state = null,
Object? verifier = null,
Object? cookie = null,
Object? isFlowComplete = null,
Object? authToken = freezed,
}) {
return _then(_$FlowImpl(
serverUri: null == serverUri
? _value.serverUri
: serverUri // ignore: cast_nullable_to_non_nullable
as Uri,
state: null == state
? _value.state
: state // ignore: cast_nullable_to_non_nullable
as String,
verifier: null == verifier
? _value.verifier
: verifier // ignore: cast_nullable_to_non_nullable
as String,
cookie: null == cookie
? _value.cookie
: cookie // ignore: cast_nullable_to_non_nullable
as Cookie,
isFlowComplete: null == isFlowComplete
? _value.isFlowComplete
: isFlowComplete // ignore: cast_nullable_to_non_nullable
as bool,
authToken: freezed == authToken
? _value.authToken
: authToken // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
class _$FlowImpl implements _Flow {
const _$FlowImpl(
{required this.serverUri,
required this.state,
required this.verifier,
required this.cookie,
this.isFlowComplete = false,
this.authToken});
@override @override
final Uri serverUri; int get hashCode => Object.hash(runtimeType,serverUri,state,verifier,cookie,isFlowComplete,authToken);
@override
final String state;
@override
final String verifier;
@override
final Cookie cookie;
@override
@JsonKey()
final bool isFlowComplete;
@override
final String? authToken;
@override @override
String toString() { String toString() {
return 'Flow(serverUri: $serverUri, state: $state, verifier: $verifier, cookie: $cookie, isFlowComplete: $isFlowComplete, authToken: $authToken)'; return 'Flow(serverUri: $serverUri, state: $state, verifier: $verifier, cookie: $cookie, isFlowComplete: $isFlowComplete, authToken: $authToken)';
} }
}
/// @nodoc
abstract mixin class $FlowCopyWith<$Res> {
factory $FlowCopyWith(Flow value, $Res Function(Flow) _then) = _$FlowCopyWithImpl;
@useResult
$Res call({
Uri serverUri, String state, String verifier, Cookie cookie, bool isFlowComplete, String? authToken
});
}
/// @nodoc
class _$FlowCopyWithImpl<$Res>
implements $FlowCopyWith<$Res> {
_$FlowCopyWithImpl(this._self, this._then);
final Flow _self;
final $Res Function(Flow) _then;
/// Create a copy of Flow
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? serverUri = null,Object? state = null,Object? verifier = null,Object? cookie = null,Object? isFlowComplete = null,Object? authToken = freezed,}) {
return _then(_self.copyWith(
serverUri: null == serverUri ? _self.serverUri : serverUri // ignore: cast_nullable_to_non_nullable
as Uri,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
as String,verifier: null == verifier ? _self.verifier : verifier // ignore: cast_nullable_to_non_nullable
as String,cookie: null == cookie ? _self.cookie : cookie // ignore: cast_nullable_to_non_nullable
as Cookie,isFlowComplete: null == isFlowComplete ? _self.isFlowComplete : isFlowComplete // ignore: cast_nullable_to_non_nullable
as bool,authToken: freezed == authToken ? _self.authToken : authToken // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// Adds pattern-matching-related methods to [Flow].
extension FlowPatterns on Flow {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _Flow value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _Flow() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _Flow value) $default,){
final _that = this;
switch (_that) {
case _Flow():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _Flow value)? $default,){
final _that = this;
switch (_that) {
case _Flow() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( Uri serverUri, String state, String verifier, Cookie cookie, bool isFlowComplete, String? authToken)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _Flow() when $default != null:
return $default(_that.serverUri,_that.state,_that.verifier,_that.cookie,_that.isFlowComplete,_that.authToken);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( Uri serverUri, String state, String verifier, Cookie cookie, bool isFlowComplete, String? authToken) $default,) {final _that = this;
switch (_that) {
case _Flow():
return $default(_that.serverUri,_that.state,_that.verifier,_that.cookie,_that.isFlowComplete,_that.authToken);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( Uri serverUri, String state, String verifier, Cookie cookie, bool isFlowComplete, String? authToken)? $default,) {final _that = this;
switch (_that) {
case _Flow() when $default != null:
return $default(_that.serverUri,_that.state,_that.verifier,_that.cookie,_that.isFlowComplete,_that.authToken);case _:
return null;
}
}
}
/// @nodoc
class _Flow implements Flow {
const _Flow({required this.serverUri, required this.state, required this.verifier, required this.cookie, this.isFlowComplete = false, this.authToken});
@override final Uri serverUri;
@override final String state;
@override final String verifier;
@override final Cookie cookie;
@override@JsonKey() final bool isFlowComplete;
@override final String? authToken;
/// Create a copy of Flow
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$FlowCopyWith<_Flow> get copyWith => __$FlowCopyWithImpl<_Flow>(this, _$identity);
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) || (other.runtimeType == runtimeType&&other is _Flow&&(identical(other.serverUri, serverUri) || other.serverUri == serverUri)&&(identical(other.state, state) || other.state == state)&&(identical(other.verifier, verifier) || other.verifier == verifier)&&(identical(other.cookie, cookie) || other.cookie == cookie)&&(identical(other.isFlowComplete, isFlowComplete) || other.isFlowComplete == isFlowComplete)&&(identical(other.authToken, authToken) || other.authToken == authToken));
(other.runtimeType == runtimeType &&
other is _$FlowImpl &&
(identical(other.serverUri, serverUri) ||
other.serverUri == serverUri) &&
(identical(other.state, state) || other.state == state) &&
(identical(other.verifier, verifier) ||
other.verifier == verifier) &&
(identical(other.cookie, cookie) || other.cookie == cookie) &&
(identical(other.isFlowComplete, isFlowComplete) ||
other.isFlowComplete == isFlowComplete) &&
(identical(other.authToken, authToken) ||
other.authToken == authToken));
} }
@override @override
int get hashCode => Object.hash(runtimeType, serverUri, state, verifier, int get hashCode => Object.hash(runtimeType,serverUri,state,verifier,cookie,isFlowComplete,authToken);
cookie, isFlowComplete, authToken);
@override
String toString() {
return 'Flow(serverUri: $serverUri, state: $state, verifier: $verifier, cookie: $cookie, isFlowComplete: $isFlowComplete, authToken: $authToken)';
}
}
/// @nodoc
abstract mixin class _$FlowCopyWith<$Res> implements $FlowCopyWith<$Res> {
factory _$FlowCopyWith(_Flow value, $Res Function(_Flow) _then) = __$FlowCopyWithImpl;
@override @useResult
$Res call({
Uri serverUri, String state, String verifier, Cookie cookie, bool isFlowComplete, String? authToken
});
}
/// @nodoc
class __$FlowCopyWithImpl<$Res>
implements _$FlowCopyWith<$Res> {
__$FlowCopyWithImpl(this._self, this._then);
final _Flow _self;
final $Res Function(_Flow) _then;
/// Create a copy of Flow /// Create a copy of Flow
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') $Res call({Object? serverUri = null,Object? state = null,Object? verifier = null,Object? cookie = null,Object? isFlowComplete = null,Object? authToken = freezed,}) {
@override return _then(_Flow(
@pragma('vm:prefer-inline') serverUri: null == serverUri ? _self.serverUri : serverUri // ignore: cast_nullable_to_non_nullable
_$$FlowImplCopyWith<_$FlowImpl> get copyWith => as Uri,state: null == state ? _self.state : state // ignore: cast_nullable_to_non_nullable
__$$FlowImplCopyWithImpl<_$FlowImpl>(this, _$identity); as String,verifier: null == verifier ? _self.verifier : verifier // ignore: cast_nullable_to_non_nullable
as String,cookie: null == cookie ? _self.cookie : cookie // ignore: cast_nullable_to_non_nullable
as Cookie,isFlowComplete: null == isFlowComplete ? _self.isFlowComplete : isFlowComplete // ignore: cast_nullable_to_non_nullable
as bool,authToken: freezed == authToken ? _self.authToken : authToken // ignore: cast_nullable_to_non_nullable
as String?,
));
} }
abstract class _Flow implements Flow {
const factory _Flow(
{required final Uri serverUri,
required final String state,
required final String verifier,
required final Cookie cookie,
final bool isFlowComplete,
final String? authToken}) = _$FlowImpl;
@override
Uri get serverUri;
@override
String get state;
@override
String get verifier;
@override
Cookie get cookie;
@override
bool get isFlowComplete;
@override
String? get authToken;
/// Create a copy of Flow
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$FlowImplCopyWith<_$FlowImpl> get copyWith =>
throw _privateConstructorUsedError;
} }
// dart format on

View file

@ -1,5 +1,6 @@
import 'dart:io'; import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/models/error_response.dart'; import 'package:vaani/models/error_response.dart';
@ -52,8 +53,10 @@ class OauthFlows extends _$OauthFlows {
} }
state = { state = {
...state, ...state,
oauthState: state[oauthState]! oauthState: state[oauthState]!.copyWith(
.copyWith(isFlowComplete: true, authToken: authToken), isFlowComplete: true,
authToken: authToken,
),
}; };
} }
} }
@ -61,7 +64,7 @@ class OauthFlows extends _$OauthFlows {
/// the code returned by the server in exchange for the verifier /// the code returned by the server in exchange for the verifier
@riverpod @riverpod
Future<String?> loginInExchangeForCode( Future<String?> loginInExchangeForCode(
LoginInExchangeForCodeRef ref, { Ref ref, {
required State oauthState, required State oauthState,
required Code code, required Code code,
ErrorResponseHandler? responseHandler, ErrorResponseHandler? responseHandler,

View file

@ -6,219 +6,167 @@ part of 'oauth_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$loginInExchangeForCodeHash() => // GENERATED CODE - DO NOT MODIFY BY HAND
r'e931254959d9eb8196439c6b0c884c26cbe17c2f'; // ignore_for_file: type=lint, type=warning
/// Copied from Dart SDK @ProviderFor(OauthFlows)
class _SystemHash { final oauthFlowsProvider = OauthFlowsProvider._();
_SystemHash._();
static int combine(int hash, int value) { final class OauthFlowsProvider
// ignore: parameter_assignments extends $NotifierProvider<OauthFlows, Map<State, Flow>> {
hash = 0x1fffffff & (hash + value); OauthFlowsProvider._()
// ignore: parameter_assignments : super(
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); from: null,
return hash ^ (hash >> 6); argument: null,
} retry: null,
name: r'oauthFlowsProvider',
static int finish(int hash) { isAutoDispose: false,
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// the code returned by the server in exchange for the verifier
///
/// Copied from [loginInExchangeForCode].
@ProviderFor(loginInExchangeForCode)
const loginInExchangeForCodeProvider = LoginInExchangeForCodeFamily();
/// the code returned by the server in exchange for the verifier
///
/// Copied from [loginInExchangeForCode].
class LoginInExchangeForCodeFamily extends Family<AsyncValue<String?>> {
/// the code returned by the server in exchange for the verifier
///
/// Copied from [loginInExchangeForCode].
const LoginInExchangeForCodeFamily();
/// the code returned by the server in exchange for the verifier
///
/// Copied from [loginInExchangeForCode].
LoginInExchangeForCodeProvider call({
required String oauthState,
required String code,
ErrorResponseHandler? responseHandler,
}) {
return LoginInExchangeForCodeProvider(
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
);
}
@override
LoginInExchangeForCodeProvider getProviderOverride(
covariant LoginInExchangeForCodeProvider provider,
) {
return call(
oauthState: provider.oauthState,
code: provider.code,
responseHandler: provider.responseHandler,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'loginInExchangeForCodeProvider';
}
/// the code returned by the server in exchange for the verifier
///
/// Copied from [loginInExchangeForCode].
class LoginInExchangeForCodeProvider
extends AutoDisposeFutureProvider<String?> {
/// the code returned by the server in exchange for the verifier
///
/// Copied from [loginInExchangeForCode].
LoginInExchangeForCodeProvider({
required String oauthState,
required String code,
ErrorResponseHandler? responseHandler,
}) : this._internal(
(ref) => loginInExchangeForCode(
ref as LoginInExchangeForCodeRef,
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
),
from: loginInExchangeForCodeProvider,
name: r'loginInExchangeForCodeProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$loginInExchangeForCodeHash,
dependencies: LoginInExchangeForCodeFamily._dependencies,
allTransitiveDependencies:
LoginInExchangeForCodeFamily._allTransitiveDependencies,
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
);
LoginInExchangeForCodeProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.oauthState,
required this.code,
required this.responseHandler,
}) : super.internal();
final String oauthState;
final String code;
final ErrorResponseHandler? responseHandler;
@override
Override overrideWith(
FutureOr<String?> Function(LoginInExchangeForCodeRef provider) create,
) {
return ProviderOverride(
origin: this,
override: LoginInExchangeForCodeProvider._internal(
(ref) => create(ref as LoginInExchangeForCodeRef),
from: from,
name: null,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, $allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
),
); );
}
@override @override
AutoDisposeFutureProviderElement<String?> createElement() { String debugGetCreateSourceHash() => _$oauthFlowsHash();
return _LoginInExchangeForCodeProviderElement(this);
@$internal
@override
OauthFlows create() => OauthFlows();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Map<State, Flow> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Map<State, Flow>>(value),
);
}
}
String _$oauthFlowsHash() => r'4e278baa0bf26f2a10694ca2caadb68dd5b6156f';
abstract class _$OauthFlows extends $Notifier<Map<State, Flow>> {
Map<State, Flow> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<Map<State, Flow>, Map<State, Flow>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Map<State, Flow>, Map<State, Flow>>,
Map<State, Flow>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
/// the code returned by the server in exchange for the verifier
@ProviderFor(loginInExchangeForCode)
final loginInExchangeForCodeProvider = LoginInExchangeForCodeFamily._();
/// the code returned by the server in exchange for the verifier
final class LoginInExchangeForCodeProvider
extends $FunctionalProvider<AsyncValue<String?>, String?, FutureOr<String?>>
with $FutureModifier<String?>, $FutureProvider<String?> {
/// the code returned by the server in exchange for the verifier
LoginInExchangeForCodeProvider._({
required LoginInExchangeForCodeFamily super.from,
required ({
State oauthState,
Code code,
ErrorResponseHandler? responseHandler,
})
super.argument,
}) : super(
retry: null,
name: r'loginInExchangeForCodeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$loginInExchangeForCodeHash();
@override
String toString() {
return r'loginInExchangeForCodeProvider'
''
'$argument';
}
@$internal
@override
$FutureProviderElement<String?> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<String?> create(Ref ref) {
final argument =
this.argument
as ({
State oauthState,
Code code,
ErrorResponseHandler? responseHandler,
});
return loginInExchangeForCode(
ref,
oauthState: argument.oauthState,
code: argument.code,
responseHandler: argument.responseHandler,
);
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is LoginInExchangeForCodeProvider && return other is LoginInExchangeForCodeProvider &&
other.oauthState == oauthState && other.argument == argument;
other.code == code &&
other.responseHandler == responseHandler;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); return argument.hashCode;
hash = _SystemHash.combine(hash, oauthState.hashCode);
hash = _SystemHash.combine(hash, code.hashCode);
hash = _SystemHash.combine(hash, responseHandler.hashCode);
return _SystemHash.finish(hash);
} }
} }
mixin LoginInExchangeForCodeRef on AutoDisposeFutureProviderRef<String?> { String _$loginInExchangeForCodeHash() =>
/// The parameter `oauthState` of this provider. r'bfc3945529048a0f536052fd5579b76457560fcd';
String get oauthState;
/// The parameter `code` of this provider. /// the code returned by the server in exchange for the verifier
String get code;
/// The parameter `responseHandler` of this provider. final class LoginInExchangeForCodeFamily extends $Family
ErrorResponseHandler? get responseHandler; with
} $FunctionalFamilyOverride<
FutureOr<String?>,
class _LoginInExchangeForCodeProviderElement ({State oauthState, Code code, ErrorResponseHandler? responseHandler})
extends AutoDisposeFutureProviderElement<String?> > {
with LoginInExchangeForCodeRef { LoginInExchangeForCodeFamily._()
_LoginInExchangeForCodeProviderElement(super.provider); : super(
retry: null,
@override name: r'loginInExchangeForCodeProvider',
String get oauthState =>
(origin as LoginInExchangeForCodeProvider).oauthState;
@override
String get code => (origin as LoginInExchangeForCodeProvider).code;
@override
ErrorResponseHandler? get responseHandler =>
(origin as LoginInExchangeForCodeProvider).responseHandler;
}
String _$oauthFlowsHash() => r'4e278baa0bf26f2a10694ca2caadb68dd5b6156f';
/// See also [OauthFlows].
@ProviderFor(OauthFlows)
final oauthFlowsProvider =
NotifierProvider<OauthFlows, Map<State, Flow>>.internal(
OauthFlows.new,
name: r'oauthFlowsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$oauthFlowsHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, $allTransitiveDependencies: null,
isAutoDispose: true,
); );
typedef _$OauthFlows = Notifier<Map<State, Flow>>; /// the code returned by the server in exchange for the verifier
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member LoginInExchangeForCodeProvider call({
required State oauthState,
required Code code,
ErrorResponseHandler? responseHandler,
}) => LoginInExchangeForCodeProvider._(
argument: (
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
),
from: this,
);
@override
String toString() => r'loginInExchangeForCodeProvider';
}

View file

@ -27,9 +27,7 @@ class CallbackPage extends HookConsumerWidget {
// check if the state is in the flows // check if the state is in the flows
if (!flows.containsKey(state)) { if (!flows.containsKey(state)) {
return const _SomethingWentWrong( return const _SomethingWentWrong(message: 'State not found');
message: 'State not found',
);
} }
// get the token // get the token
@ -45,26 +43,21 @@ class CallbackPage extends HookConsumerWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text('Contacting server...\nPlease wait\n\nGot:' Text(
'\nState: $state\nCode: $code'), 'Contacting server...\nPlease wait\n\nGot:'
'\nState: $state\nCode: $code',
),
loginAuthToken.when( loginAuthToken.when(
data: (authenticationToken) { data: (authenticationToken) {
if (authenticationToken == null) { if (authenticationToken == null) {
handleServerError( handleServerError(context, serverErrorResponse);
context,
serverErrorResponse,
);
return const BackToLoginButton(); return const BackToLoginButton();
} }
return Text('Token: $authenticationToken'); return Text('Token: $authenticationToken');
}, },
loading: () => const CircularProgressIndicator(), loading: () => const CircularProgressIndicator(),
error: (error, _) { error: (error, _) {
handleServerError( handleServerError(context, serverErrorResponse, e: error);
context,
serverErrorResponse,
e: error,
);
return Column( return Column(
children: [ children: [
Text('Error with OAuth flow: $error'), Text('Error with OAuth flow: $error'),
@ -81,9 +74,7 @@ class CallbackPage extends HookConsumerWidget {
} }
class BackToLoginButton extends StatelessWidget { class BackToLoginButton extends StatelessWidget {
const BackToLoginButton({ const BackToLoginButton({super.key});
super.key,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -97,10 +88,7 @@ class BackToLoginButton extends StatelessWidget {
} }
class _SomethingWentWrong extends StatelessWidget { class _SomethingWentWrong extends StatelessWidget {
const _SomethingWentWrong({ const _SomethingWentWrong({this.message = 'Error with OAuth flow'});
super.key,
this.message = 'Error with OAuth flow',
});
final String message; final String message;
@ -110,10 +98,7 @@ class _SomethingWentWrong extends StatelessWidget {
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [Text(message), const BackToLoginButton()],
Text(message),
const BackToLoginButton(),
],
), ),
), ),
); );

View file

@ -9,24 +9,36 @@ import 'package:vaani/shared/utils.dart';
import 'package:vaani/shared/widgets/add_new_server.dart'; import 'package:vaani/shared/widgets/add_new_server.dart';
class OnboardingSinglePage extends HookConsumerWidget { class OnboardingSinglePage extends HookConsumerWidget {
const OnboardingSinglePage({ const OnboardingSinglePage({super.key});
super.key,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final apiSettings = ref.watch(apiSettingsProvider); return Scaffold(
final serverUriController = useTextEditingController( body: LayoutBuilder(
text: apiSettings.activeServer?.serverUrl.toString() ?? '', 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()),
),
),
),
); );
var audiobookshelfUri = makeBaseUrl(serverUriController.text); },
),
);
}
}
final canUserLogin = useState(apiSettings.activeServer != null); Widget fadeSlideTransitionBuilder(Widget child, Animation<double> animation) {
fadeSlideTransitionBuilder(
Widget child,
Animation<double> animation,
) {
return FadeTransition( return FadeTransition(
opacity: animation, opacity: animation,
child: SlideTransition( child: SlideTransition(
@ -39,9 +51,22 @@ class OnboardingSinglePage extends HookConsumerWidget {
); );
} }
return Scaffold( class OnboardingBody extends HookConsumerWidget {
body: Column( const OnboardingBody({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final apiSettings = ref.watch(apiSettingsProvider);
final serverUriController = useTextEditingController(
text: apiSettings.activeServer?.serverUrl.toString() ?? 'https://',
);
var audiobookshelfUri = makeBaseUrl(serverUriController.text);
final canUserLogin = useState(apiSettings.activeServer != null);
return Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
@ -50,9 +75,7 @@ class OnboardingSinglePage extends HookConsumerWidget {
style: Theme.of(context).textTheme.headlineSmall, style: Theme.of(context).textTheme.headlineSmall,
), ),
), ),
const SizedBox.square( const SizedBox.square(dimension: 16.0),
dimension: 16.0,
),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: AnimatedSwitcher( child: AnimatedSwitcher(
@ -81,13 +104,12 @@ class OnboardingSinglePage extends HookConsumerWidget {
}, },
), ),
), ),
const SizedBox.square(dimension: 16.0),
AnimatedSwitcher( AnimatedSwitcher(
duration: 500.ms, duration: 500.ms,
transitionBuilder: fadeSlideTransitionBuilder, transitionBuilder: fadeSlideTransitionBuilder,
child: canUserLogin.value child: canUserLogin.value
? UserLoginWidget( ? UserLoginWidget(server: audiobookshelfUri)
server: audiobookshelfUri,
)
// ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0) // ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0)
: const RedirectToABS().animate().fadeIn().slideY( : const RedirectToABS().animate().fadeIn().slideY(
curve: Curves.easeInOut, curve: Curves.easeInOut,
@ -95,15 +117,12 @@ class OnboardingSinglePage extends HookConsumerWidget {
), ),
), ),
], ],
),
); );
} }
} }
class RedirectToABS extends StatelessWidget { class RedirectToABS extends StatelessWidget {
const RedirectToABS({ const RedirectToABS({super.key});
super.key,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -119,18 +138,14 @@ class RedirectToABS extends StatelessWidget {
isSemanticButton: false, isSemanticButton: false,
style: ButtonStyle( style: ButtonStyle(
elevation: WidgetStateProperty.all(0), elevation: WidgetStateProperty.all(0),
padding: WidgetStateProperty.all( padding: WidgetStateProperty.all(const EdgeInsets.all(0)),
const EdgeInsets.all(0),
),
), ),
onPressed: () async { onPressed: () async {
// open the github page // open the github page
// ignore: avoid_print // ignore: avoid_print
print('Opening the github page'); print('Opening the github page');
await handleLaunchUrl( await handleLaunchUrl(
Uri.parse( Uri.parse('https://www.audiobookshelf.org'),
'https://www.audiobookshelf.org',
),
); );
}, },
child: const Text('Click here'), child: const Text('Click here'),

View file

@ -1,32 +1,38 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' show AuthMethod;
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart' show serverStatusProvider;
import 'package:vaani/api/server_provider.dart'; import 'package:vaani/api/server_provider.dart'
import 'package:vaani/features/onboarding/view/user_login_with_open_id.dart'; show ServerAlreadyExistsException, audiobookShelfServerProvider;
import 'package:vaani/features/onboarding/view/user_login_with_password.dart'; import 'package:vaani/features/onboarding/view/onboarding_single_page.dart'
import 'package:vaani/features/onboarding/view/user_login_with_token.dart'; show fadeSlideTransitionBuilder;
import 'package:vaani/hacks/fix_autofill_losing_focus.dart'; import 'package:vaani/features/onboarding/view/user_login_with_open_id.dart'
import 'package:vaani/models/error_response.dart'; show UserLoginWithOpenID;
import 'package:vaani/settings/api_settings_provider.dart'; 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; import 'package:vaani/settings/models/models.dart' as model;
class UserLoginWidget extends HookConsumerWidget { class UserLoginWidget extends HookConsumerWidget {
UserLoginWidget({ const UserLoginWidget({super.key, required this.server, this.onSuccess});
super.key,
required this.server,
});
final Uri server; final Uri server;
final serverStatusError = ErrorResponseHandler(); final Function(model.AuthenticatedUser)? onSuccess;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final serverStatus = final serverStatusError = useMemoized(() => ErrorResponseHandler(), []);
ref.watch(serverStatusProvider(server, serverStatusError.storeError)); final serverStatus = ref.watch(
serverStatusProvider(server, serverStatusError.storeError),
final api = ref.watch(audiobookshelfApiProvider(server)); );
return serverStatus.when( return serverStatus.when(
data: (value) { data: (value) {
@ -42,12 +48,11 @@ class UserLoginWidget extends HookConsumerWidget {
openIDAvailable: openIDAvailable:
value.authMethods?.contains(AuthMethod.openid) ?? false, value.authMethods?.contains(AuthMethod.openid) ?? false,
openIDButtonText: value.authFormData?.authOpenIDButtonText, openIDButtonText: value.authFormData?.authOpenIDButtonText,
onSuccess: onSuccess,
); );
}, },
loading: () { loading: () {
return const Center( return const Center(child: CircularProgressIndicator());
child: CircularProgressIndicator(),
);
}, },
error: (error, _) { error: (error, _) {
return Center( return Center(
@ -58,10 +63,7 @@ class UserLoginWidget extends HookConsumerWidget {
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
ref.invalidate( ref.invalidate(
serverStatusProvider( serverStatusProvider(server, serverStatusError.storeError),
server,
serverStatusError.storeError,
),
); );
}, },
child: const Text('Try again'), child: const Text('Try again'),
@ -74,11 +76,7 @@ class UserLoginWidget extends HookConsumerWidget {
} }
} }
enum AuthMethodChoice { enum AuthMethodChoice { local, openid, authToken }
local,
openid,
authToken,
}
class UserLoginMultipleAuth extends HookConsumerWidget { class UserLoginMultipleAuth extends HookConsumerWidget {
const UserLoginMultipleAuth({ const UserLoginMultipleAuth({
@ -88,6 +86,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
this.openIDAvailable = false, this.openIDAvailable = false,
this.onPressed, this.onPressed,
this.openIDButtonText, this.openIDButtonText,
this.onSuccess,
}); });
final Uri server; final Uri server;
@ -95,6 +94,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
final bool openIDAvailable; final bool openIDAvailable;
final void Function()? onPressed; final void Function()? onPressed;
final String? openIDButtonText; final String? openIDButtonText;
final Function(model.AuthenticatedUser)? onSuccess;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -104,24 +104,18 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
localAvailable ? AuthMethodChoice.local : AuthMethodChoice.authToken, localAvailable ? AuthMethodChoice.local : AuthMethodChoice.authToken,
); );
final apiSettings = ref.watch(apiSettingsProvider);
model.AudiobookShelfServer addServer() { model.AudiobookShelfServer addServer() {
var newServer = model.AudiobookShelfServer( var newServer = model.AudiobookShelfServer(serverUrl: server);
serverUrl: server,
);
try { try {
// add the server to the list of servers // add the server to the list of servers
ref.read(audiobookShelfServerProvider.notifier).addServer( ref.read(audiobookShelfServerProvider.notifier).addServer(newServer);
newServer,
);
} on ServerAlreadyExistsException catch (e) { } on ServerAlreadyExistsException catch (e) {
newServer = e.server; newServer = e.server;
} finally { } finally {
ref.read(apiSettingsProvider.notifier).updateState( ref
apiSettings.copyWith( .read(apiSettingsProvider.notifier)
activeServer: newServer, .updateState(
), ref.read(apiSettingsProvider).copyWith(activeServer: newServer),
); );
} }
return newServer; return newServer;
@ -130,22 +124,25 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
return Center( return Center(
child: InactiveFocusScopeObserver( child: InactiveFocusScopeObserver(
child: AutofillGroup( child: AutofillGroup(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
children: [ children: [
Wrap( Padding(
padding: const EdgeInsets.all(8.0),
child: Wrap(
// mainAxisAlignment: MainAxisAlignment.center, // mainAxisAlignment: MainAxisAlignment.center,
spacing: 10, spacing: 10,
runAlignment: WrapAlignment.center, runAlignment: WrapAlignment.center,
runSpacing: 10, runSpacing: 10,
alignment: WrapAlignment.center, alignment: WrapAlignment.center,
children: [ children:
[
// a small label to show the user what to do // a small label to show the user what to do
if (localAvailable) if (localAvailable)
ChoiceChip( ChoiceChip(
label: const Text('Local'), label: const Text('Local'),
selected: methodChoice.value == AuthMethodChoice.local, selected:
methodChoice.value ==
AuthMethodChoice.local,
onSelected: (selected) { onSelected: (selected) {
if (selected) { if (selected) {
methodChoice.value = AuthMethodChoice.local; methodChoice.value = AuthMethodChoice.local;
@ -155,46 +152,60 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
if (openIDAvailable) if (openIDAvailable)
ChoiceChip( ChoiceChip(
label: const Text('OpenID'), label: const Text('OpenID'),
selected: methodChoice.value == AuthMethodChoice.openid, selected:
methodChoice.value ==
AuthMethodChoice.openid,
onSelected: (selected) { onSelected: (selected) {
if (selected) { if (selected) {
methodChoice.value = AuthMethodChoice.openid; methodChoice.value =
AuthMethodChoice.openid;
} }
}, },
), ),
ChoiceChip( ChoiceChip(
label: const Text('Token'), label: const Text('Token'),
selected: selected:
methodChoice.value == AuthMethodChoice.authToken, methodChoice.value ==
AuthMethodChoice.authToken,
onSelected: (selected) { onSelected: (selected) {
if (selected) { if (selected) {
methodChoice.value = AuthMethodChoice.authToken; methodChoice.value =
AuthMethodChoice.authToken;
} }
}, },
), ),
], ]
.animate(interval: 100.ms)
.fadeIn(duration: 150.ms, curve: Curves.easeIn),
), ),
const SizedBox.square(
dimension: 8,
), ),
switch (methodChoice.value) { Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedSwitcher(
duration: 200.ms,
transitionBuilder: fadeSlideTransitionBuilder,
child: switch (methodChoice.value) {
AuthMethodChoice.authToken => UserLoginWithToken( AuthMethodChoice.authToken => UserLoginWithToken(
server: server, server: server,
addServer: addServer, addServer: addServer,
onSuccess: onSuccess,
), ),
AuthMethodChoice.local => UserLoginWithPassword( AuthMethodChoice.local => UserLoginWithPassword(
server: server, server: server,
addServer: addServer, addServer: addServer,
onSuccess: onSuccess,
), ),
AuthMethodChoice.openid => UserLoginWithOpenID( AuthMethodChoice.openid => UserLoginWithOpenID(
server: server, server: server,
addServer: addServer, addServer: addServer,
openIDButtonText: openIDButtonText, openIDButtonText: openIDButtonText,
onSuccess: onSuccess,
), ),
}, },
],
), ),
), ),
],
),
), ),
), ),
); );

View file

@ -11,6 +11,7 @@ import 'package:vaani/models/error_response.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/constants.dart'; import 'package:vaani/settings/constants.dart';
import 'package:vaani/settings/models/models.dart' as model; import 'package:vaani/settings/models/models.dart' as model;
import 'package:vaani/shared/extensions/obfuscation.dart';
import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/utils.dart';
class UserLoginWithOpenID extends HookConsumerWidget { class UserLoginWithOpenID extends HookConsumerWidget {
@ -19,12 +20,14 @@ class UserLoginWithOpenID extends HookConsumerWidget {
required this.server, required this.server,
required this.addServer, required this.addServer,
this.openIDButtonText, this.openIDButtonText,
this.onSuccess,
}); });
final Uri server; final Uri server;
final model.AudiobookShelfServer Function() addServer; final model.AudiobookShelfServer Function() addServer;
final String? openIDButtonText; final String? openIDButtonText;
final responseErrorHandler = ErrorResponseHandler(name: 'OpenID'); final responseErrorHandler = ErrorResponseHandler(name: 'OpenID');
final Function(model.AuthenticatedUser)? onSuccess;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -51,9 +54,9 @@ class UserLoginWithOpenID extends HookConsumerWidget {
if (openIDLoginEndpoint == null) { if (openIDLoginEndpoint == null) {
if (responseErrorHandler.response.statusCode == 400 && if (responseErrorHandler.response.statusCode == 400 &&
responseErrorHandler.response.body responseErrorHandler.response.body.toLowerCase().contains(
.toLowerCase() RegExp(r'invalid.*redirect.*uri'),
.contains(RegExp(r'invalid.*redirect.*uri'))) { )) {
// show error // show error
handleServerError( handleServerError(
context, context,
@ -89,19 +92,21 @@ class UserLoginWithOpenID extends HookConsumerWidget {
return; return;
} }
appLogger.fine('Got OpenID login endpoint: $openIDLoginEndpoint'); appLogger.fine(
'Got OpenID login endpoint: ${openIDLoginEndpoint.obfuscate()}',
);
// add the flow to the provider // add the flow to the provider
ref.read(oauthFlowsProvider.notifier).addFlow( ref
.read(oauthFlowsProvider.notifier)
.addFlow(
oauthState, oauthState,
verifier: verifier, verifier: verifier,
serverUri: server, serverUri: server,
cookie: Cookie.fromSetCookieValue(authCookie!), cookie: Cookie.fromSetCookieValue(authCookie!),
); );
await handleLaunchUrl( await handleLaunchUrl(openIDLoginEndpoint);
openIDLoginEndpoint,
);
} }
return Column( return Column(

View file

@ -5,10 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lottie/lottie.dart'; import 'package:lottie/lottie.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.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/hacks/fix_autofill_losing_focus.dart';
import 'package:vaani/models/error_response.dart'; import 'package:vaani/models/error_response.dart';
import 'package:vaani/router/router.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/settings/models/models.dart' as model;
import 'package:vaani/shared/utils.dart'; import 'package:vaani/shared/utils.dart';
@ -17,17 +18,20 @@ class UserLoginWithPassword extends HookConsumerWidget {
super.key, super.key,
required this.server, required this.server,
required this.addServer, required this.addServer,
this.onSuccess,
}); });
final Uri server; final Uri server;
final model.AudiobookShelfServer Function() addServer; final model.AudiobookShelfServer Function() addServer;
final serverErrorResponse = ErrorResponseHandler(); final serverErrorResponse = ErrorResponseHandler();
final Function(model.AuthenticatedUser)? onSuccess;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final usernameController = useTextEditingController(); final usernameController = useTextEditingController();
final passwordController = useTextEditingController(); final passwordController = useTextEditingController();
final isPasswordVisibleAnimationController = useAnimationController( final isPasswordVisibleAnimationController = useAnimationController(
initialValue: 1,
duration: const Duration(milliseconds: 500), duration: const Duration(milliseconds: 500),
); );
@ -35,17 +39,14 @@ class UserLoginWithPassword extends HookConsumerWidget {
final api = ref.watch(audiobookshelfApiProvider(server)); final api = ref.watch(audiobookshelfApiProvider(server));
// forward animation when the password visibility changes // forward animation when the password visibility changes
useEffect( useEffect(() {
() {
if (isPasswordVisible.value) { if (isPasswordVisible.value) {
isPasswordVisibleAnimationController.forward(); isPasswordVisibleAnimationController.forward();
} else { } else {
isPasswordVisibleAnimationController.reverse(); isPasswordVisibleAnimationController.reverse();
} }
return null; return null;
}, }, [isPasswordVisible.value]);
[isPasswordVisible.value],
);
/// Login to the server and save the user /// Login to the server and save the user
Future<void> loginAndSave() async { Future<void> loginAndSave() async {
@ -76,24 +77,24 @@ class UserLoginWithPassword extends HookConsumerWidget {
final authenticatedUser = model.AuthenticatedUser( final authenticatedUser = model.AuthenticatedUser(
server: addServer(), server: addServer(),
id: success.user.id, id: success.user.id,
password: password,
username: username, username: username,
authToken: api.token!, authToken: api.token!,
); );
if (onSuccess != null) {
onSuccess!(authenticatedUser);
} else {
// add the user to the list of users // add the user to the list of users
ref ref
.read(authenticatedUserProvider.notifier) .read(authenticatedUsersProvider.notifier)
.addUser(authenticatedUser, setActive: true); .addUser(authenticatedUser, setActive: true);
context.goNamed(Routes.home.name);
// redirect to the library page }
GoRouter.of(context).goNamed(Routes.home.name);
} }
return Center( return Center(
child: InactiveFocusScopeObserver( child: InactiveFocusScopeObserver(
child: AutofillGroup( child: AutofillGroup(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@ -105,10 +106,9 @@ class UserLoginWithPassword extends HookConsumerWidget {
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Username', labelText: 'Username',
labelStyle: TextStyle( labelStyle: TextStyle(
color: Theme.of(context) color: Theme.of(
.colorScheme context,
.onSurface ).colorScheme.onSurface.withValues(alpha: 0.8),
.withOpacity(0.8),
), ),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
@ -125,15 +125,16 @@ class UserLoginWithPassword extends HookConsumerWidget {
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Password', labelText: 'Password',
labelStyle: TextStyle( labelStyle: TextStyle(
color: Theme.of(context) color: Theme.of(
.colorScheme context,
.onSurface ).colorScheme.onSurface.withValues(alpha: 0.8),
.withOpacity(0.8),
), ),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
suffixIcon: ColorFiltered( suffixIcon: ColorFiltered(
colorFilter: ColorFilter.mode( colorFilter: ColorFilter.mode(
Theme.of(context).colorScheme.primary.withOpacity(0.8), Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.8),
BlendMode.srcIn, BlendMode.srcIn,
), ),
child: InkWell( child: InkWell(
@ -150,9 +151,7 @@ class UserLoginWithPassword extends HookConsumerWidget {
), ),
), ),
), ),
suffixIconConstraints: const BoxConstraints( suffixIconConstraints: const BoxConstraints(maxHeight: 45),
maxHeight: 45,
),
), ),
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
@ -164,7 +163,6 @@ class UserLoginWithPassword extends HookConsumerWidget {
), ),
), ),
), ),
),
); );
} }
} }
@ -191,10 +189,12 @@ Future<void> handleServerError(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Error'), title: const Text('Error'),
content: SelectableText('$title\n' content: SelectableText(
'$title\n'
'Got response: ${responseErrorHandler.response.body} (${responseErrorHandler.response.statusCode})\n' 'Got response: ${responseErrorHandler.response.body} (${responseErrorHandler.response.statusCode})\n'
'Stacktrace: $e\n\n' 'Stacktrace: $e\n\n'
'$body\n\n'), '$body\n\n',
),
actions: [ actions: [
if (outLink != null) if (outLink != null)
TextButton( TextButton(
@ -207,8 +207,10 @@ Future<void> handleServerError(
onPressed: () { onPressed: () {
// open an issue on the github page // open an issue on the github page
handleLaunchUrl( handleLaunchUrl(
Uri.parse( AppMetadata.githubRepo
'https://github.com/Dr-Blank/Vaani/issues', // append the issue url
.replace(
path: '${AppMetadata.githubRepo.path}/issues/new',
), ),
); );
}, },

View file

@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.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/models/error_response.dart';
import 'package:vaani/router/router.dart'; import 'package:vaani/router/router.dart';
import 'package:vaani/settings/models/models.dart' as model; import 'package:vaani/settings/models/models.dart' as model;
@ -14,11 +14,13 @@ class UserLoginWithToken extends HookConsumerWidget {
super.key, super.key,
required this.server, required this.server,
required this.addServer, required this.addServer,
this.onSuccess,
}); });
final Uri server; final Uri server;
final model.AudiobookShelfServer Function() addServer; final model.AudiobookShelfServer Function() addServer;
final serverErrorResponse = ErrorResponseHandler(); final serverErrorResponse = ErrorResponseHandler();
final Function(model.AuthenticatedUser)? onSuccess;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -65,12 +67,15 @@ class UserLoginWithToken extends HookConsumerWidget {
authToken: api.token!, authToken: api.token!,
); );
if (onSuccess != null) {
onSuccess!(authenticatedUser);
} else {
ref ref
.read(authenticatedUserProvider.notifier) .read(authenticatedUsersProvider.notifier)
.addUser(authenticatedUser, setActive: true); .addUser(authenticatedUser, setActive: true);
context.goNamed(Routes.home.name); context.goNamed(Routes.home.name);
} }
}
return Form( return Form(
child: Column( child: Column(
@ -84,7 +89,9 @@ class UserLoginWithToken extends HookConsumerWidget {
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'API Token', labelText: 'API Token',
labelStyle: TextStyle( labelStyle: TextStyle(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8), color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.8),
), ),
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
@ -99,10 +106,7 @@ class UserLoginWithToken extends HookConsumerWidget {
}, },
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
ElevatedButton( ElevatedButton(onPressed: loginAndSave, child: const Text('Login')),
onPressed: loginAndSave,
child: const Text('Login'),
),
], ],
), ),
); );

View file

@ -6,7 +6,7 @@ part 'book_settings.g.dart';
/// per book settings /// per book settings
@freezed @freezed
class BookSettings with _$BookSettings { sealed class BookSettings with _$BookSettings {
const factory BookSettings({ const factory BookSettings({
required String bookId, required String bookId,
@Default(NullablePlayerSettings()) NullablePlayerSettings playerSettings, @Default(NullablePlayerSettings()) NullablePlayerSettings playerSettings,

View file

@ -1,5 +1,5 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
@ -9,70 +9,272 @@ part of 'book_settings.dart';
// FreezedGenerator // FreezedGenerator
// ************************************************************************** // **************************************************************************
// dart format off
T _$identity<T>(T value) => value; T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
BookSettings _$BookSettingsFromJson(Map<String, dynamic> json) {
return _BookSettings.fromJson(json);
}
/// @nodoc /// @nodoc
mixin _$BookSettings { mixin _$BookSettings {
String get bookId => throw _privateConstructorUsedError;
NullablePlayerSettings get playerSettings =>
throw _privateConstructorUsedError;
/// Serializes this BookSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
String get bookId; NullablePlayerSettings get playerSettings;
/// Create a copy of BookSettings /// Create a copy of BookSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
$BookSettingsCopyWith<BookSettings> get copyWith => @pragma('vm:prefer-inline')
throw _privateConstructorUsedError; $BookSettingsCopyWith<BookSettings> get copyWith => _$BookSettingsCopyWithImpl<BookSettings>(this as BookSettings, _$identity);
/// Serializes this BookSettings to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is BookSettings&&(identical(other.bookId, bookId) || other.bookId == bookId)&&(identical(other.playerSettings, playerSettings) || other.playerSettings == playerSettings));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,bookId,playerSettings);
@override
String toString() {
return 'BookSettings(bookId: $bookId, playerSettings: $playerSettings)';
}
} }
/// @nodoc /// @nodoc
abstract class $BookSettingsCopyWith<$Res> { abstract mixin class $BookSettingsCopyWith<$Res> {
factory $BookSettingsCopyWith( factory $BookSettingsCopyWith(BookSettings value, $Res Function(BookSettings) _then) = _$BookSettingsCopyWithImpl;
BookSettings value, $Res Function(BookSettings) then) =
_$BookSettingsCopyWithImpl<$Res, BookSettings>;
@useResult @useResult
$Res call({String bookId, NullablePlayerSettings playerSettings}); $Res call({
String bookId, NullablePlayerSettings playerSettings
});
$NullablePlayerSettingsCopyWith<$Res> get playerSettings; $NullablePlayerSettingsCopyWith<$Res> get playerSettings;
} }
/// @nodoc /// @nodoc
class _$BookSettingsCopyWithImpl<$Res, $Val extends BookSettings> class _$BookSettingsCopyWithImpl<$Res>
implements $BookSettingsCopyWith<$Res> { implements $BookSettingsCopyWith<$Res> {
_$BookSettingsCopyWithImpl(this._value, this._then); _$BookSettingsCopyWithImpl(this._self, this._then);
// ignore: unused_field final BookSettings _self;
final $Val _value; final $Res Function(BookSettings) _then;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of BookSettings /// Create a copy of BookSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline') @override $Res call({Object? bookId = null,Object? playerSettings = null,}) {
@override return _then(_self.copyWith(
$Res call({ bookId: null == bookId ? _self.bookId : bookId // ignore: cast_nullable_to_non_nullable
Object? bookId = null, as String,playerSettings: null == playerSettings ? _self.playerSettings : playerSettings // ignore: cast_nullable_to_non_nullable
Object? playerSettings = null,
}) {
return _then(_value.copyWith(
bookId: null == bookId
? _value.bookId
: bookId // ignore: cast_nullable_to_non_nullable
as String,
playerSettings: null == playerSettings
? _value.playerSettings
: playerSettings // ignore: cast_nullable_to_non_nullable
as NullablePlayerSettings, as NullablePlayerSettings,
) as $Val); ));
}
/// Create a copy of BookSettings
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$NullablePlayerSettingsCopyWith<$Res> get playerSettings {
return $NullablePlayerSettingsCopyWith<$Res>(_self.playerSettings, (value) {
return _then(_self.copyWith(playerSettings: value));
});
}
}
/// Adds pattern-matching-related methods to [BookSettings].
extension BookSettingsPatterns on BookSettings {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _BookSettings value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _BookSettings() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _BookSettings value) $default,){
final _that = this;
switch (_that) {
case _BookSettings():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _BookSettings value)? $default,){
final _that = this;
switch (_that) {
case _BookSettings() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String bookId, NullablePlayerSettings playerSettings)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _BookSettings() when $default != null:
return $default(_that.bookId,_that.playerSettings);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String bookId, NullablePlayerSettings playerSettings) $default,) {final _that = this;
switch (_that) {
case _BookSettings():
return $default(_that.bookId,_that.playerSettings);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String bookId, NullablePlayerSettings playerSettings)? $default,) {final _that = this;
switch (_that) {
case _BookSettings() when $default != null:
return $default(_that.bookId,_that.playerSettings);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _BookSettings implements BookSettings {
const _BookSettings({required this.bookId, this.playerSettings = const NullablePlayerSettings()});
factory _BookSettings.fromJson(Map<String, dynamic> json) => _$BookSettingsFromJson(json);
@override final String bookId;
@override@JsonKey() final NullablePlayerSettings playerSettings;
/// Create a copy of BookSettings
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$BookSettingsCopyWith<_BookSettings> get copyWith => __$BookSettingsCopyWithImpl<_BookSettings>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$BookSettingsToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _BookSettings&&(identical(other.bookId, bookId) || other.bookId == bookId)&&(identical(other.playerSettings, playerSettings) || other.playerSettings == playerSettings));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,bookId,playerSettings);
@override
String toString() {
return 'BookSettings(bookId: $bookId, playerSettings: $playerSettings)';
}
}
/// @nodoc
abstract mixin class _$BookSettingsCopyWith<$Res> implements $BookSettingsCopyWith<$Res> {
factory _$BookSettingsCopyWith(_BookSettings value, $Res Function(_BookSettings) _then) = __$BookSettingsCopyWithImpl;
@override @useResult
$Res call({
String bookId, NullablePlayerSettings playerSettings
});
@override $NullablePlayerSettingsCopyWith<$Res> get playerSettings;
}
/// @nodoc
class __$BookSettingsCopyWithImpl<$Res>
implements _$BookSettingsCopyWith<$Res> {
__$BookSettingsCopyWithImpl(this._self, this._then);
final _BookSettings _self;
final $Res Function(_BookSettings) _then;
/// Create a copy of BookSettings
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? bookId = null,Object? playerSettings = null,}) {
return _then(_BookSettings(
bookId: null == bookId ? _self.bookId : bookId // ignore: cast_nullable_to_non_nullable
as String,playerSettings: null == playerSettings ? _self.playerSettings : playerSettings // ignore: cast_nullable_to_non_nullable
as NullablePlayerSettings,
));
} }
/// Create a copy of BookSettings /// Create a copy of BookSettings
@ -80,124 +282,11 @@ class _$BookSettingsCopyWithImpl<$Res, $Val extends BookSettings>
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$NullablePlayerSettingsCopyWith<$Res> get playerSettings { $NullablePlayerSettingsCopyWith<$Res> get playerSettings {
return $NullablePlayerSettingsCopyWith<$Res>(_value.playerSettings,
(value) { return $NullablePlayerSettingsCopyWith<$Res>(_self.playerSettings, (value) {
return _then(_value.copyWith(playerSettings: value) as $Val); return _then(_self.copyWith(playerSettings: value));
}); });
} }
} }
/// @nodoc // dart format on
abstract class _$$BookSettingsImplCopyWith<$Res>
implements $BookSettingsCopyWith<$Res> {
factory _$$BookSettingsImplCopyWith(
_$BookSettingsImpl value, $Res Function(_$BookSettingsImpl) then) =
__$$BookSettingsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({String bookId, NullablePlayerSettings playerSettings});
@override
$NullablePlayerSettingsCopyWith<$Res> get playerSettings;
}
/// @nodoc
class __$$BookSettingsImplCopyWithImpl<$Res>
extends _$BookSettingsCopyWithImpl<$Res, _$BookSettingsImpl>
implements _$$BookSettingsImplCopyWith<$Res> {
__$$BookSettingsImplCopyWithImpl(
_$BookSettingsImpl _value, $Res Function(_$BookSettingsImpl) _then)
: super(_value, _then);
/// Create a copy of BookSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? bookId = null,
Object? playerSettings = null,
}) {
return _then(_$BookSettingsImpl(
bookId: null == bookId
? _value.bookId
: bookId // ignore: cast_nullable_to_non_nullable
as String,
playerSettings: null == playerSettings
? _value.playerSettings
: playerSettings // ignore: cast_nullable_to_non_nullable
as NullablePlayerSettings,
));
}
}
/// @nodoc
@JsonSerializable()
class _$BookSettingsImpl implements _BookSettings {
const _$BookSettingsImpl(
{required this.bookId,
this.playerSettings = const NullablePlayerSettings()});
factory _$BookSettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$BookSettingsImplFromJson(json);
@override
final String bookId;
@override
@JsonKey()
final NullablePlayerSettings playerSettings;
@override
String toString() {
return 'BookSettings(bookId: $bookId, playerSettings: $playerSettings)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$BookSettingsImpl &&
(identical(other.bookId, bookId) || other.bookId == bookId) &&
(identical(other.playerSettings, playerSettings) ||
other.playerSettings == playerSettings));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType, bookId, playerSettings);
/// Create a copy of BookSettings
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$BookSettingsImplCopyWith<_$BookSettingsImpl> get copyWith =>
__$$BookSettingsImplCopyWithImpl<_$BookSettingsImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$BookSettingsImplToJson(
this,
);
}
}
abstract class _BookSettings implements BookSettings {
const factory _BookSettings(
{required final String bookId,
final NullablePlayerSettings playerSettings}) = _$BookSettingsImpl;
factory _BookSettings.fromJson(Map<String, dynamic> json) =
_$BookSettingsImpl.fromJson;
@override
String get bookId;
@override
NullablePlayerSettings get playerSettings;
/// Create a copy of BookSettings
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$BookSettingsImplCopyWith<_$BookSettingsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -6,16 +6,17 @@ part of 'book_settings.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
_$BookSettingsImpl _$$BookSettingsImplFromJson(Map<String, dynamic> json) => _BookSettings _$BookSettingsFromJson(Map<String, dynamic> json) =>
_$BookSettingsImpl( _BookSettings(
bookId: json['bookId'] as String, bookId: json['bookId'] as String,
playerSettings: json['playerSettings'] == null playerSettings: json['playerSettings'] == null
? const NullablePlayerSettings() ? const NullablePlayerSettings()
: NullablePlayerSettings.fromJson( : NullablePlayerSettings.fromJson(
json['playerSettings'] as Map<String, dynamic>), json['playerSettings'] as Map<String, dynamic>,
),
); );
Map<String, dynamic> _$$BookSettingsImplToJson(_$BookSettingsImpl instance) => Map<String, dynamic> _$BookSettingsToJson(_BookSettings instance) =>
<String, dynamic>{ <String, dynamic>{
'bookId': instance.bookId, 'bookId': instance.bookId,
'playerSettings': instance.playerSettings, 'playerSettings': instance.playerSettings,

View file

@ -5,7 +5,7 @@ part 'nullable_player_settings.freezed.dart';
part 'nullable_player_settings.g.dart'; part 'nullable_player_settings.g.dart';
@freezed @freezed
class NullablePlayerSettings with _$NullablePlayerSettings { sealed class NullablePlayerSettings with _$NullablePlayerSettings {
const factory NullablePlayerSettings({ const factory NullablePlayerSettings({
MinimizedPlayerSettings? miniPlayerSettings, MinimizedPlayerSettings? miniPlayerSettings,
ExpandedPlayerSettings? expandedPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings,

View file

@ -1,5 +1,5 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// coverage:ignore-file
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
@ -9,270 +9,251 @@ part of 'nullable_player_settings.dart';
// FreezedGenerator // FreezedGenerator
// ************************************************************************** // **************************************************************************
// dart format off
T _$identity<T>(T value) => value; T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
NullablePlayerSettings _$NullablePlayerSettingsFromJson(
Map<String, dynamic> json) {
return _NullablePlayerSettings.fromJson(json);
}
/// @nodoc /// @nodoc
mixin _$NullablePlayerSettings { mixin _$NullablePlayerSettings {
MinimizedPlayerSettings? get miniPlayerSettings =>
throw _privateConstructorUsedError;
ExpandedPlayerSettings? get expandedPlayerSettings =>
throw _privateConstructorUsedError;
double? get preferredDefaultVolume => throw _privateConstructorUsedError;
double? get preferredDefaultSpeed => throw _privateConstructorUsedError;
List<double>? get speedOptions => throw _privateConstructorUsedError;
SleepTimerSettings? get sleepTimerSettings =>
throw _privateConstructorUsedError;
Duration? get playbackReportInterval => throw _privateConstructorUsedError;
/// Serializes this NullablePlayerSettings to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
MinimizedPlayerSettings? get miniPlayerSettings; ExpandedPlayerSettings? get expandedPlayerSettings; double? get preferredDefaultVolume; double? get preferredDefaultSpeed; List<double>? get speedOptions; SleepTimerSettings? get sleepTimerSettings; Duration? get playbackReportInterval;
/// Create a copy of NullablePlayerSettings /// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
$NullablePlayerSettingsCopyWith<NullablePlayerSettings> get copyWith => @pragma('vm:prefer-inline')
throw _privateConstructorUsedError; $NullablePlayerSettingsCopyWith<NullablePlayerSettings> get copyWith => _$NullablePlayerSettingsCopyWithImpl<NullablePlayerSettings>(this as NullablePlayerSettings, _$identity);
/// Serializes this NullablePlayerSettings to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is NullablePlayerSettings&&(identical(other.miniPlayerSettings, miniPlayerSettings) || other.miniPlayerSettings == miniPlayerSettings)&&(identical(other.expandedPlayerSettings, expandedPlayerSettings) || other.expandedPlayerSettings == expandedPlayerSettings)&&(identical(other.preferredDefaultVolume, preferredDefaultVolume) || other.preferredDefaultVolume == preferredDefaultVolume)&&(identical(other.preferredDefaultSpeed, preferredDefaultSpeed) || other.preferredDefaultSpeed == preferredDefaultSpeed)&&const DeepCollectionEquality().equals(other.speedOptions, speedOptions)&&(identical(other.sleepTimerSettings, sleepTimerSettings) || other.sleepTimerSettings == sleepTimerSettings)&&(identical(other.playbackReportInterval, playbackReportInterval) || other.playbackReportInterval == playbackReportInterval));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,miniPlayerSettings,expandedPlayerSettings,preferredDefaultVolume,preferredDefaultSpeed,const DeepCollectionEquality().hash(speedOptions),sleepTimerSettings,playbackReportInterval);
@override
String toString() {
return 'NullablePlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, playbackReportInterval: $playbackReportInterval)';
}
} }
/// @nodoc /// @nodoc
abstract class $NullablePlayerSettingsCopyWith<$Res> { abstract mixin class $NullablePlayerSettingsCopyWith<$Res> {
factory $NullablePlayerSettingsCopyWith(NullablePlayerSettings value, factory $NullablePlayerSettingsCopyWith(NullablePlayerSettings value, $Res Function(NullablePlayerSettings) _then) = _$NullablePlayerSettingsCopyWithImpl;
$Res Function(NullablePlayerSettings) then) =
_$NullablePlayerSettingsCopyWithImpl<$Res, NullablePlayerSettings>;
@useResult @useResult
$Res call( $Res call({
{MinimizedPlayerSettings? miniPlayerSettings, MinimizedPlayerSettings? miniPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings, double? preferredDefaultVolume, double? preferredDefaultSpeed, List<double>? speedOptions, SleepTimerSettings? sleepTimerSettings, Duration? playbackReportInterval
ExpandedPlayerSettings? expandedPlayerSettings, });
double? preferredDefaultVolume,
double? preferredDefaultSpeed,
List<double>? speedOptions, $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings;$ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings;$SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings;
SleepTimerSettings? sleepTimerSettings,
Duration? playbackReportInterval});
$MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings;
$ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings;
$SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings;
} }
/// @nodoc /// @nodoc
class _$NullablePlayerSettingsCopyWithImpl<$Res, class _$NullablePlayerSettingsCopyWithImpl<$Res>
$Val extends NullablePlayerSettings>
implements $NullablePlayerSettingsCopyWith<$Res> { implements $NullablePlayerSettingsCopyWith<$Res> {
_$NullablePlayerSettingsCopyWithImpl(this._value, this._then); _$NullablePlayerSettingsCopyWithImpl(this._self, this._then);
// ignore: unused_field final NullablePlayerSettings _self;
final $Val _value; final $Res Function(NullablePlayerSettings) _then;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of NullablePlayerSettings /// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline') @override $Res call({Object? miniPlayerSettings = freezed,Object? expandedPlayerSettings = freezed,Object? preferredDefaultVolume = freezed,Object? preferredDefaultSpeed = freezed,Object? speedOptions = freezed,Object? sleepTimerSettings = freezed,Object? playbackReportInterval = freezed,}) {
@override return _then(_self.copyWith(
$Res call({ miniPlayerSettings: freezed == miniPlayerSettings ? _self.miniPlayerSettings : miniPlayerSettings // ignore: cast_nullable_to_non_nullable
Object? miniPlayerSettings = freezed, as MinimizedPlayerSettings?,expandedPlayerSettings: freezed == expandedPlayerSettings ? _self.expandedPlayerSettings : expandedPlayerSettings // ignore: cast_nullable_to_non_nullable
Object? expandedPlayerSettings = freezed, as ExpandedPlayerSettings?,preferredDefaultVolume: freezed == preferredDefaultVolume ? _self.preferredDefaultVolume : preferredDefaultVolume // ignore: cast_nullable_to_non_nullable
Object? preferredDefaultVolume = freezed, as double?,preferredDefaultSpeed: freezed == preferredDefaultSpeed ? _self.preferredDefaultSpeed : preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable
Object? preferredDefaultSpeed = freezed, as double?,speedOptions: freezed == speedOptions ? _self.speedOptions : speedOptions // ignore: cast_nullable_to_non_nullable
Object? speedOptions = freezed, as List<double>?,sleepTimerSettings: freezed == sleepTimerSettings ? _self.sleepTimerSettings : sleepTimerSettings // ignore: cast_nullable_to_non_nullable
Object? sleepTimerSettings = freezed, as SleepTimerSettings?,playbackReportInterval: freezed == playbackReportInterval ? _self.playbackReportInterval : playbackReportInterval // ignore: cast_nullable_to_non_nullable
Object? playbackReportInterval = freezed,
}) {
return _then(_value.copyWith(
miniPlayerSettings: freezed == miniPlayerSettings
? _value.miniPlayerSettings
: miniPlayerSettings // ignore: cast_nullable_to_non_nullable
as MinimizedPlayerSettings?,
expandedPlayerSettings: freezed == expandedPlayerSettings
? _value.expandedPlayerSettings
: expandedPlayerSettings // ignore: cast_nullable_to_non_nullable
as ExpandedPlayerSettings?,
preferredDefaultVolume: freezed == preferredDefaultVolume
? _value.preferredDefaultVolume
: preferredDefaultVolume // ignore: cast_nullable_to_non_nullable
as double?,
preferredDefaultSpeed: freezed == preferredDefaultSpeed
? _value.preferredDefaultSpeed
: preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable
as double?,
speedOptions: freezed == speedOptions
? _value.speedOptions
: speedOptions // ignore: cast_nullable_to_non_nullable
as List<double>?,
sleepTimerSettings: freezed == sleepTimerSettings
? _value.sleepTimerSettings
: sleepTimerSettings // ignore: cast_nullable_to_non_nullable
as SleepTimerSettings?,
playbackReportInterval: freezed == playbackReportInterval
? _value.playbackReportInterval
: playbackReportInterval // ignore: cast_nullable_to_non_nullable
as Duration?, as Duration?,
) as $Val); ));
} }
/// Create a copy of NullablePlayerSettings /// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings { $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings {
if (_value.miniPlayerSettings == null) { if (_self.miniPlayerSettings == null) {
return null; return null;
} }
return $MinimizedPlayerSettingsCopyWith<$Res>(_value.miniPlayerSettings!, return $MinimizedPlayerSettingsCopyWith<$Res>(_self.miniPlayerSettings!, (value) {
(value) { return _then(_self.copyWith(miniPlayerSettings: value));
return _then(_value.copyWith(miniPlayerSettings: value) as $Val);
}); });
} }/// Create a copy of NullablePlayerSettings
/// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings { $ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings {
if (_value.expandedPlayerSettings == null) { if (_self.expandedPlayerSettings == null) {
return null; return null;
} }
return $ExpandedPlayerSettingsCopyWith<$Res>(_value.expandedPlayerSettings!, return $ExpandedPlayerSettingsCopyWith<$Res>(_self.expandedPlayerSettings!, (value) {
(value) { return _then(_self.copyWith(expandedPlayerSettings: value));
return _then(_value.copyWith(expandedPlayerSettings: value) as $Val);
}); });
} }/// Create a copy of NullablePlayerSettings
/// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
$SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings { $SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings {
if (_value.sleepTimerSettings == null) { if (_self.sleepTimerSettings == null) {
return null; return null;
} }
return $SleepTimerSettingsCopyWith<$Res>(_value.sleepTimerSettings!, return $SleepTimerSettingsCopyWith<$Res>(_self.sleepTimerSettings!, (value) {
(value) { return _then(_self.copyWith(sleepTimerSettings: value));
return _then(_value.copyWith(sleepTimerSettings: value) as $Val);
}); });
} }
} }
/// @nodoc
abstract class _$$NullablePlayerSettingsImplCopyWith<$Res>
implements $NullablePlayerSettingsCopyWith<$Res> {
factory _$$NullablePlayerSettingsImplCopyWith(
_$NullablePlayerSettingsImpl value,
$Res Function(_$NullablePlayerSettingsImpl) then) =
__$$NullablePlayerSettingsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{MinimizedPlayerSettings? miniPlayerSettings,
ExpandedPlayerSettings? expandedPlayerSettings,
double? preferredDefaultVolume,
double? preferredDefaultSpeed,
List<double>? speedOptions,
SleepTimerSettings? sleepTimerSettings,
Duration? playbackReportInterval});
@override /// Adds pattern-matching-related methods to [NullablePlayerSettings].
$MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings; extension NullablePlayerSettingsPatterns on NullablePlayerSettings {
@override /// A variant of `map` that fallback to returning `orElse`.
$ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings; ///
@override /// It is equivalent to doing:
$SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings; /// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _NullablePlayerSettings value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _NullablePlayerSettings() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _NullablePlayerSettings value) $default,){
final _that = this;
switch (_that) {
case _NullablePlayerSettings():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _NullablePlayerSettings value)? $default,){
final _that = this;
switch (_that) {
case _NullablePlayerSettings() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( MinimizedPlayerSettings? miniPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings, double? preferredDefaultVolume, double? preferredDefaultSpeed, List<double>? speedOptions, SleepTimerSettings? sleepTimerSettings, Duration? playbackReportInterval)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _NullablePlayerSettings() when $default != null:
return $default(_that.miniPlayerSettings,_that.expandedPlayerSettings,_that.preferredDefaultVolume,_that.preferredDefaultSpeed,_that.speedOptions,_that.sleepTimerSettings,_that.playbackReportInterval);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( MinimizedPlayerSettings? miniPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings, double? preferredDefaultVolume, double? preferredDefaultSpeed, List<double>? speedOptions, SleepTimerSettings? sleepTimerSettings, Duration? playbackReportInterval) $default,) {final _that = this;
switch (_that) {
case _NullablePlayerSettings():
return $default(_that.miniPlayerSettings,_that.expandedPlayerSettings,_that.preferredDefaultVolume,_that.preferredDefaultSpeed,_that.speedOptions,_that.sleepTimerSettings,_that.playbackReportInterval);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( MinimizedPlayerSettings? miniPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings, double? preferredDefaultVolume, double? preferredDefaultSpeed, List<double>? speedOptions, SleepTimerSettings? sleepTimerSettings, Duration? playbackReportInterval)? $default,) {final _that = this;
switch (_that) {
case _NullablePlayerSettings() when $default != null:
return $default(_that.miniPlayerSettings,_that.expandedPlayerSettings,_that.preferredDefaultVolume,_that.preferredDefaultSpeed,_that.speedOptions,_that.sleepTimerSettings,_that.playbackReportInterval);case _:
return null;
}
} }
/// @nodoc
class __$$NullablePlayerSettingsImplCopyWithImpl<$Res>
extends _$NullablePlayerSettingsCopyWithImpl<$Res,
_$NullablePlayerSettingsImpl>
implements _$$NullablePlayerSettingsImplCopyWith<$Res> {
__$$NullablePlayerSettingsImplCopyWithImpl(
_$NullablePlayerSettingsImpl _value,
$Res Function(_$NullablePlayerSettingsImpl) _then)
: super(_value, _then);
/// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? miniPlayerSettings = freezed,
Object? expandedPlayerSettings = freezed,
Object? preferredDefaultVolume = freezed,
Object? preferredDefaultSpeed = freezed,
Object? speedOptions = freezed,
Object? sleepTimerSettings = freezed,
Object? playbackReportInterval = freezed,
}) {
return _then(_$NullablePlayerSettingsImpl(
miniPlayerSettings: freezed == miniPlayerSettings
? _value.miniPlayerSettings
: miniPlayerSettings // ignore: cast_nullable_to_non_nullable
as MinimizedPlayerSettings?,
expandedPlayerSettings: freezed == expandedPlayerSettings
? _value.expandedPlayerSettings
: expandedPlayerSettings // ignore: cast_nullable_to_non_nullable
as ExpandedPlayerSettings?,
preferredDefaultVolume: freezed == preferredDefaultVolume
? _value.preferredDefaultVolume
: preferredDefaultVolume // ignore: cast_nullable_to_non_nullable
as double?,
preferredDefaultSpeed: freezed == preferredDefaultSpeed
? _value.preferredDefaultSpeed
: preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable
as double?,
speedOptions: freezed == speedOptions
? _value._speedOptions
: speedOptions // ignore: cast_nullable_to_non_nullable
as List<double>?,
sleepTimerSettings: freezed == sleepTimerSettings
? _value.sleepTimerSettings
: sleepTimerSettings // ignore: cast_nullable_to_non_nullable
as SleepTimerSettings?,
playbackReportInterval: freezed == playbackReportInterval
? _value.playbackReportInterval
: playbackReportInterval // ignore: cast_nullable_to_non_nullable
as Duration?,
));
}
} }
/// @nodoc /// @nodoc
@JsonSerializable() @JsonSerializable()
class _$NullablePlayerSettingsImpl implements _NullablePlayerSettings {
const _$NullablePlayerSettingsImpl(
{this.miniPlayerSettings,
this.expandedPlayerSettings,
this.preferredDefaultVolume,
this.preferredDefaultSpeed,
final List<double>? speedOptions,
this.sleepTimerSettings,
this.playbackReportInterval})
: _speedOptions = speedOptions;
factory _$NullablePlayerSettingsImpl.fromJson(Map<String, dynamic> json) => class _NullablePlayerSettings implements NullablePlayerSettings {
_$$NullablePlayerSettingsImplFromJson(json); const _NullablePlayerSettings({this.miniPlayerSettings, this.expandedPlayerSettings, this.preferredDefaultVolume, this.preferredDefaultSpeed, final List<double>? speedOptions, this.sleepTimerSettings, this.playbackReportInterval}): _speedOptions = speedOptions;
factory _NullablePlayerSettings.fromJson(Map<String, dynamic> json) => _$NullablePlayerSettingsFromJson(json);
@override @override final MinimizedPlayerSettings? miniPlayerSettings;
final MinimizedPlayerSettings? miniPlayerSettings; @override final ExpandedPlayerSettings? expandedPlayerSettings;
@override @override final double? preferredDefaultVolume;
final ExpandedPlayerSettings? expandedPlayerSettings; @override final double? preferredDefaultSpeed;
@override
final double? preferredDefaultVolume;
@override
final double? preferredDefaultSpeed;
final List<double>? _speedOptions; final List<double>? _speedOptions;
@override @override List<double>? get speedOptions {
List<double>? get speedOptions {
final value = _speedOptions; final value = _speedOptions;
if (value == null) return null; if (value == null) return null;
if (_speedOptions is EqualUnmodifiableListView) return _speedOptions; if (_speedOptions is EqualUnmodifiableListView) return _speedOptions;
@ -280,98 +261,109 @@ class _$NullablePlayerSettingsImpl implements _NullablePlayerSettings {
return EqualUnmodifiableListView(value); return EqualUnmodifiableListView(value);
} }
@override final SleepTimerSettings? sleepTimerSettings;
@override final Duration? playbackReportInterval;
/// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$NullablePlayerSettingsCopyWith<_NullablePlayerSettings> get copyWith => __$NullablePlayerSettingsCopyWithImpl<_NullablePlayerSettings>(this, _$identity);
@override @override
final SleepTimerSettings? sleepTimerSettings; Map<String, dynamic> toJson() {
return _$NullablePlayerSettingsToJson(this, );
}
@override @override
final Duration? playbackReportInterval; bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _NullablePlayerSettings&&(identical(other.miniPlayerSettings, miniPlayerSettings) || other.miniPlayerSettings == miniPlayerSettings)&&(identical(other.expandedPlayerSettings, expandedPlayerSettings) || other.expandedPlayerSettings == expandedPlayerSettings)&&(identical(other.preferredDefaultVolume, preferredDefaultVolume) || other.preferredDefaultVolume == preferredDefaultVolume)&&(identical(other.preferredDefaultSpeed, preferredDefaultSpeed) || other.preferredDefaultSpeed == preferredDefaultSpeed)&&const DeepCollectionEquality().equals(other._speedOptions, _speedOptions)&&(identical(other.sleepTimerSettings, sleepTimerSettings) || other.sleepTimerSettings == sleepTimerSettings)&&(identical(other.playbackReportInterval, playbackReportInterval) || other.playbackReportInterval == playbackReportInterval));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,miniPlayerSettings,expandedPlayerSettings,preferredDefaultVolume,preferredDefaultSpeed,const DeepCollectionEquality().hash(_speedOptions),sleepTimerSettings,playbackReportInterval);
@override @override
String toString() { String toString() {
return 'NullablePlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, playbackReportInterval: $playbackReportInterval)'; return 'NullablePlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings, playbackReportInterval: $playbackReportInterval)';
} }
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$NullablePlayerSettingsImpl &&
(identical(other.miniPlayerSettings, miniPlayerSettings) ||
other.miniPlayerSettings == miniPlayerSettings) &&
(identical(other.expandedPlayerSettings, expandedPlayerSettings) ||
other.expandedPlayerSettings == expandedPlayerSettings) &&
(identical(other.preferredDefaultVolume, preferredDefaultVolume) ||
other.preferredDefaultVolume == preferredDefaultVolume) &&
(identical(other.preferredDefaultSpeed, preferredDefaultSpeed) ||
other.preferredDefaultSpeed == preferredDefaultSpeed) &&
const DeepCollectionEquality()
.equals(other._speedOptions, _speedOptions) &&
(identical(other.sleepTimerSettings, sleepTimerSettings) ||
other.sleepTimerSettings == sleepTimerSettings) &&
(identical(other.playbackReportInterval, playbackReportInterval) ||
other.playbackReportInterval == playbackReportInterval));
} }
@JsonKey(includeFromJson: false, includeToJson: false) /// @nodoc
@override abstract mixin class _$NullablePlayerSettingsCopyWith<$Res> implements $NullablePlayerSettingsCopyWith<$Res> {
int get hashCode => Object.hash( factory _$NullablePlayerSettingsCopyWith(_NullablePlayerSettings value, $Res Function(_NullablePlayerSettings) _then) = __$NullablePlayerSettingsCopyWithImpl;
runtimeType, @override @useResult
miniPlayerSettings, $Res call({
expandedPlayerSettings, MinimizedPlayerSettings? miniPlayerSettings, ExpandedPlayerSettings? expandedPlayerSettings, double? preferredDefaultVolume, double? preferredDefaultSpeed, List<double>? speedOptions, SleepTimerSettings? sleepTimerSettings, Duration? playbackReportInterval
preferredDefaultVolume, });
preferredDefaultSpeed,
const DeepCollectionEquality().hash(_speedOptions),
sleepTimerSettings, @override $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings;@override $ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings;@override $SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings;
playbackReportInterval);
}
/// @nodoc
class __$NullablePlayerSettingsCopyWithImpl<$Res>
implements _$NullablePlayerSettingsCopyWith<$Res> {
__$NullablePlayerSettingsCopyWithImpl(this._self, this._then);
final _NullablePlayerSettings _self;
final $Res Function(_NullablePlayerSettings) _then;
/// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? miniPlayerSettings = freezed,Object? expandedPlayerSettings = freezed,Object? preferredDefaultVolume = freezed,Object? preferredDefaultSpeed = freezed,Object? speedOptions = freezed,Object? sleepTimerSettings = freezed,Object? playbackReportInterval = freezed,}) {
return _then(_NullablePlayerSettings(
miniPlayerSettings: freezed == miniPlayerSettings ? _self.miniPlayerSettings : miniPlayerSettings // ignore: cast_nullable_to_non_nullable
as MinimizedPlayerSettings?,expandedPlayerSettings: freezed == expandedPlayerSettings ? _self.expandedPlayerSettings : expandedPlayerSettings // ignore: cast_nullable_to_non_nullable
as ExpandedPlayerSettings?,preferredDefaultVolume: freezed == preferredDefaultVolume ? _self.preferredDefaultVolume : preferredDefaultVolume // ignore: cast_nullable_to_non_nullable
as double?,preferredDefaultSpeed: freezed == preferredDefaultSpeed ? _self.preferredDefaultSpeed : preferredDefaultSpeed // ignore: cast_nullable_to_non_nullable
as double?,speedOptions: freezed == speedOptions ? _self._speedOptions : speedOptions // ignore: cast_nullable_to_non_nullable
as List<double>?,sleepTimerSettings: freezed == sleepTimerSettings ? _self.sleepTimerSettings : sleepTimerSettings // ignore: cast_nullable_to_non_nullable
as SleepTimerSettings?,playbackReportInterval: freezed == playbackReportInterval ? _self.playbackReportInterval : playbackReportInterval // ignore: cast_nullable_to_non_nullable
as Duration?,
));
}
/// Create a copy of NullablePlayerSettings /// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override @override
@pragma('vm:prefer-inline') @pragma('vm:prefer-inline')
_$$NullablePlayerSettingsImplCopyWith<_$NullablePlayerSettingsImpl> $MinimizedPlayerSettingsCopyWith<$Res>? get miniPlayerSettings {
get copyWith => __$$NullablePlayerSettingsImplCopyWithImpl< if (_self.miniPlayerSettings == null) {
_$NullablePlayerSettingsImpl>(this, _$identity); return null;
@override
Map<String, dynamic> toJson() {
return _$$NullablePlayerSettingsImplToJson(
this,
);
}
} }
abstract class _NullablePlayerSettings implements NullablePlayerSettings { return $MinimizedPlayerSettingsCopyWith<$Res>(_self.miniPlayerSettings!, (value) {
const factory _NullablePlayerSettings( return _then(_self.copyWith(miniPlayerSettings: value));
{final MinimizedPlayerSettings? miniPlayerSettings, });
final ExpandedPlayerSettings? expandedPlayerSettings, }/// Create a copy of NullablePlayerSettings
final double? preferredDefaultVolume,
final double? preferredDefaultSpeed,
final List<double>? speedOptions,
final SleepTimerSettings? sleepTimerSettings,
final Duration? playbackReportInterval}) = _$NullablePlayerSettingsImpl;
factory _NullablePlayerSettings.fromJson(Map<String, dynamic> json) =
_$NullablePlayerSettingsImpl.fromJson;
@override
MinimizedPlayerSettings? get miniPlayerSettings;
@override
ExpandedPlayerSettings? get expandedPlayerSettings;
@override
double? get preferredDefaultVolume;
@override
double? get preferredDefaultSpeed;
@override
List<double>? get speedOptions;
@override
SleepTimerSettings? get sleepTimerSettings;
@override
Duration? get playbackReportInterval;
/// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @override
@JsonKey(includeFromJson: false, includeToJson: false) @pragma('vm:prefer-inline')
_$$NullablePlayerSettingsImplCopyWith<_$NullablePlayerSettingsImpl> $ExpandedPlayerSettingsCopyWith<$Res>? get expandedPlayerSettings {
get copyWith => throw _privateConstructorUsedError; if (_self.expandedPlayerSettings == null) {
return null;
} }
return $ExpandedPlayerSettingsCopyWith<$Res>(_self.expandedPlayerSettings!, (value) {
return _then(_self.copyWith(expandedPlayerSettings: value));
});
}/// Create a copy of NullablePlayerSettings
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$SleepTimerSettingsCopyWith<$Res>? get sleepTimerSettings {
if (_self.sleepTimerSettings == null) {
return null;
}
return $SleepTimerSettingsCopyWith<$Res>(_self.sleepTimerSettings!, (value) {
return _then(_self.copyWith(sleepTimerSettings: value));
});
}
}
// dart format on

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