Compare commits

..

92 commits
v0.0.3 ... main

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
Dr-Blank
7b0c2c4b88
chore: bump version to 0.0.10+1 in pubspec.yaml 2024-10-02 09:35:46 -04:00
Dr-Blank
02b6194d8f
ci: reorder Java setup in CI workflow for improved efficiency in case of test failure 2024-10-02 09:33:12 -04:00
Dr-Blank
be5eb3fd13
test: add TimeOfDay extension tests and enable test execution in CI 2024-10-02 09:32:05 -04:00
Dr.Blank
12100ffbcd
ui: better sleep timer ui in player and fix auto turn on settings (#43)
* refactor: enhance sleep timer functionality and improve duration formatting

* refactor: update sleep timer settings handling

* refactor: update cancel icon for sleep timer button

* refactor: implement isBetween method for TimeOfDay and simplify sleep timer logic

* refactor: update alwaysAutoTurnOnTimer default value and improve icon usage in settings

* refactor: remove unused IconButton and update sleep timer preset durations
2024-10-02 09:18:06 -04:00
Dr-Blank
933bfc5750
chore: update release drafter configuration to include UI changes category 2024-10-02 09:07:21 -04:00
Dr-Blank
4c5dc53169
refactor: update default speed options for player settings 2024-10-02 02:39:33 -04:00
Dr-Blank
ce74ead2b3
docs: update README with features and download instructions 2024-10-02 02:20:16 -04:00
Dr-Blank
e9b2fc074c
chore: add privacy policy document 2024-10-01 02:47:22 -04:00
Dr-Blank
e9c4b9cd86
chore: remove rxdart dependency from pubspec.yaml 2024-10-01 02:17:38 -04:00
Dr.Blank
29f4c93554
fix: notification logo not showing (#42) 2024-10-01 02:16:08 -04:00
Dr-Blank
3400b725f2
chore: update dependencies to latest versions 2024-09-30 23:18:02 -04:00
Dr-Blank
35d3bb362d
chore: bump version to 0.0.9+1 2024-09-30 05:44:52 -04:00
Dr.Blank
90913162cc
fix: speed selector wheel was not visible (#41) 2024-09-30 04:19:15 -04:00
Dr-Blank
a790a7e583
ci: add upload step for APKs in test workflow 2024-09-30 02:47:13 -04:00
Dr.Blank
67d6c9240b
Fix Shake Detector not working when app in background (#40)
* feat: update shake detection settings to reduce cooldown and feedback options

* fix: shake detector not detecting in background

* enhance shake action handling to avoid unnecessary feedback

* disable shake detector when player not playing anything

* refactor: remove outdated TODO regarding shake detection optimization

* refactor: comment out notifyListeners call in restartTimer method for clarity
2024-09-30 02:34:13 -04:00
Dr-Blank
6c0265fe5f
chore: bump version to 0.0.8+1 2024-09-28 04:37:27 -04:00
Dr-Blank
b8059cad4d
chore: update GitHub Actions workflow for APK and AAB builds 2024-09-28 04:34:40 -04:00
Dr.Blank
b229c4f2f5
feat: add shake detection functionality (#36)
* feat: add shake detection functionality and integrate vibration support

* feat: add shake detector settings page
2024-09-28 01:27:56 -04:00
Dr-Blank
2e3b1de529
chore: bump version to 0.0.7+1 2024-09-26 04:48:10 -04:00
Dr.Blank
150e5c9025
feat: add player settings page and enhance settings UI (#33) 2024-09-26 04:30:51 -04:00
Dr.Blank
8049a660e6
feat: new settings to configure when to report playback and when to mark item complete (#32) 2024-09-26 01:39:43 -04:00
Dr-Blank
7279fa0bd6
chore: bump version to 0.0.6+1 2024-09-25 03:23:44 -04:00
Dr.Blank
3cf0a0b124
feat: extensive settings for media controls through notification (#28)
* feat: add notification settings customisation options

* feat: add notification settings page and update routing
2024-09-25 03:13:42 -04:00
Dr.Blank
721b0a87fc
feat: enhance navigation handling by adding debug messages and settings page routing (#29) 2024-09-25 02:58:33 -04:00
Dr.Blank
bf53ec9299
fix android notification icon missing for media playback (#25) 2024-09-24 00:17:36 -04:00
Dr-Blank
84e5fd9d81
refactor: clamp download progress value to start from .05 2024-09-23 04:28:08 -04:00
Dr-Blank
a5ec573a76
chore: bump version to 0.0.5+1 2024-09-23 03:57:15 -04:00
Dr-Blank
405d625cdc
refactor: update cover image handling to use item IDs and simplify library item actions 2024-09-23 03:55:32 -04:00
Dr-Blank
d25d23a0b7
feat: add keystore decoding and signing configuration for release builds 2024-09-23 01:09:45 -04:00
Dr-Blank
c032595fc1
chore: bump version to 0.0.4+1 2024-09-22 22:43:10 -04:00
Dr-Blank
ef134af1fc
feat: add GitHub Actions workflow for Flutter testing and APK build 2024-09-22 22:30:58 -04:00
Dr-Blank
5c9273c812
refactor: simplify queueAudioBookDownload method by removing callback handling 2024-09-22 22:24:36 -04:00
Dr.Blank
792448b0ef
feat: downloads support (#22)
* feat: enhance download manager with improved logging and task handling

* feat: add total size calculation for library items and improve download manager functionality

* refactor: simplify parameters in queueAudioBookDownload and improve logging message in deleteDownloadedItem
2024-09-22 22:05:28 -04:00
Dr-Blank
bee1d233bf
refactor: replace BottomNavigationBar with NavigationBar and adjust height calculation 2024-09-19 00:44:27 -04:00
Dr-Blank
36c90c20bc
chore: update release drafter configuration and workflow to v6 2024-09-18 01:38:02 -04:00
219 changed files with 17306 additions and 10620 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

@ -1,13 +1,14 @@
name-template: "v$RESOLVED_VERSION 🌈"
name-template: "v$RESOLVED_VERSION"
tag-template: "v$RESOLVED_VERSION"
# name-template: 'v$NEXT_PATCH_VERSION'
# tag-template: 'v$NEXT_PATCH_VERSION'
categories:
- title: "🚀 Features"
labels:
- "feature"
- "enhancement"
- title: "📱 UI Changes"
labels:
- "ui"
- title: "🐛 Bug Fixes"
labels:
- "fix"
@ -15,7 +16,10 @@ categories:
- "bug"
- title: "🧰 Maintenance"
label: "chore"
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
- title: "💥 Breaking Changes"
labels:
- "breaking"
change-template: "- $TITLE (#$NUMBER)"
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
version-resolver:
major:
@ -33,10 +37,10 @@ template: |
$CHANGES
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...$NEW_TAG
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
exclude-labels:
- "skip-changelog"
- "skip changelog"
exclude-contributors:
- "Dr-Blank"
@ -51,6 +55,15 @@ autolabeler:
branch:
- '/feature\/.+/'
title:
- "/feat(ure)?/i"
- "/^feat(ure)?/i"
body:
- "/JIRA-[0-9]{1,4}/"
- label: "chore"
title:
- "/^chore\b/i"
- label: "ui"
title:
- "/^ui\b/i"
- label: "refactor"
title:
- "/^refactor/i"

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,75 +0,0 @@
name: Flutter Release Workflow
on:
push:
tags:
- "v**"
# manually trigger a release if needed
workflow_dispatch:
jobs:
build:
permissions:
# write permission is required to create a github release
contents: write
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Checkout shelfsdk
uses: actions/checkout@v3
with:
repository: Dr-Blank/shelfsdk
path: ./shelfsdk
- name: Set Up Java
uses: actions/setup-java@v3.12.0
with:
distribution: "oracle"
java-version: "17"
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
- name: Install dependencies
run: flutter pub get
# - name: Run tests
# run: flutter test
- name: Build APK
run: flutter build apk --release
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: release-apk
path: build/app/outputs/flutter-apk/app-release.apk
- name: version
id: version
run: |
tag=${GITHUB_REF/refs\/tags\//}
version=${tag#v}
major=${version%%.*}
echo "tag=${tag}" >> $GITHUB_OUTPUT
echo "version=${version}" >> $GITHUB_OUTPUT
echo "major=${major}" >> $GITHUB_OUTPUT
- name: Generate Release Notes
id: generate_release_notes
uses: release-drafter/release-drafter@v5
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
artifacts: build/app/outputs/flutter-apk/app-release.apk
name: Release ${{ steps.version.outputs.version }}
tag: ${{ github.ref }}
body: ${{ steps.generate_release_notes.outputs.body }}

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/
/build/
dist/
# Symbolication related
app.*.symbols
@ -41,6 +42,10 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
/android/app/.cxx/
# separate git repo for api sdk
/shelfsdk
# secret keys
/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",
"request": "launch",
"program": "lib/main.dart",
"type": "dart"
},
{

28
.vscode/settings.json vendored
View file

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

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

@ -6,17 +6,38 @@
Client for [Audiobookshelf](https://github.com/advplyr/audiobookshelf) server made with Flutter.
## Features
* Functional Player: Speed Control, Sleep Timer, Shake to Control Player
* Save data with Offline listening and caching
* Material Design
* Extensive Settings to customize the every tiny detail
## Download
### Android
<!-- 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) [<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)
*<small>Play Store version is paid if you want to support the development.</small>*
### 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
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" />|
|:---:|:---:|:---:|
|Home|Book View|Player|
| <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 |
Currently, the app is in development and is not ready for production use.
Plan is to have support for android, ios, and desktop.
Plan is to have support for android, and desktop.

View file

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

View file

@ -22,10 +22,20 @@ if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
namespace "dr.blank.vaani"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
// The NDK version is set to a specific version since it was not building
// TODO remove when https://github.com/flutter/flutter/issues/139427 is closed
// ndkVersion = "29.0.13113456"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
@ -40,22 +50,40 @@ android {
main.java.srcDirs += 'src/main/kotlin'
}
// see: https://gitlab.com/IzzyOnDroid/repo/-/issues/623#note_2149548690
// https://android.izzysoft.de/articles/named/iod-scan-apkchecks#blobs
dependenciesInfo {
// Disables dependency metadata when building APKs.
includeInApk = false
// Disables dependency metadata when building Android App Bundles.
includeInBundle = false
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "dr.blank.vaani"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 23
minSdkVersion flutter.minSdkVersion
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
signingConfigs {
release {
keyAlias = keystoreProperties['keyAlias']
keyPassword = keystoreProperties['keyPassword']
storeFile = keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword = keystoreProperties['storePassword']
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig signingConfigs.debug
// signingConfig signingConfigs.debug
signingConfig = signingConfigs.release
}
}
}
@ -65,3 +93,11 @@ flutter {
}
dependencies {}
// https://stackoverflow.com/questions/78626580/how-to-resolve-app-execution-failure-due-to-androidx-corecore1-15-0-alpha
configurations.all {
resolutionStrategy {
force "androidx.core:core:1.13.1"
force "androidx.core:core-ktx:1.13.1"
}
}

View file

@ -6,9 +6,14 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<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
android:label="Vaani"
android:name="${applicationName}"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:icon="@mipmap/ic_launcher">
<!-- android:name=".MainActivity" -->
<activity

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@drawable/ic_stat_logo" />

View file

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

View file

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

BIN
assets/fonts/AbsIcons.ttf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
assets/sounds/beep.mp3 Normal file

Binary file not shown.

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 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/db/cache_manager.dart';
import 'package:vaani/models/error_response.dart';
import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/authenticated_user.dart';
import 'package:vaani/shared/extensions/obfuscation.dart';
part 'api_provider.g.dart';
// TODO: workaround for https://github.com/rrousselGit/riverpod/issues/3718
typedef ResponseErrorHandler = void Function(
Response response, [
Object? error,
]);
typedef ResponseErrorHandler =
void Function(Response response, [Object? error]);
final _logger = Logger('api_provider');
@ -31,23 +33,21 @@ Uri makeBaseUrl(String address) {
/// get the api instance for the given base url
@riverpod
AudiobookshelfApi audiobookshelfApi(AudiobookshelfApiRef ref, Uri? baseUrl) {
AudiobookshelfApi audiobookshelfApi(Ref ref, Uri? baseUrl) {
// try to get the base url from app settings
final apiSettings = ref.watch(apiSettingsProvider);
baseUrl ??= apiSettings.activeServer?.serverUrl;
return AudiobookshelfApi(
baseUrl: makeBaseUrl(baseUrl.toString()),
);
return AudiobookshelfApi(baseUrl: makeBaseUrl(baseUrl.toString()));
}
/// get the api instance for the authenticated user
///
/// if the user is not authenticated throw an error
@Riverpod(keepAlive: true)
AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) {
final apiSettings = ref.watch(apiSettingsProvider);
final user = apiSettings.activeUser;
AudiobookshelfApi authenticatedApi(Ref ref) {
final user = ref.watch(apiSettingsProvider.select((s) => s.activeUser));
if (user == null) {
_logger.severe('No active user can not provide authenticated api');
throw StateError('No active user');
}
return AudiobookshelfApi(
@ -58,15 +58,15 @@ AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) {
/// ping the server to check if it is reachable
@riverpod
FutureOr<bool> isServerAlive(IsServerAliveRef ref, String address) async {
FutureOr<bool> isServerAlive(Ref ref, String address) async {
if (address.isEmpty) {
return false;
}
try {
return await AudiobookshelfApi(baseUrl: makeBaseUrl(address))
.server
.ping() ??
return await AudiobookshelfApi(
baseUrl: makeBaseUrl(address),
).server.ping() ??
false;
} catch (e) {
return false;
@ -76,14 +76,15 @@ FutureOr<bool> isServerAlive(IsServerAliveRef ref, String address) async {
/// fetch status of server
@riverpod
FutureOr<ServerStatusResponse?> serverStatus(
ServerStatusRef ref,
Ref ref,
Uri baseUrl, [
ResponseErrorHandler? responseErrorHandler,
]) async {
_logger.fine('fetching server status: $baseUrl');
_logger.fine('fetching server status: ${baseUrl.obfuscate()}');
final api = ref.watch(audiobookshelfApiProvider(baseUrl));
final res =
await api.server.status(responseErrorHandler: responseErrorHandler);
final res = await api.server.status(
responseErrorHandler: responseErrorHandler,
);
_logger.fine('server status: $res');
return res;
}
@ -96,20 +97,32 @@ class PersonalizedView extends _$PersonalizedView {
final api = ref.watch(authenticatedApiProvider);
final apiSettings = ref.watch(apiSettingsProvider);
final user = apiSettings.activeUser;
if (user == null) {
_logger.warning('no active user');
yield [];
return;
}
if (apiSettings.activeLibraryId == null) {
// set it to default user library by logging in and getting the library id
final login =
await api.login(username: user!.username!, password: user.password!);
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(activeLibraryId: login!.userDefaultLibraryId),
final login = await ref.read(loginProvider().future);
if (login == null) {
_logger.shout('failed to login, not building personalized view');
yield [];
return;
}
ref
.read(apiSettingsProvider.notifier)
.updateState(
apiSettings.copyWith(activeLibraryId: login.userDefaultLibraryId),
);
yield [];
return;
}
// try to find in cache
// final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}';
var key = 'personalizedView:${apiSettings.activeLibraryId! + user!.id!}';
final cachedRes = await apiResponseCacheManager.getFileFromMemory(
key,
) ??
final key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}';
final cachedRes =
await apiResponseCacheManager.getFileFromMemory(key) ??
await apiResponseCacheManager.getFileFromCache(key);
if (cachedRes != null) {
_logger.fine('reading from cache: $cachedRes for key: $key');
@ -126,10 +139,11 @@ class PersonalizedView extends _$PersonalizedView {
}
}
// ! exagerated delay
// ! exaggerated delay
// await Future.delayed(const Duration(seconds: 2));
final res = await api.libraries
.getPersonalized(libraryId: apiSettings.activeLibraryId!);
final res = await api.libraries.getPersonalized(
libraryId: apiSettings.activeLibraryId!,
);
// debugPrint('personalizedView: ${res!.map((e) => e).toSet()}');
// save to cache
if (res != null) {
@ -145,21 +159,19 @@ class PersonalizedView extends _$PersonalizedView {
_logger.warning('failed to fetch personalized view');
yield [];
}
}
// method to force refresh the view and ignore the cache
Future<void> forceRefresh() async {
// clear the cache
// TODO: find a better way to clear the cache for only personalized view key
return apiResponseCacheManager.emptyCache();
}
}
/// fetch continue listening audiobooks
@riverpod
FutureOr<GetUserSessionsResponse> fetchContinueListening(
FetchContinueListeningRef ref,
) async {
FutureOr<GetUserSessionsResponse> fetchContinueListening(Ref ref) async {
final api = ref.watch(authenticatedApiProvider);
final res = await api.me.getSessions();
// debugPrint(
@ -169,10 +181,46 @@ FutureOr<GetUserSessionsResponse> fetchContinueListening(
}
@riverpod
FutureOr<User> me(
MeRef ref,
) async {
FutureOr<User> me(Ref ref) async {
final api = ref.watch(authenticatedApiProvider);
final res = await api.me.getUser();
return res!;
final errorResponseHandler = ErrorResponseHandler();
final res = await api.me.getUser(
responseErrorHandler: errorResponseHandler.storeError,
);
if (res == null) {
_logger.severe(
'me failed, got response: ${errorResponseHandler.response.obfuscate()}',
);
throw StateError('me failed');
}
return res;
}
@riverpod
FutureOr<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
// **************************************************************************
String _$audiobookshelfApiHash() => r'2c310ea77fea9918ccf96180a92075acd037bd95';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
@ProviderFor(audiobookshelfApi)
const audiobookshelfApiProvider = AudiobookshelfApiFamily();
final audiobookshelfApiProvider = AudiobookshelfApiFamily._();
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
class AudiobookshelfApiFamily extends Family<AudiobookshelfApi> {
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
const AudiobookshelfApiFamily();
final class AudiobookshelfApiProvider
extends
$FunctionalProvider<
AudiobookshelfApi,
AudiobookshelfApi,
AudiobookshelfApi
>
with $Provider<AudiobookshelfApi> {
/// get the api instance for the given base url
///
/// Copied from [audiobookshelfApi].
AudiobookshelfApiProvider call(
Uri? baseUrl,
) {
return AudiobookshelfApiProvider(
baseUrl,
);
AudiobookshelfApiProvider._({
required AudiobookshelfApiFamily super.from,
required Uri? super.argument,
}) : super(
retry: null,
name: r'audiobookshelfApiProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$audiobookshelfApiHash();
@override
String toString() {
return r'audiobookshelfApiProvider'
''
'($argument)';
}
@$internal
@override
AudiobookshelfApiProvider getProviderOverride(
covariant AudiobookshelfApiProvider provider,
) {
return call(
provider.baseUrl,
);
$ProviderElement<AudiobookshelfApi> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
AudiobookshelfApi create(Ref ref) {
final argument = this.argument as Uri?;
return audiobookshelfApi(ref, argument);
}
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',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$audiobookshelfApiHash,
dependencies: AudiobookshelfApiFamily._dependencies,
allTransitiveDependencies:
AudiobookshelfApiFamily._allTransitiveDependencies,
baseUrl: baseUrl,
);
AudiobookshelfApiProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.baseUrl,
}) : super.internal();
final Uri? baseUrl;
@override
Override overrideWith(
AudiobookshelfApi Function(AudiobookshelfApiRef provider) create,
) {
return ProviderOverride(
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AudiobookshelfApi value) {
return $ProviderOverride(
origin: this,
override: AudiobookshelfApiProvider._internal(
(ref) => create(ref as AudiobookshelfApiRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
baseUrl: baseUrl,
),
providerOverride: $SyncValueProvider<AudiobookshelfApi>(value),
);
}
@override
AutoDisposeProviderElement<AudiobookshelfApi> createElement() {
return _AudiobookshelfApiProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AudiobookshelfApiProvider && other.baseUrl == baseUrl;
return other is AudiobookshelfApiProvider && other.argument == argument;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, baseUrl.hashCode);
return _SystemHash.finish(hash);
return argument.hashCode;
}
}
mixin AudiobookshelfApiRef on AutoDisposeProviderRef<AudiobookshelfApi> {
/// The parameter `baseUrl` of this provider.
Uri? get baseUrl;
}
String _$audiobookshelfApiHash() => r'f23a06c404e11867a7f796877eaca99b8ff25458';
class _AudiobookshelfApiProviderElement
extends AutoDisposeProviderElement<AudiobookshelfApi>
with AudiobookshelfApiRef {
_AudiobookshelfApiProviderElement(super.provider);
/// get the api instance for the given base url
final class AudiobookshelfApiFamily extends $Family
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
Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl;
String toString() => r'audiobookshelfApiProvider';
}
String _$authenticatedApiHash() => r'f555efb6eede590b5a8d60cad2e6bfc2847e2d14';
/// get the api instance for the authenticated user
///
/// if the user is not authenticated throw an error
///
/// Copied from [authenticatedApi].
@ProviderFor(authenticatedApi)
final authenticatedApiProvider = Provider<AudiobookshelfApi>.internal(
authenticatedApi,
name: r'authenticatedApiProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$authenticatedApiHash,
dependencies: null,
allTransitiveDependencies: null,
);
final authenticatedApiProvider = AuthenticatedApiProvider._();
typedef AuthenticatedApiRef = ProviderRef<AudiobookshelfApi>;
String _$isServerAliveHash() => r'6ff90b6e0febd2cd4a4d3a5209a59afc778cd3b6';
/// ping the server to check if it is reachable
/// get the api instance for the authenticated user
///
/// Copied from [isServerAlive].
@ProviderFor(isServerAlive)
const isServerAliveProvider = IsServerAliveFamily();
/// if the user is not authenticated throw an error
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
class IsServerAliveFamily extends Family<AsyncValue<bool>> {
/// ping the server to check if it is reachable
final class AuthenticatedApiProvider
extends
$FunctionalProvider<
AudiobookshelfApi,
AudiobookshelfApi,
AudiobookshelfApi
>
with $Provider<AudiobookshelfApi> {
/// get the api instance for the authenticated user
///
/// Copied from [isServerAlive].
const IsServerAliveFamily();
/// if the user is not authenticated throw an error
AuthenticatedApiProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'authenticatedApiProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
IsServerAliveProvider call(
String address,
) {
return IsServerAliveProvider(
address,
);
@override
String debugGetCreateSourceHash() => _$authenticatedApiHash();
@$internal
@override
$ProviderElement<AudiobookshelfApi> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
AudiobookshelfApi create(Ref ref) {
return authenticatedApi(ref);
}
@override
IsServerAliveProvider getProviderOverride(
covariant IsServerAliveProvider provider,
) {
return call(
provider.address,
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AudiobookshelfApi value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AudiobookshelfApi>(value),
);
}
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';
}
String _$authenticatedApiHash() => r'284be2c39823c20fb70035a136c430862c28fa27';
/// ping the server to check if it is reachable
///
/// Copied from [isServerAlive].
class IsServerAliveProvider extends AutoDisposeFutureProvider<bool> {
@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
///
/// Copied from [isServerAlive].
IsServerAliveProvider(
String address,
) : this._internal(
(ref) => isServerAlive(
ref as IsServerAliveRef,
address,
),
from: isServerAliveProvider,
name: r'isServerAliveProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$isServerAliveHash,
dependencies: IsServerAliveFamily._dependencies,
allTransitiveDependencies:
IsServerAliveFamily._allTransitiveDependencies,
address: address,
);
IsServerAliveProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.address,
}) : super.internal();
final String address;
IsServerAliveProvider._({
required IsServerAliveFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'isServerAliveProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
Override overrideWith(
FutureOr<bool> Function(IsServerAliveRef provider) create,
) {
return ProviderOverride(
origin: this,
override: IsServerAliveProvider._internal(
(ref) => create(ref as IsServerAliveRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
address: address,
),
);
String debugGetCreateSourceHash() => _$isServerAliveHash();
@override
String toString() {
return r'isServerAliveProvider'
''
'($argument)';
}
@$internal
@override
AutoDisposeFutureProviderElement<bool> createElement() {
return _IsServerAliveProviderElement(this);
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<bool> create(Ref ref) {
final argument = this.argument as String;
return isServerAlive(ref, argument);
}
@override
bool operator ==(Object other) {
return other is IsServerAliveProvider && other.address == address;
return other is IsServerAliveProvider && other.argument == argument;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, address.hashCode);
return _SystemHash.finish(hash);
return argument.hashCode;
}
}
mixin IsServerAliveRef on AutoDisposeFutureProviderRef<bool> {
/// The parameter `address` of this provider.
String get address;
}
String _$isServerAliveHash() => r'bb3a53cae1eb64b8760a56864feed47b7a3f1c29';
class _IsServerAliveProviderElement
extends AutoDisposeFutureProviderElement<bool> with IsServerAliveRef {
_IsServerAliveProviderElement(super.provider);
/// ping the server to check if it is reachable
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
String get address => (origin as IsServerAliveProvider).address;
String toString() => r'isServerAliveProvider';
}
String _$serverStatusHash() => r'2739906a1862d09b098588ebd16749a09032ee99';
/// fetch status of server
///
/// Copied from [serverStatus].
@ProviderFor(serverStatus)
const serverStatusProvider = ServerStatusFamily();
final serverStatusProvider = ServerStatusFamily._();
/// fetch status of server
///
/// Copied from [serverStatus].
class ServerStatusFamily extends Family<AsyncValue<ServerStatusResponse?>> {
final class ServerStatusProvider
extends
$FunctionalProvider<
AsyncValue<ServerStatusResponse?>,
ServerStatusResponse?,
FutureOr<ServerStatusResponse?>
>
with
$FutureModifier<ServerStatusResponse?>,
$FutureProvider<ServerStatusResponse?> {
/// fetch status of server
///
/// Copied from [serverStatus].
const ServerStatusFamily();
ServerStatusProvider._({
required ServerStatusFamily super.from,
required (Uri, ResponseErrorHandler?) super.argument,
}) : super(
retry: null,
name: r'serverStatusProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$serverStatusHash();
@override
String toString() {
return r'serverStatusProvider'
''
'$argument';
}
@$internal
@override
$FutureProviderElement<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
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
///
/// Copied from [serverStatus].
ServerStatusProvider call(
Uri baseUrl, [
void Function(Response, [Object?])? responseErrorHandler,
]) {
return ServerStatusProvider(
baseUrl,
responseErrorHandler,
);
}
ResponseErrorHandler? responseErrorHandler,
]) => ServerStatusProvider._(
argument: (baseUrl, responseErrorHandler),
from: this,
);
@override
ServerStatusProvider getProviderOverride(
covariant ServerStatusProvider provider,
) {
return call(
provider.baseUrl,
provider.responseErrorHandler,
);
}
static const Iterable<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';
String toString() => 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,
);
/// fetch the personalized view
ServerStatusProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.baseUrl,
required this.responseErrorHandler,
}) : super.internal();
@ProviderFor(PersonalizedView)
final personalizedViewProvider = PersonalizedViewProvider._();
final Uri baseUrl;
final void Function(Response, [Object?])? responseErrorHandler;
@override
Override overrideWith(
FutureOr<ServerStatusResponse?> Function(ServerStatusRef provider) create,
) {
return ProviderOverride(
origin: this,
override: ServerStatusProvider._internal(
(ref) => create(ref as ServerStatusRef),
from: from,
name: null,
/// fetch the personalized view
final class PersonalizedViewProvider
extends $StreamNotifierProvider<PersonalizedView, List<Shelf>> {
/// fetch the personalized view
PersonalizedViewProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'personalizedViewProvider',
isAutoDispose: true,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
baseUrl: baseUrl,
responseErrorHandler: responseErrorHandler,
),
);
}
$allTransitiveDependencies: null,
);
@override
AutoDisposeFutureProviderElement<ServerStatusResponse?> createElement() {
return _ServerStatusProviderElement(this);
}
String debugGetCreateSourceHash() => _$personalizedViewHash();
@$internal
@override
bool operator ==(Object other) {
return other is ServerStatusProvider &&
other.baseUrl == baseUrl &&
other.responseErrorHandler == responseErrorHandler;
}
PersonalizedView create() => PersonalizedView();
}
String _$personalizedViewHash() => r'425e89d99d7e4712b4d6a688f3a12442bd66584f';
/// fetch the personalized view
abstract class _$PersonalizedView extends $StreamNotifier<List<Shelf>> {
Stream<List<Shelf>> build();
@$mustCallSuper
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, baseUrl.hashCode);
hash = _SystemHash.combine(hash, responseErrorHandler.hashCode);
return _SystemHash.finish(hash);
void runBuild() {
final ref = this.ref as $Ref<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);
}
}
mixin ServerStatusRef on AutoDisposeFutureProviderRef<ServerStatusResponse?> {
/// The parameter `baseUrl` of this provider.
Uri get baseUrl;
/// fetch continue listening audiobooks
/// The parameter `responseErrorHandler` of this provider.
void Function(Response, [Object?])? get responseErrorHandler;
}
@ProviderFor(fetchContinueListening)
final fetchContinueListeningProvider = FetchContinueListeningProvider._();
class _ServerStatusProviderElement
extends AutoDisposeFutureProviderElement<ServerStatusResponse?>
with ServerStatusRef {
_ServerStatusProviderElement(super.provider);
/// 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
Uri get baseUrl => (origin as ServerStatusProvider).baseUrl;
String debugGetCreateSourceHash() => _$fetchContinueListeningHash();
@$internal
@override
void Function(Response, [Object?])? get responseErrorHandler =>
(origin as ServerStatusProvider).responseErrorHandler;
$FutureProviderElement<GetUserSessionsResponse> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<GetUserSessionsResponse> create(Ref ref) {
return fetchContinueListening(ref);
}
}
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)
final meProvider = AutoDisposeFutureProvider<User>.internal(
me,
name: r'meProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$meHash,
dependencies: null,
allTransitiveDependencies: null,
);
final meProvider = MeProvider._();
typedef MeRef = AutoDisposeFutureProviderRef<User>;
String _$personalizedViewHash() => r'4c392ece4650bdc36d7195a0ddb8810e8fe4caa9';
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',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
/// fetch the personalized view
///
/// Copied from [PersonalizedView].
@ProviderFor(PersonalizedView)
final personalizedViewProvider =
AutoDisposeStreamNotifierProvider<PersonalizedView, List<Shelf>>.internal(
PersonalizedView.new,
name: r'personalizedViewProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$personalizedViewHash,
dependencies: null,
allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$meHash();
typedef _$PersonalizedView = AutoDisposeStreamNotifier<List<Shelf>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@$internal
@override
$FutureProviderElement<User> $createElement($ProviderPointer pointer) =>
$FutureProviderElement(pointer);
@override
FutureOr<User> create(Ref ref) {
return me(ref);
}
}
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,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$loginHash();
@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/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/audiobookshelf_server.dart';
import 'package:vaani/settings/models/authenticated_user.dart'
as model;
import 'package:vaani/settings/models/authenticated_user.dart' as model;
import 'package:vaani/shared/extensions/obfuscation.dart';
part 'authenticated_user_provider.g.dart';
part 'authenticated_users_provider.g.dart';
final _box = AvailableHiveBoxes.authenticatedUserBox;
final _logger = Logger('authenticated_user_provider');
final _logger = Logger('authenticated_users_provider');
/// provides with a set of authenticated users
@riverpod
class AuthenticatedUser extends _$AuthenticatedUser {
class AuthenticatedUsers extends _$AuthenticatedUsers {
@override
Set<model.AuthenticatedUser> build() {
ref.listenSelf((_, __) {
listenSelf((_, __) {
writeStateToBox();
});
// get the app settings
@ -35,7 +35,7 @@ class AuthenticatedUser extends _$AuthenticatedUser {
Set<model.AuthenticatedUser> readFromBoxOrCreate() {
if (_box.isNotEmpty) {
final foundData = _box.getRange(0, _box.length);
_logger.fine('found users in box: $foundData');
_logger.fine('found users in box: ${foundData.obfuscate()}');
return foundData.toSet();
} else {
_logger.fine('no settings found in box');
@ -49,18 +49,17 @@ class AuthenticatedUser extends _$AuthenticatedUser {
return;
}
_box.addAll(state);
_logger.fine('writing state to box: $state');
_logger.fine('writing state to box: ${state.obfuscate()}');
}
void addUser(model.AuthenticatedUser user, {bool setActive = false}) {
state = state..add(user);
ref.invalidateSelf();
if (setActive) {
final apiSettings = ref.read(apiSettingsProvider);
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(
activeUser: user,
),
);
ref
.read(apiSettingsProvider.notifier)
.updateState(apiSettings.copyWith(activeUser: user));
}
}
@ -80,11 +79,12 @@ class AuthenticatedUser extends _$AuthenticatedUser {
// also remove the user from the active user
final apiSettings = ref.read(apiSettingsProvider);
if (apiSettings.activeUser == user) {
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(
activeUser: null,
),
);
// replace the active user with the first user in the list
// or null if there are no users left
final newActiveUser = state.isNotEmpty ? state.first : null;
ref
.read(apiSettingsProvider.notifier)
.updateState(apiSettings.copyWith(activeUser: newActiveUser));
}
}
}

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

@ -4,6 +4,7 @@ import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.dart';
import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/db/cache_manager.dart';
/// provides cover images for the audiobooks
@ -19,52 +20,52 @@ final _logger = Logger('cover_image_provider');
@Riverpod(keepAlive: true)
class CoverImage extends _$CoverImage {
@override
Stream<Uint8List> build(LibraryItem libraryItem) async* {
Stream<Uint8List> build(String itemId) async* {
final api = ref.watch(authenticatedApiProvider);
// ! artifical delay for testing
// await Future.delayed(const Duration(seconds: 2));
// try to get the image from the cache
final file = await imageCacheManager.getFileFromMemory(libraryItem.id) ??
await imageCacheManager.getFileFromCache(libraryItem.id);
final file =
await imageCacheManager.getFileFromMemory(itemId) ??
await imageCacheManager.getFileFromCache(itemId);
if (file != null) {
// if the image is in the cache, yield it
_logger.fine(
'cover image found in cache for ${libraryItem.id} at ${file.file.path}',
'cover image found in cache for $itemId at ${file.file.path}',
);
yield await file.file.readAsBytes();
final libraryItem = await ref.watch(libraryItemProvider(itemId).future);
// return if no need to fetch from the server
if (libraryItem.updatedAt.isBefore(await file.file.lastModified())) {
_logger.fine(
'cover image is up to date for ${libraryItem.id}, no need to fetch from the server',
'cover image is up to date for $itemId, no need to fetch from the server',
);
return;
} else {
_logger.fine(
'cover image stale for ${libraryItem.id}, fetching from the server',
);
_logger.fine('cover image stale for $itemId, fetching from the server');
}
} else {
_logger.fine('cover image not found in cache for ${libraryItem.id}');
_logger.fine('cover image not found in cache for $itemId');
}
// check if the image is in the cache
final coverImage = await api.items.getCover(
libraryItemId: libraryItem.id,
libraryItemId: itemId,
parameters: const GetImageReqParams(width: 1200),
);
// save the image to the cache
if (coverImage != null) {
final newFile = await imageCacheManager.putFile(
libraryItem.id,
itemId,
coverImage,
key: libraryItem.id,
key: itemId,
fileExtension: 'jpg',
);
_logger.fine(
'cover image fetched for for ${libraryItem.id}, file time: ${await newFile.lastModified()}',
'cover image fetched for for $itemId, file time: ${await newFile.lastModified()}',
);
}

View file

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

@ -13,7 +13,7 @@ part 'library_item_provider.g.dart';
final _logger = Logger('LibraryItemProvider');
/// provides the library item for the given id
@riverpod
@Riverpod(keepAlive: true)
class LibraryItem extends _$LibraryItem {
@override
Stream<shelfsdk.LibraryItemExpanded> build(String id) async* {
@ -22,11 +22,12 @@ class LibraryItem extends _$LibraryItem {
_logger.fine('LibraryItemProvider fetching library item: $id');
// ! this is a mock delay
// await Future.delayed(const Duration(seconds: 10));
// await Future.delayed(const Duration(seconds: 150));
// look for the item in the cache
final key = CacheKey.libraryItem(id);
final cachedFile = await apiResponseCacheManager.getFileFromMemory(key) ??
final cachedFile =
await apiResponseCacheManager.getFileFromMemory(key) ??
await apiResponseCacheManager.getFileFromCache(key);
if (cachedFile != null) {
_logger.fine(

View file

@ -6,183 +6,112 @@ part of 'library_item_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$libraryItemHash() => r'fa3f8309349c5b1b777f1bc919616e51c3f5b520';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$LibraryItem
extends BuildlessAutoDisposeStreamNotifier<shelfsdk.LibraryItemExpanded> {
late final String id;
Stream<shelfsdk.LibraryItemExpanded> build(
String id,
);
}
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
@ProviderFor(LibraryItem)
const libraryItemProvider = LibraryItemFamily();
final libraryItemProvider = LibraryItemFamily._();
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
class LibraryItemFamily
extends Family<AsyncValue<shelfsdk.LibraryItemExpanded>> {
final class LibraryItemProvider
extends $StreamNotifierProvider<LibraryItem, shelfsdk.LibraryItemExpanded> {
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
const LibraryItemFamily();
LibraryItemProvider._({
required LibraryItemFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'libraryItemProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
LibraryItemProvider call(
String id,
) {
return LibraryItemProvider(
id,
);
@override
String debugGetCreateSourceHash() => _$libraryItemHash();
@override
String toString() {
return r'libraryItemProvider'
''
'($argument)';
}
@$internal
@override
LibraryItemProvider getProviderOverride(
covariant LibraryItemProvider provider,
) {
return call(
provider.id,
);
}
static const Iterable<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 AutoDisposeStreamNotifierProviderImpl<
LibraryItem, shelfsdk.LibraryItemExpanded> {
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
LibraryItemProvider(
String id,
) : this._internal(
() => LibraryItem()..id = id,
from: libraryItemProvider,
name: r'libraryItemProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$libraryItemHash,
dependencies: LibraryItemFamily._dependencies,
allTransitiveDependencies:
LibraryItemFamily._allTransitiveDependencies,
id: id,
);
LibraryItemProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.id,
}) : super.internal();
final String id;
@override
Stream<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,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
id: id,
),
);
}
@override
AutoDisposeStreamNotifierProviderElement<LibraryItem,
shelfsdk.LibraryItemExpanded> createElement() {
return _LibraryItemProviderElement(this);
}
LibraryItem create() => LibraryItem();
@override
bool operator ==(Object other) {
return other is LibraryItemProvider && other.id == id;
return other is LibraryItemProvider && other.argument == argument;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
return argument.hashCode;
}
}
mixin LibraryItemRef
on AutoDisposeStreamNotifierProviderRef<shelfsdk.LibraryItemExpanded> {
/// The parameter `id` of this provider.
String get id;
}
String _$libraryItemHash() => r'a3cfa7f912e9498a70b5782899018b6964d6445c';
class _LibraryItemProviderElement
extends AutoDisposeStreamNotifierProviderElement<LibraryItem,
shelfsdk.LibraryItemExpanded> with LibraryItemRef {
_LibraryItemProviderElement(super.provider);
/// provides the library item for the given id
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
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:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:vaani/api/authenticated_user_provider.dart';
import 'package:vaani/api/authenticated_users_provider.dart';
import 'package:vaani/db/storage.dart';
import 'package:vaani/settings/api_settings_provider.dart';
import 'package:vaani/settings/models/audiobookshelf_server.dart'
as model;
import 'package:vaani/settings/models/audiobookshelf_server.dart' as model;
import 'package:vaani/shared/extensions/obfuscation.dart';
part 'server_provider.g.dart';
final _box = AvailableHiveBoxes.serverBox;
final _logger = Logger('AudiobookShelfServerProvider');
class ServerAlreadyExistsException implements Exception {
final model.AudiobookShelfServer server;
@ -27,7 +28,7 @@ class ServerAlreadyExistsException implements Exception {
class AudiobookShelfServer extends _$AudiobookShelfServer {
@override
Set<model.AudiobookShelfServer> build() {
ref.listenSelf((_, __) {
listenSelf((_, __) {
writeStateToBox();
});
// get the app settings
@ -47,10 +48,10 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
Set<model.AudiobookShelfServer> readFromBoxOrCreate() {
if (_box.isNotEmpty) {
final foundServers = _box.getRange(0, _box.length);
debugPrint('found servers in box: $foundServers');
return foundServers.whereNotNull().toSet();
_logger.info('found servers in box: ${foundServers.obfuscate()}');
return foundServers.nonNulls.toSet();
} else {
debugPrint('no settings found in box');
_logger.info('no settings found in box');
return {};
}
}
@ -61,7 +62,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
return;
}
_box.addAll(state);
debugPrint('writing state to box: $state');
_logger.info('writing state to box: ${state.obfuscate()}');
}
void addServer(model.AudiobookShelfServer server) {
@ -71,23 +72,21 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
state = {...state, server};
}
void removeServer(model.AudiobookShelfServer server,
{
void removeServer(
model.AudiobookShelfServer server, {
bool removeUsers = false,
}) {
state = state.where((s) => s != server).toSet();
// remove the server from the active server
final apiSettings = ref.read(apiSettingsProvider);
if (apiSettings.activeServer == server) {
ref.read(apiSettingsProvider.notifier).updateState(
apiSettings.copyWith(
activeServer: null,
),
);
ref
.read(apiSettingsProvider.notifier)
.updateState(apiSettings.copyWith(activeServer: null));
}
// remove the users of this server
if (removeUsers) {
ref.read(authenticatedUserProvider.notifier).removeUsersOfServer(server);
ref.read(authenticatedUsersProvider.notifier).removeUsersOfServer(server);
}
}

View file

@ -6,25 +6,78 @@ part of 'server_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$audiobookShelfServerHash() =>
r'f0d645bb42233c59886bc43fdc473897484ceca1';
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// provides with a set of servers added by the user
@ProviderFor(AudiobookShelfServer)
final audiobookShelfServerProvider = AudiobookShelfServerProvider._();
/// provides with a set of servers added by the user
///
/// Copied from [AudiobookShelfServer].
@ProviderFor(AudiobookShelfServer)
final audiobookShelfServerProvider = AutoDisposeNotifierProvider<
AudiobookShelfServer, Set<model.AudiobookShelfServer>>.internal(
AudiobookShelfServer.new,
name: r'audiobookShelfServerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$audiobookShelfServerHash,
dependencies: null,
allTransitiveDependencies: null,
);
final class AudiobookShelfServerProvider
extends
$NotifierProvider<
AudiobookShelfServer,
Set<model.AudiobookShelfServer>
> {
/// provides with a set of servers added by the user
AudiobookShelfServerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'audiobookShelfServerProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
typedef _$AudiobookShelfServer
= AutoDisposeNotifier<Set<model.AudiobookShelfServer>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@override
String debugGetCreateSourceHash() => _$audiobookShelfServerHash();
@$internal
@override
AudiobookShelfServer create() => AudiobookShelfServer();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Set<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 narratorName = 'narrator_name_';
static const String libraryItemPlayButton = 'library_item_play_button_';
}

View file

@ -1,5 +1,5 @@
import 'package:flutter/foundation.dart' show immutable;
import 'package:hive/hive.dart';
import 'package:hive_plus_secure/hive_plus_secure.dart';
import 'package:vaani/features/per_book_settings/models/book_settings.dart';
import 'package:vaani/settings/models/models.dart';
@ -14,14 +14,17 @@ class AvailableHiveBoxes {
static final apiSettingsBox = Hive.box<ApiSettings>(name: 'apiSettings');
/// stores the a list of [AudiobookShelfServer]
static final serverBox =
Hive.box<AudiobookShelfServer>(name: 'audiobookShelfServer');
static final serverBox = Hive.box<AudiobookShelfServer>(
name: 'audiobookShelfServer',
);
/// stores the a list of [AuthenticatedUser]
static final authenticatedUserBox =
Hive.box<AuthenticatedUser>(name: 'authenticatedUser');
static final authenticatedUserBox = Hive.box<AuthenticatedUser>(
name: 'authenticatedUser',
);
/// stores the a list of [BookSettings]
static final individualBookSettingsBox =
Hive.box<BookSettings>(name: 'bookSettings');
static final individualBookSettingsBox = Hive.box<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
///
/// stores 2 paths, one is thumbnail and the other is the full size image
/// both are optional
/// also stores last fetched date for the image
/// Id is passed as a parameter to the collection annotation (the lib_item_id)
/// also index the id
/// This is because the image is a part of the library item and the library item
/// is the parent of the image
@Collection(ignore: {'path'})
@Name('CacheImage')
class Image {
@Id()
int id;
// /// Represents a cover image for a library item
// ///
// /// stores 2 paths, one is thumbnail and the other is the full size image
// /// both are optional
// /// also stores last fetched date for the image
// /// Id is passed as a parameter to the collection annotation (the lib_item_id)
// /// also index the id
// /// This is because the image is a part of the library item and the library item
// /// is the parent of the image
// @Collection(ignore: {'path'})
// @Name('CacheImage')
// class Image {
// @Id()
// int id;
String? thumbnailPath;
String? imagePath;
DateTime lastSaved;
// String? thumbnailPath;
// String? imagePath;
// DateTime lastSaved;
Image({
required this.id,
this.thumbnailPath,
this.imagePath,
}) : lastSaved = DateTime.now();
// Image({
// required this.id,
// this.thumbnailPath,
// this.imagePath,
// }) : lastSaved = DateTime.now();
/// returns the path to the image
String? get path => thumbnailPath ?? imagePath;
// /// returns the path to the image
// String? get path => thumbnailPath ?? imagePath;
/// automatically updates the last fetched date when saving a new path
void updatePath(String? thumbnailPath, String? imagePath) async {
this.thumbnailPath = thumbnailPath;
this.imagePath = imagePath;
lastSaved = DateTime.now();
}
}
// /// automatically updates the last fetched date when saving a new path
// void updatePath(String? thumbnailPath, String? imagePath) async {
// this.thumbnailPath = thumbnailPath;
// this.imagePath = imagePath;
// lastSaved = DateTime.now();
// }
// }

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

View file

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

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/settings/models/models.dart';

View file

@ -1,5 +1,6 @@
// download manager to handle download tasks of files
import 'dart:async';
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
@ -7,6 +8,7 @@ import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/extensions/obfuscation.dart';
final _logger = Logger('AudiobookDownloadManager');
final tq = MemoryTaskQueue();
@ -34,10 +36,13 @@ class AudiobookDownloadManager {
FileDownloader().addTaskQueue(tq);
_logger.fine('initialized with baseUrl: $baseUrl, token: $token');
_logger.fine(
'initialized with baseUrl: ${Uri.parse(baseUrl).obfuscate()} and token: ${token.obfuscate()}',
);
_logger.fine(
'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause',
);
initDownloadManager();
}
// the base url for the audio files
@ -55,17 +60,20 @@ class AudiobookDownloadManager {
// whether to allow pausing of downloads
final bool allowPause;
// final List<DownloadTask> _downloadTasks = [];
final StreamController<TaskUpdate> _taskStatusController =
StreamController.broadcast();
Stream<TaskUpdate> get taskUpdateStream => _taskStatusController.stream;
late StreamSubscription<TaskUpdate> _updatesSubscription;
Future<void> queueAudioBookDownload(LibraryItemExpanded item) async {
_logger.info('queuing download for item: ${item.toJson()}');
_logger.info('queuing download for item: ${item.id}');
// create a download task for each file in the item
final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) {
// check if the file is already downloaded
if (isFileDownloaded(
constructFilePath(directory, item, file),
)) {
if (isFileDownloaded(constructFilePath(directory, item, file))) {
_logger.info('file already downloaded: ${file.metadata.filename}');
continue;
}
@ -80,61 +88,56 @@ class AudiobookDownloadManager {
allowPause: allowPause,
group: item.id,
baseDirectory: downloadDirectory,
updates: Updates.statusAndProgress,
// metaData: token
);
// _downloadTasks.add(task);
tq.add(task);
_logger.info('queued task: ${task.toJson()}');
_logger.info('queued task: ${task.taskId}');
}
FileDownloader().registerCallbacks(
group: item.id,
taskProgressCallback: (update) {
_logger.info('Group: ${item.id}, Progress Update: ${update.progress}');
},
taskStatusCallback: (update) {
switch (update.status) {
case TaskStatus.complete:
_logger.info('Group: ${item.id}, Download Complete');
break;
case TaskStatus.failed:
_logger.warning('Group: ${item.id}, Download Failed');
break;
default:
_logger
.info('Group: ${item.id}, Download Status: ${update.status}');
}
},
taskNotificationTapCallback: (task, notificationType) {
_logger.info('Group: ${item.id}, Task: ${task.toJson()}');
},
);
}
String constructFilePath(
Directory directory,
LibraryItemExpanded item,
LibraryFile file,
) =>
'${directory.path}/${item.relPath}/${file.metadata.filename}';
// void startDownload() {
// for (final task in _downloadTasks) {
// _logger.fine('enqueuing task: $task');
// FileDownloader().enqueue(task);
// }
// }
) => '${directory.path}/${item.relPath}/${file.metadata.filename}';
void dispose() {
// tq.removeAll();
_logger.fine('disposed');
_updatesSubscription.cancel();
FileDownloader().destroy();
_logger.fine('Destroyed download manager');
}
bool isItemDownloading(String id) {
return tq.enqueued.any((task) => task.group == id);
}
bool isFileDownloaded(String filePath) {
// Check if the file exists
final fileExists = File(filePath).existsSync();
return File(filePath).existsSync();
}
return fileExists;
Future<List<LibraryFile>> getDownloadedFilesMetadata(
LibraryItemExpanded item,
) async {
final directory = await getApplicationSupportDirectory();
final downloadedFiles = <LibraryFile>[];
for (final file in item.libraryFiles) {
final filePath = constructFilePath(directory, item, file);
if (isFileDownloaded(filePath)) {
downloadedFiles.add(file);
}
}
return downloadedFiles;
}
Future<int> getDownloadedSize(LibraryItemExpanded item) async {
final files = await getDownloadedFilesMetadata(item);
return files.fold<int>(
0,
(previousValue, element) => previousValue + element.metadata.size,
);
}
Future<bool> isItemDownloaded(LibraryItemExpanded item) async {
@ -145,11 +148,12 @@ class AudiobookDownloadManager {
return false;
}
}
_logger.info('all files downloaded for item id: ${item.id}');
return true;
}
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
_logger.info('deleting downloaded item with id: ${item.id}');
final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) {
final filePath = constructFilePath(directory, item, file);
@ -160,7 +164,7 @@ class AudiobookDownloadManager {
}
}
Future<List<Uri>> getDownloadedFiles(LibraryItemExpanded item) async {
Future<List<Uri>> getDownloadedFilesUri(LibraryItemExpanded item) async {
final directory = await getApplicationSupportDirectory();
final files = <Uri>[];
for (final file in item.libraryFiles) {
@ -172,14 +176,28 @@ class AudiobookDownloadManager {
return files;
}
}
Future<void> initDownloadManager() async {
// initialize the download manager
var fileDownloader = FileDownloader();
fileDownloader.configureNotification(
running: const TaskNotification('Downloading', 'file: {filename}'),
progressBar: true,
);
await fileDownloader.trackTasks();
Future<void> initDownloadManager() async {
// initialize the download manager
_logger.info('Initializing download manager');
final fileDownloader = FileDownloader();
_logger.info('Configuring Notification');
fileDownloader.configureNotification(
running: const TaskNotification('Downloading', 'file: {filename}'),
progressBar: true,
);
await fileDownloader.trackTasks();
try {
_updatesSubscription = fileDownloader.updates.listen((event) {
_logger.finer('Got event: $event');
_taskStatusController.add(event);
});
_logger.info('Listening to download manager updates');
} catch (e) {
_logger.warning('Error when listening to download manager updates: $e');
}
}
}

View file

@ -1,13 +1,18 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.dart';
import 'package:vaani/features/downloads/core/download_manager.dart'
as core;
import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/features/downloads/core/download_manager.dart' as core;
import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/item_files.dart';
part 'download_manager.g.dart';
final _logger = Logger('AudiobookDownloadManagerProvider');
@Riverpod(keepAlive: true)
class SimpleDownloadManager extends _$SimpleDownloadManager {
@override
@ -25,27 +30,107 @@ class SimpleDownloadManager extends _$SimpleDownloadManager {
core.tq.maxConcurrent = downloadSettings.maxConcurrent;
core.tq.maxConcurrentByHost = downloadSettings.maxConcurrentByHost;
core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup;
ref.onDispose(() {
_logger.info('disposing download manager');
manager.dispose();
});
_logger.config('initialized download manager');
return manager;
}
}
@Riverpod(keepAlive: true)
class DownloadManager extends _$DownloadManager {
@override
core.AudiobookDownloadManager build() {
final manager = ref.watch(simpleDownloadManagerProvider);
manager.taskUpdateStream.listen((_) {
ref.notifyListeners();
});
return manager;
}
Future<void> queueAudioBookDownload(LibraryItemExpanded item) async {
_logger.fine('queueing download for ${item.id}');
await state.queueAudioBookDownload(item);
}
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
_logger.fine('deleting downloaded item ${item.id}');
await state.deleteDownloadedItem(item);
ref.notifyListeners();
}
}
@riverpod
FutureOr<List<TaskRecord>> downloadHistory(
DownloadHistoryRef ref, {
String? group,
}) async {
class IsItemDownloading extends _$IsItemDownloading {
@override
bool build(String id) {
final manager = ref.watch(downloadManagerProvider);
return manager.isItemDownloading(id);
}
}
@riverpod
class ItemDownloadProgress extends _$ItemDownloadProgress {
@override
Future<double?> build(String id) async {
final item = await ref.watch(libraryItemProvider(id).future);
final manager = ref.read(downloadManagerProvider);
manager.taskUpdateStream
.map((taskUpdate) {
if (taskUpdate is! TaskProgressUpdate) {
return null;
}
if (taskUpdate.task.group == id) {
return taskUpdate;
}
})
.listen((task) async {
if (task != null) {
final totalSize = item.totalSize;
// if total size is 0, return 0
if (totalSize == 0) {
state = const AsyncValue.data(0.0);
return;
}
final downloadedFiles = await manager.getDownloadedFilesMetadata(
item,
);
// calculate total size of downloaded files and total size of item, then divide
// to get percentage
final downloadedSize = downloadedFiles.fold<int>(
0,
(previousValue, element) => previousValue + element.metadata.size,
);
final inProgressFileSize = task.progress * task.expectedFileSize;
final totalDownloadedSize = downloadedSize + inProgressFileSize;
final progress = totalDownloadedSize / totalSize;
// if current progress is more than calculated progress, do not update
if (progress < (state.value ?? 0.0)) {
return;
}
state = AsyncValue.data(progress.clamp(0.0, 1.0));
}
});
return null;
}
}
@riverpod
FutureOr<List<TaskRecord>> downloadHistory(Ref ref, {String? group}) async {
return await FileDownloader().database.allRecords(group: group);
}
@Riverpod(keepAlive: false)
FutureOr<bool> downloadStatus(
DownloadStatusRef ref,
LibraryItemExpanded item,
) async {
final manager = ref.read(simpleDownloadManagerProvider);
return manager.isItemDownloaded(item);
@riverpod
class IsItemDownloaded extends _$IsItemDownloaded {
@override
FutureOr<bool> build(LibraryItemExpanded item) {
final manager = ref.watch(downloadManagerProvider);
return manager.isItemDownloaded(item);
}
}

View file

@ -6,301 +6,480 @@ part of 'download_manager.dart';
// RiverpodGenerator
// **************************************************************************
String _$downloadHistoryHash() => r'76c449e8abfa61d57566991686f534a06dc7fef7';
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
@ProviderFor(SimpleDownloadManager)
final simpleDownloadManagerProvider = SimpleDownloadManagerProvider._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [downloadHistory].
@ProviderFor(downloadHistory)
const downloadHistoryProvider = DownloadHistoryFamily();
/// See also [downloadHistory].
class DownloadHistoryFamily extends Family<AsyncValue<List<TaskRecord>>> {
/// See also [downloadHistory].
const DownloadHistoryFamily();
/// See also [downloadHistory].
DownloadHistoryProvider call({
String? group,
}) {
return DownloadHistoryProvider(
group: group,
);
}
@override
DownloadHistoryProvider getProviderOverride(
covariant DownloadHistoryProvider provider,
) {
return call(
group: provider.group,
);
}
static const Iterable<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'downloadHistoryProvider';
}
/// See also [downloadHistory].
class DownloadHistoryProvider
extends AutoDisposeFutureProvider<List<TaskRecord>> {
/// See also [downloadHistory].
DownloadHistoryProvider({
String? group,
}) : this._internal(
(ref) => downloadHistory(
ref as DownloadHistoryRef,
group: group,
),
from: downloadHistoryProvider,
name: r'downloadHistoryProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$downloadHistoryHash,
dependencies: DownloadHistoryFamily._dependencies,
allTransitiveDependencies:
DownloadHistoryFamily._allTransitiveDependencies,
group: group,
);
DownloadHistoryProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.group,
}) : super.internal();
final String? group;
@override
Override overrideWith(
FutureOr<List<TaskRecord>> Function(DownloadHistoryRef provider) create,
) {
return ProviderOverride(
origin: this,
override: DownloadHistoryProvider._internal(
(ref) => create(ref as DownloadHistoryRef),
from: from,
name: null,
final class SimpleDownloadManagerProvider
extends
$NotifierProvider<
SimpleDownloadManager,
core.AudiobookDownloadManager
> {
SimpleDownloadManagerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'simpleDownloadManagerProvider',
isAutoDispose: false,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
group: group,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$simpleDownloadManagerHash();
@$internal
@override
SimpleDownloadManager create() => SimpleDownloadManager();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(core.AudiobookDownloadManager value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<core.AudiobookDownloadManager>(
value,
),
);
}
@override
AutoDisposeFutureProviderElement<List<TaskRecord>> createElement() {
return _DownloadHistoryProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DownloadHistoryProvider && other.group == group;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, group.hashCode);
return _SystemHash.finish(hash);
}
}
mixin DownloadHistoryRef on AutoDisposeFutureProviderRef<List<TaskRecord>> {
/// The parameter `group` of this provider.
String? get group;
}
class _DownloadHistoryProviderElement
extends AutoDisposeFutureProviderElement<List<TaskRecord>>
with DownloadHistoryRef {
_DownloadHistoryProviderElement(super.provider);
@override
String? get group => (origin as DownloadHistoryProvider).group;
}
String _$downloadStatusHash() => r'f37b4678d3c2a7c6e985b0149d72ea0f9b1b42ca';
/// See also [downloadStatus].
@ProviderFor(downloadStatus)
const downloadStatusProvider = DownloadStatusFamily();
/// See also [downloadStatus].
class DownloadStatusFamily extends Family<AsyncValue<bool>> {
/// See also [downloadStatus].
const DownloadStatusFamily();
/// See also [downloadStatus].
DownloadStatusProvider call(
LibraryItemExpanded item,
) {
return DownloadStatusProvider(
item,
);
}
@override
DownloadStatusProvider getProviderOverride(
covariant DownloadStatusProvider provider,
) {
return call(
provider.item,
);
}
static const Iterable<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'downloadStatusProvider';
}
/// See also [downloadStatus].
class DownloadStatusProvider extends AutoDisposeFutureProvider<bool> {
/// See also [downloadStatus].
DownloadStatusProvider(
LibraryItemExpanded item,
) : this._internal(
(ref) => downloadStatus(
ref as DownloadStatusRef,
item,
),
from: downloadStatusProvider,
name: r'downloadStatusProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$downloadStatusHash,
dependencies: DownloadStatusFamily._dependencies,
allTransitiveDependencies:
DownloadStatusFamily._allTransitiveDependencies,
item: item,
);
DownloadStatusProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.item,
}) : super.internal();
final LibraryItemExpanded item;
@override
Override overrideWith(
FutureOr<bool> Function(DownloadStatusRef provider) create,
) {
return ProviderOverride(
origin: this,
override: DownloadStatusProvider._internal(
(ref) => create(ref as DownloadStatusRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
item: item,
),
);
}
@override
AutoDisposeFutureProviderElement<bool> createElement() {
return _DownloadStatusProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DownloadStatusProvider && other.item == item;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, item.hashCode);
return _SystemHash.finish(hash);
}
}
mixin DownloadStatusRef on AutoDisposeFutureProviderRef<bool> {
/// The parameter `item` of this provider.
LibraryItemExpanded get item;
}
class _DownloadStatusProviderElement
extends AutoDisposeFutureProviderElement<bool> with DownloadStatusRef {
_DownloadStatusProviderElement(super.provider);
@override
LibraryItemExpanded get item => (origin as DownloadStatusProvider).item;
}
String _$simpleDownloadManagerHash() =>
r'cec95717c86e422f88f78aa014d29e800e5a2089';
r'8ab13f06ec5f2f73b73064bd285813dc890b7f36';
/// See also [SimpleDownloadManager].
@ProviderFor(SimpleDownloadManager)
final simpleDownloadManagerProvider = NotifierProvider<SimpleDownloadManager,
core.AudiobookDownloadManager>.internal(
SimpleDownloadManager.new,
name: r'simpleDownloadManagerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$simpleDownloadManagerHash,
dependencies: null,
allTransitiveDependencies: null,
);
abstract class _$SimpleDownloadManager
extends $Notifier<core.AudiobookDownloadManager> {
core.AudiobookDownloadManager build();
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<
core.AudiobookDownloadManager,
core.AudiobookDownloadManager
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
core.AudiobookDownloadManager,
core.AudiobookDownloadManager
>,
core.AudiobookDownloadManager,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@ProviderFor(DownloadManager)
final downloadManagerProvider = DownloadManagerProvider._();
final class DownloadManagerProvider
extends $NotifierProvider<DownloadManager, core.AudiobookDownloadManager> {
DownloadManagerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'downloadManagerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$downloadManagerHash();
@$internal
@override
DownloadManager create() => DownloadManager();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(core.AudiobookDownloadManager value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<core.AudiobookDownloadManager>(
value,
),
);
}
}
String _$downloadManagerHash() => r'852012e32e613f86445afc7f7e4e85bec808e982';
abstract class _$DownloadManager
extends $Notifier<core.AudiobookDownloadManager> {
core.AudiobookDownloadManager build();
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<
core.AudiobookDownloadManager,
core.AudiobookDownloadManager
>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
core.AudiobookDownloadManager,
core.AudiobookDownloadManager
>,
core.AudiobookDownloadManager,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(IsItemDownloading)
final isItemDownloadingProvider = IsItemDownloadingFamily._();
final class IsItemDownloadingProvider
extends $NotifierProvider<IsItemDownloading, bool> {
IsItemDownloadingProvider._({
required IsItemDownloadingFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'isItemDownloadingProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$isItemDownloadingHash();
@override
String toString() {
return r'isItemDownloadingProvider'
''
'($argument)';
}
@$internal
@override
IsItemDownloading create() => IsItemDownloading();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
@override
bool operator ==(Object other) {
return other is IsItemDownloadingProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$isItemDownloadingHash() => r'ea43c06393beec828134e08d5f896ddbcfbac8f0';
final class IsItemDownloadingFamily extends $Family
with $ClassFamilyOverride<IsItemDownloading, bool, bool, bool, String> {
IsItemDownloadingFamily._()
: super(
retry: null,
name: r'isItemDownloadingProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
IsItemDownloadingProvider call(String id) =>
IsItemDownloadingProvider._(argument: id, from: this);
@override
String toString() => r'isItemDownloadingProvider';
}
abstract class _$IsItemDownloading extends $Notifier<bool> {
late final _$args = ref.$arg as String;
String get id => _$args;
bool build(String id);
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}
@ProviderFor(ItemDownloadProgress)
final itemDownloadProgressProvider = ItemDownloadProgressFamily._();
final class ItemDownloadProgressProvider
extends $AsyncNotifierProvider<ItemDownloadProgress, double?> {
ItemDownloadProgressProvider._({
required ItemDownloadProgressFamily super.from,
required String super.argument,
}) : super(
retry: null,
name: r'itemDownloadProgressProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$itemDownloadProgressHash();
@override
String toString() {
return r'itemDownloadProgressProvider'
''
'($argument)';
}
@$internal
@override
ItemDownloadProgress create() => ItemDownloadProgress();
@override
bool operator ==(Object other) {
return other is ItemDownloadProgressProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$itemDownloadProgressHash() =>
r'95f6ec0945f73d9156bf89bdb1865f3b2c9ffcaa';
final class ItemDownloadProgressFamily extends $Family
with
$ClassFamilyOverride<
ItemDownloadProgress,
AsyncValue<double?>,
double?,
FutureOr<double?>,
String
> {
ItemDownloadProgressFamily._()
: super(
retry: null,
name: r'itemDownloadProgressProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
ItemDownloadProgressProvider call(String id) =>
ItemDownloadProgressProvider._(argument: id, from: this);
@override
String toString() => r'itemDownloadProgressProvider';
}
abstract class _$ItemDownloadProgress extends $AsyncNotifier<double?> {
late final _$args = ref.$arg as String;
String get id => _$args;
FutureOr<double?> build(String id);
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<double?>, double?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<double?>, double?>,
AsyncValue<double?>,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}
@ProviderFor(downloadHistory)
final downloadHistoryProvider = DownloadHistoryFamily._();
final class DownloadHistoryProvider
extends
$FunctionalProvider<
AsyncValue<List<TaskRecord>>,
List<TaskRecord>,
FutureOr<List<TaskRecord>>
>
with $FutureModifier<List<TaskRecord>>, $FutureProvider<List<TaskRecord>> {
DownloadHistoryProvider._({
required DownloadHistoryFamily super.from,
required String? super.argument,
}) : super(
retry: null,
name: r'downloadHistoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$downloadHistoryHash();
@override
String toString() {
return r'downloadHistoryProvider'
''
'($argument)';
}
@$internal
@override
$FutureProviderElement<List<TaskRecord>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<TaskRecord>> create(Ref ref) {
final argument = this.argument as String?;
return downloadHistory(ref, group: argument);
}
@override
bool operator ==(Object other) {
return other is DownloadHistoryProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$downloadHistoryHash() => r'4d8b84e30f7ff5ae69d23c8e03ff24af1234a1ad';
final class DownloadHistoryFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<List<TaskRecord>>, String?> {
DownloadHistoryFamily._()
: super(
retry: null,
name: r'downloadHistoryProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
DownloadHistoryProvider call({String? group}) =>
DownloadHistoryProvider._(argument: group, from: this);
@override
String toString() => r'downloadHistoryProvider';
}
@ProviderFor(IsItemDownloaded)
final isItemDownloadedProvider = IsItemDownloadedFamily._();
final class IsItemDownloadedProvider
extends $AsyncNotifierProvider<IsItemDownloaded, bool> {
IsItemDownloadedProvider._({
required IsItemDownloadedFamily super.from,
required LibraryItemExpanded super.argument,
}) : super(
retry: null,
name: r'isItemDownloadedProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$isItemDownloadedHash();
@override
String toString() {
return r'isItemDownloadedProvider'
''
'($argument)';
}
@$internal
@override
IsItemDownloaded create() => IsItemDownloaded();
@override
bool operator ==(Object other) {
return other is IsItemDownloadedProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$isItemDownloadedHash() => r'9bb7ba28bdb73e1ba706e849fedc9c7bd67f4b67';
final class IsItemDownloadedFamily extends $Family
with
$ClassFamilyOverride<
IsItemDownloaded,
AsyncValue<bool>,
bool,
FutureOr<bool>,
LibraryItemExpanded
> {
IsItemDownloadedFamily._()
: super(
retry: null,
name: r'isItemDownloadedProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
IsItemDownloadedProvider call(LibraryItemExpanded item) =>
IsItemDownloadedProvider._(argument: item, from: this);
@override
String toString() => r'isItemDownloadedProvider';
}
abstract class _$IsItemDownloaded extends $AsyncNotifier<bool> {
late final _$args = ref.$arg as LibraryItemExpanded;
LibraryItemExpanded get item => _$args;
FutureOr<bool> build(LibraryItemExpanded item);
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<bool>, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<bool>, bool>,
AsyncValue<bool>,
Object?,
Object?
>;
element.handleCreate(ref, () => build(_$args));
}
}

View file

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

View file

@ -6,24 +6,64 @@ part of 'search_controller.dart';
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// The controller for the search bar.
@ProviderFor(GlobalSearchController)
final globalSearchControllerProvider = GlobalSearchControllerProvider._();
/// The controller for the search bar.
final class GlobalSearchControllerProvider
extends $NotifierProvider<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() =>
r'd854ace6f2e00a10fc33aba63051375f82ad1b10';
/// 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>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
abstract class _$GlobalSearchController
extends $Notifier<Raw<SearchController>> {
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:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.dart';
@ -8,7 +9,7 @@ part 'search_result_provider.g.dart';
/// The provider for the search result.
@riverpod
FutureOr<LibrarySearchResponse?> searchResult(
SearchResultRef ref,
Ref ref,
String query, {
int limit = 25,
}) async {

View file

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

View file

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

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/shared/extensions/model_conversions.dart';
enum SearchResultCategory {
books,
authors,
series,
tags,
narrators,
}
enum SearchResultCategory { books, authors, series, tags, narrators }
class SearchResultPage extends HookConsumerWidget {
const SearchResultPage({
@ -41,9 +35,7 @@ class SearchResultPage extends HookConsumerWidget {
body: results.when(
data: (options) {
if (options == null) {
return Container(
child: const Text('No data found'),
);
return Container(child: const Text('No data found'));
}
if (options is BookLibrarySearchResponse) {
if (category == null) {
@ -51,18 +43,15 @@ class SearchResultPage extends HookConsumerWidget {
}
return switch (category!) {
SearchResultCategory.books => ListView.builder(
itemCount: options.book.length,
itemBuilder: (context, index) {
final book =
options.book[index].libraryItem.media.asBookExpanded;
final metadata = book.metadata.asBookMetadataExpanded;
itemCount: options.book.length,
itemBuilder: (context, index) {
final book =
options.book[index].libraryItem.media.asBookExpanded;
final metadata = book.metadata.asBookMetadataExpanded;
return BookSearchResultMini(
book: book,
metadata: metadata,
);
},
),
return BookSearchResultMini(book: book, metadata: metadata);
},
),
SearchResultCategory.authors => Container(),
SearchResultCategory.series => Container(),
SearchResultCategory.tags => Container(),
@ -71,12 +60,8 @@ class SearchResultPage extends HookConsumerWidget {
}
return null;
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(
child: Text('Error: $error'),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stackTrace) => Center(child: Text('Error: $error')),
),
);
}

View file

@ -1,5 +1,6 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
@ -8,7 +9,10 @@ import 'package:vaani/constants/hero_tag_conventions.dart';
import 'package:vaani/features/downloads/providers/download_manager.dart'
show
downloadHistoryProvider,
downloadStatusProvider,
downloadManagerProvider,
isItemDownloadedProvider,
isItemDownloadingProvider,
itemDownloadProgressProvider,
simpleDownloadManagerProvider;
import 'package:vaani/features/item_viewer/view/library_item_page.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
@ -22,21 +26,17 @@ import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/utils.dart';
class LibraryItemActions extends HookConsumerWidget {
LibraryItemActions({
super.key,
required this.item,
}) {
book = item.media.asBookExpanded;
}
const LibraryItemActions({super.key, required this.id});
final String id;
final shelfsdk.LibraryItemExpanded item;
late final shelfsdk.BookExpanded book;
@override
Widget build(BuildContext context, WidgetRef ref) {
final manager = ref.read(simpleDownloadManagerProvider);
final item = ref.watch(libraryItemProvider(id)).value;
if (item == null) {
return const SizedBox.shrink();
}
final downloadHistory = ref.watch(downloadHistoryProvider(group: item.id));
final isItemDownloaded = ref.watch(downloadStatusProvider(item));
final isBookPlaying = ref.watch(audiobookPlayerProvider).book != null;
final apiSettings = ref.watch(apiSettingsProvider);
return Padding(
@ -65,9 +65,7 @@ class LibraryItemActions extends HookConsumerWidget {
// read list button
IconButton(
onPressed: () {},
icon: const Icon(
Icons.playlist_add_rounded,
),
icon: const Icon(Icons.playlist_add_rounded),
),
// share button
IconButton(
@ -76,8 +74,9 @@ class LibraryItemActions extends HookConsumerWidget {
var currentServerUrl =
apiSettings.activeServer!.serverUrl;
if (!currentServerUrl.hasScheme) {
currentServerUrl =
Uri.https(currentServerUrl.toString());
currentServerUrl = Uri.https(
currentServerUrl.toString(),
);
}
handleLaunchUrl(
Uri.parse(
@ -93,64 +92,9 @@ class LibraryItemActions extends HookConsumerWidget {
},
icon: const Icon(Icons.share_rounded),
),
// check if the book is downloaded using manager.isItemDownloaded
isItemDownloaded.when(
data: (isDownloaded) {
if (isDownloaded) {
// already downloaded button
return IconButton(
onPressed: () {
appLogger
.fine('Pressed already downloaded button');
// manager.openDownloadedFile(item);
// open popup menu to open or delete the file
showModalBottomSheet(
useRootNavigator: false,
context: context,
builder: (context) {
return Padding(
padding: EdgeInsets.only(
top: 8.0,
bottom: (isBookPlaying
? playerMinHeight
: 0) +
8,
),
child: DownloadSheet(
item: item,
),
);
},
);
},
icon: const Icon(
Icons.download_done_rounded,
),
);
}
// download button
return IconButton(
onPressed: () {
appLogger.fine('Pressed download button');
manager.queueAudioBookDownload(item);
},
icon: const Icon(
Icons.download_rounded,
),
);
},
loading: () => const CircularProgressIndicator(),
error: (error, stackTrace) {
return IconButton(
onPressed: () {
appLogger.warning(
'Error checking download status: $error',
);
},
icon: const Icon(Icons.error_rounded),
);
},
),
// download button
LibItemDownloadButton(item: item),
// more button
IconButton(
onPressed: () {
@ -192,7 +136,8 @@ class LibraryItemActions extends HookConsumerWidget {
.database
.deleteRecordWithId(
record
.task.taskId,
.task
.taskId,
);
Navigator.pop(context);
},
@ -213,8 +158,8 @@ class LibraryItemActions extends HookConsumerWidget {
// open the file location
final didOpen =
await FileDownloader().openFile(
task: record.task,
);
task: record.task,
);
if (!didOpen) {
appLogger.warning(
@ -234,16 +179,13 @@ class LibraryItemActions extends HookConsumerWidget {
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(
child: Text('Error: $error'),
),
error: (error, stackTrace) =>
Center(child: Text('Error: $error')),
);
},
);
},
icon: const Icon(
Icons.more_vert_rounded,
),
icon: const Icon(Icons.more_vert_rounded),
),
],
),
@ -257,17 +199,118 @@ class LibraryItemActions extends HookConsumerWidget {
}
}
class DownloadSheet extends HookConsumerWidget {
const DownloadSheet({
super.key,
required this.item,
});
class LibItemDownloadButton extends HookConsumerWidget {
const LibItemDownloadButton({super.key, required this.item});
final shelfsdk.LibraryItemExpanded item;
@override
Widget build(BuildContext context, WidgetRef ref) {
final manager = ref.read(simpleDownloadManagerProvider);
final isItemDownloaded = ref.watch(isItemDownloadedProvider(item));
if (isItemDownloaded.value ?? false) {
return AlreadyItemDownloadedButton(item: item);
}
final isItemDownloading = ref.watch(isItemDownloadingProvider(item.id));
return isItemDownloading
? ItemCurrentlyInDownloadQueue(item: item)
: IconButton(
onPressed: () {
appLogger.fine('Pressed download button');
ref
.read(downloadManagerProvider.notifier)
.queueAudioBookDownload(item);
},
icon: const Icon(Icons.download_rounded),
);
}
}
class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
const ItemCurrentlyInDownloadQueue({super.key, required this.item});
final shelfsdk.LibraryItemExpanded item;
@override
Widget build(BuildContext context, WidgetRef ref) {
final progress = ref
.watch(itemDownloadProgressProvider(item.id))
.value
?.clamp(0.05, 1.0);
if (progress == 1) {
return AlreadyItemDownloadedButton(item: item);
}
const shimmerDuration = Duration(milliseconds: 1000);
return Stack(
alignment: Alignment.center,
children: [
CircularProgressIndicator(value: progress, strokeWidth: 2),
const Icon(
Icons.download,
// color: Theme.of(context).progressIndicatorTheme.color,
)
.animate(onPlay: (controller) => controller.repeat())
.fade(
duration: shimmerDuration,
end: 1,
begin: 0.6,
curve: Curves.linearToEaseOut,
)
.fade(
duration: shimmerDuration,
end: 0.7,
begin: 1,
curve: Curves.easeInToLinear,
),
],
);
}
}
class AlreadyItemDownloadedButton extends HookConsumerWidget {
const AlreadyItemDownloadedButton({super.key, required this.item});
final shelfsdk.LibraryItemExpanded item;
@override
Widget build(BuildContext context, WidgetRef ref) {
final isBookPlaying = ref.watch(audiobookPlayerProvider).book != null;
return IconButton(
onPressed: () {
appLogger.fine('Pressed already downloaded button');
// manager.openDownloadedFile(item);
// open popup menu to open or delete the file
showModalBottomSheet(
useRootNavigator: false,
context: context,
builder: (context) {
return Padding(
padding: EdgeInsets.only(
top: 8.0,
bottom: (isBookPlaying ? playerMinHeight : 0) + 8,
),
child: DownloadSheet(item: item),
);
},
);
},
icon: const Icon(Icons.download_done_rounded),
);
}
}
class DownloadSheet extends HookConsumerWidget {
const DownloadSheet({super.key, required this.item});
final shelfsdk.LibraryItemExpanded item;
@override
Widget build(BuildContext context, WidgetRef ref) {
final manager = ref.watch(downloadManagerProvider);
return Column(
mainAxisSize: MainAxisSize.min,
@ -293,12 +336,10 @@ class DownloadSheet extends HookConsumerWidget {
// ),
ListTile(
title: const Text('Delete'),
leading: const Icon(
Icons.delete_rounded,
),
onTap: () {
leading: const Icon(Icons.delete_rounded),
onTap: () async {
// show the delete dialog
showDialog(
final wasDeleted = await showDialog<bool>(
useRootNavigator: false,
context: context,
builder: (context) {
@ -311,16 +352,16 @@ class DownloadSheet extends HookConsumerWidget {
TextButton(
onPressed: () {
// delete the file
manager.deleteDownloadedItem(
item,
);
GoRouter.of(context).pop();
ref
.read(downloadManagerProvider.notifier)
.deleteDownloadedItem(item);
GoRouter.of(context).pop(true);
},
child: const Text('Yes'),
),
TextButton(
onPressed: () {
GoRouter.of(context).pop();
GoRouter.of(context).pop(false);
},
child: const Text('No'),
),
@ -328,6 +369,14 @@ class DownloadSheet extends HookConsumerWidget {
);
},
);
if (wasDeleted ?? false) {
appLogger.fine('Deleted ${item.media.metadata.title}');
GoRouter.of(context).pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Deleted ${item.media.metadata.title}')),
);
}
},
),
],
@ -336,10 +385,7 @@ class DownloadSheet extends HookConsumerWidget {
}
class _LibraryItemPlayButton extends HookConsumerWidget {
const _LibraryItemPlayButton({
super.key,
required this.item,
});
const _LibraryItemPlayButton({required this.item});
final shelfsdk.LibraryItemExpanded item;
@ -390,9 +436,7 @@ class _LibraryItemPlayButton extends HookConsumerWidget {
),
label: Text(getPlayDisplayText()),
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
),
);
}
@ -415,11 +459,11 @@ class DynamicItemPlayIcon extends StatelessWidget {
return Icon(
isCurrentBookSetInPlayer
? isPlayingThisBook
? Icons.pause_rounded
: Icons.play_arrow_rounded
? Icons.pause_rounded
: Icons.play_arrow_rounded
: isBookCompleted
? Icons.replay_rounded
: Icons.play_arrow_rounded,
? Icons.replay_rounded
: Icons.play_arrow_rounded,
);
}
}
@ -430,7 +474,7 @@ Future<void> libraryItemPlayButtonOnPressed({
required shelfsdk.BookExpanded book,
shelfsdk.MediaProgress? userMediaProgress,
}) async {
debugPrint('Pressed play/resume button');
appLogger.info('Pressed play/resume button');
final player = ref.watch(audiobookPlayerProvider);
final isCurrentBookSetInPlayer = player.book == book;
@ -439,28 +483,30 @@ Future<void> libraryItemPlayButtonOnPressed({
Future<void>? setSourceFuture;
// set the book to the player if not already set
if (!isCurrentBookSetInPlayer) {
debugPrint('Setting the book ${book.libraryItemId}');
debugPrint('Initial position: ${userMediaProgress?.currentTime}');
appLogger.info('Setting the book ${book.libraryItemId}');
appLogger.info('Initial position: ${userMediaProgress?.currentTime}');
final downloadManager = ref.watch(simpleDownloadManagerProvider);
final libItem =
await ref.read(libraryItemProvider(book.libraryItemId).future);
final downloadedUris = await downloadManager.getDownloadedFiles(libItem);
final libItem = await ref.read(
libraryItemProvider(book.libraryItemId).future,
);
final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem);
setSourceFuture = player.setSourceAudiobook(
book,
initialPosition: userMediaProgress?.currentTime,
downloadedUris: downloadedUris,
);
} else {
debugPrint('Book was already set');
appLogger.info('Book was already set');
if (isPlayingThisBook) {
debugPrint('Pausing the book');
appLogger.info('Pausing the book');
await player.pause();
return;
}
}
// set the volume as this is the first time playing and dismissing causes the volume to go to 0
var bookPlayerSettings =
ref.read(bookSettingsProvider(book.libraryItemId)).playerSettings;
var bookPlayerSettings = ref
.read(bookSettingsProvider(book.libraryItemId))
.playerSettings;
var appPlayerSettings = ref.read(appSettingsProvider).playerSettings;
var configurePlayerForEveryBook =
@ -472,14 +518,14 @@ Future<void> libraryItemPlayButtonOnPressed({
player.setVolume(
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultVolume ??
appPlayerSettings.preferredDefaultVolume
appPlayerSettings.preferredDefaultVolume
: appPlayerSettings.preferredDefaultVolume,
),
// set the speed
player.setSpeed(
configurePlayerForEveryBook
? bookPlayerSettings.preferredDefaultSpeed ??
appPlayerSettings.preferredDefaultSpeed
appPlayerSettings.preferredDefaultSpeed
: appPlayerSettings.preferredDefaultSpeed,
),
]);

View file

@ -5,6 +5,7 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
import 'package:vaani/api/image_provider.dart';
import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/constants/hero_tag_conventions.dart';
import 'package:vaani/features/item_viewer/view/library_item_page.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart';
@ -14,141 +15,128 @@ import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/duration_format.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
import 'package:vaani/theme/providers/theme_from_cover_provider.dart';
class LibraryItemHeroSection extends HookConsumerWidget {
const LibraryItemHeroSection({
super.key,
required this.itemId,
required this.extraMap,
required this.providedCacheImage,
required this.item,
required this.itemBookMetadata,
required this.bookDetailsCached,
required this.coverColorScheme,
});
final String itemId;
final LibraryItemExtras? extraMap;
final Image? providedCacheImage;
final AsyncValue<shelfsdk.LibraryItemExpanded> item;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
final shelfsdk.BookMinified? bookDetailsCached;
final AsyncValue<ColorScheme?> coverColorScheme;
@override
Widget build(BuildContext context, WidgetRef ref) {
return SliverToBoxAdapter(
child: LayoutBuilder(
builder: (context, constraints) {
return Container(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// book cover
LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
width: calculateWidth(context, constraints),
child: Column(
children: [
Hero(
tag: HeroTagPrefixes.bookCover +
itemId +
(extraMap?.heroTagSuffix ?? ''),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: _BookCover(
itemId: itemId,
extraMap: extraMap,
providedCacheImage: providedCacheImage,
coverColorScheme: coverColorScheme.valueOrNull,
item: item,
),
),
),
// a progress bar if available
item.when(
data: (libraryItem) {
return Padding(
padding: const EdgeInsets.only(
top: 8.0,
right: 8.0,
left: 8.0,
),
child: _LibraryItemProgressIndicator(
libraryItem: libraryItem,
),
);
},
loading: () => const SizedBox.shrink(),
error: (error, stack) => const SizedBox.shrink(),
),
],
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
// book cover
LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
width: calculateWidth(context, constraints),
child: Column(
children: [
Hero(
tag:
HeroTagPrefixes.bookCover +
itemId +
(extraMap?.heroTagSuffix ?? ''),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: _BookCover(itemId: itemId),
),
);
},
),
// book details
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
_BookTitle(
extraMap: extraMap,
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
),
Container(
margin: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// authors info if available
_BookAuthors(
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
),
// narrators info if available
_BookNarrators(
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
),
// series info if available
_BookSeries(
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
),
],
),
),
],
),
),
// a progress bar
Padding(
padding: const EdgeInsets.only(
top: 8.0,
right: 8.0,
left: 8.0,
),
child: _LibraryItemProgressIndicator(id: itemId),
),
],
),
],
);
},
),
// book details
_BookDetails(id: itemId, extraMap: extraMap),
],
),
);
}
}
class _BookDetails extends HookConsumerWidget {
const _BookDetails({required this.id, this.extraMap});
final String id;
final LibraryItemExtras? extraMap;
@override
Widget build(BuildContext context, WidgetRef ref) {
final itemFromApi = ref.watch(libraryItemProvider(id));
final itemBookMetadata =
itemFromApi.value?.media.metadata.asBookMetadataExpanded;
return Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
_BookTitle(extraMap: extraMap, itemBookMetadata: itemBookMetadata),
Container(
margin: const EdgeInsets.symmetric(vertical: 16),
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
// authors info if available
_BookAuthors(
itemBookMetadata: itemBookMetadata,
bookDetailsCached: extraMap?.book,
),
// narrators info if available
_BookNarrators(
itemBookMetadata: itemBookMetadata,
bookDetailsCached: extraMap?.book,
),
// series info if available
_BookSeries(
itemBookMetadata: itemBookMetadata,
bookDetailsCached: extraMap?.book,
),
],
),
),
);
},
],
),
),
);
}
}
class _LibraryItemProgressIndicator extends HookConsumerWidget {
const _LibraryItemProgressIndicator({
super.key,
required this.libraryItem,
});
const _LibraryItemProgressIndicator({required this.id});
final shelfsdk.LibraryItemExpanded libraryItem;
final String id;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final libraryItem = ref.watch(libraryItemProvider(id)).value;
if (libraryItem == null) {
return const SizedBox.shrink();
}
final mediaProgress = libraryItem.userMediaProgress;
if (mediaProgress == null && player.book?.libraryItemId != libraryItem.id) {
return const SizedBox.shrink();
@ -158,13 +146,15 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
Duration remainingTime;
if (player.book?.libraryItemId == libraryItem.id) {
// final positionStream = useStream(player.slowPositionStream);
progress = (player.positionInBook).inSeconds /
progress =
(player.positionInBook).inSeconds /
libraryItem.media.asBookExpanded.duration.inSeconds;
remainingTime =
libraryItem.media.asBookExpanded.duration - player.positionInBook;
} else {
progress = mediaProgress?.progress ?? 0;
remainingTime = (libraryItem.media.asBookExpanded.duration -
remainingTime =
(libraryItem.media.asBookExpanded.duration -
mediaProgress!.currentTime);
}
@ -188,19 +178,20 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
LinearProgressIndicator(
value: progress.clamp(0.03, 1),
borderRadius: BorderRadius.circular(8),
semanticsLabel: 'Book progress',
semanticsValue: '${progressInPercent.toStringAsFixed(2)}%',
),
const SizedBox.square(
dimension: 4.0,
),
const SizedBox.square(dimension: 4.0),
// time remaining
Text(
// only show 2 decimal places
'${remainingTime.smartBinaryFormat} left',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color:
Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
),
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.75),
),
),
],
),
@ -209,11 +200,7 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
}
class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
const _HeroSectionSubLabelWithIcon({
super.key,
required this.icon,
required this.text,
});
const _HeroSectionSubLabelWithIcon({required this.icon, required this.text});
final IconData icon;
final Widget text;
@ -223,11 +210,13 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
final themeData = Theme.of(context);
final useFontAwesome =
icon.runtimeType == FontAwesomeIcons.book.runtimeType;
final useMaterialThemeOnItemPage =
ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage;
final useMaterialThemeOnItemPage = ref
.watch(appSettingsProvider)
.themeSettings
.useMaterialThemeOnItemPage;
final color = useMaterialThemeOnItemPage
? themeData.colorScheme.primary
: themeData.colorScheme.onSurface.withOpacity(0.75);
: themeData.colorScheme.onSurface.withValues(alpha: 0.75);
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
@ -235,20 +224,10 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
Container(
margin: const EdgeInsets.only(right: 8, top: 2),
child: useFontAwesome
? FaIcon(
icon,
size: 16,
color: color,
)
: Icon(
icon,
size: 16,
color: color,
),
),
Expanded(
child: text,
? FaIcon(icon, size: 16, color: color)
: Icon(icon, size: 16, color: color),
),
Expanded(child: text),
],
),
);
@ -257,7 +236,6 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
class _BookSeries extends StatelessWidget {
const _BookSeries({
super.key,
required this.itemBookMetadata,
required this.bookDetailsCached,
});
@ -303,7 +281,6 @@ class _BookSeries extends StatelessWidget {
class _BookNarrators extends StatelessWidget {
const _BookNarrators({
super.key,
required this.itemBookMetadata,
required this.bookDetailsCached,
});
@ -338,98 +315,77 @@ class _BookNarrators extends StatelessWidget {
}
class _BookCover extends HookConsumerWidget {
const _BookCover({
super.key,
required this.itemId,
required this.extraMap,
required this.providedCacheImage,
required this.item,
this.coverColorScheme,
});
const _BookCover({required this.itemId});
final String itemId;
final LibraryItemExtras? extraMap;
final Image? providedCacheImage;
final AsyncValue<shelfsdk.LibraryItemExpanded> item;
final ColorScheme? coverColorScheme;
@override
Widget build(BuildContext context, WidgetRef ref) {
final coverImage = ref.watch(coverImageProvider(itemId));
final themeData = Theme.of(context);
final useMaterialThemeOnItemPage =
ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage;
// final item = ref.watch(libraryItemProvider(itemId));
final themeSettings = ref.watch(appSettingsProvider).themeSettings;
ColorScheme? coverColorScheme;
if (themeSettings.useMaterialThemeOnItemPage) {
coverColorScheme = ref
.watch(
themeOfLibraryItemProvider(
itemId,
brightness: Theme.of(context).brightness,
highContrast:
themeSettings.highContrast ||
MediaQuery.of(context).highContrast,
),
)
.value;
}
return ThemeSwitcher(
builder: (context) {
// change theme after 2 seconds
if (useMaterialThemeOnItemPage) {
if (themeSettings.useMaterialThemeOnItemPage) {
Future.delayed(150.ms, () {
try {
ThemeSwitcher.of(context).changeTheme(
theme: coverColorScheme != null
? ThemeData.from(
colorScheme: coverColorScheme!,
colorScheme: coverColorScheme,
textTheme: themeData.textTheme,
)
: themeData,
);
} catch (e) {
appLogger.shout('Error changing theme: $e');
appLogger.severe('Error changing theme: $e');
}
});
}
return providedCacheImage ??
item.when(
data: (libraryItem) {
final coverImage = ref.watch(coverImageProvider(libraryItem));
return Stack(
children: [
coverImage.when(
data: (image) {
// return const BookCoverSkeleton();
if (image.isEmpty) {
return const Icon(Icons.error);
}
// cover 80% of parent height
return Image.memory(
image,
fit: BoxFit.cover,
// cacheWidth: (height *
// MediaQuery.of(context).devicePixelRatio)
// .round(),
);
},
loading: () {
return const Center(
child: BookCoverSkeleton(),
);
},
error: (error, stack) {
return const Icon(Icons.error);
},
),
],
);
},
error: (error, stack) => const Icon(Icons.error),
loading: () => const Center(child: BookCoverSkeleton()),
);
return coverImage.when(
data: (image) {
// return const BookCoverSkeleton();
if (image.isEmpty) {
return const Icon(Icons.error);
}
return Image.memory(image, fit: BoxFit.cover);
},
loading: () {
return const Center(child: BookCoverSkeleton());
},
error: (error, stack) {
return const Center(child: Icon(Icons.error));
},
);
},
);
}
}
class _BookTitle extends StatelessWidget {
const _BookTitle({
super.key,
required this.extraMap,
required this.itemBookMetadata,
required this.bookDetailsCached,
});
const _BookTitle({required this.extraMap, required this.itemBookMetadata});
final LibraryItemExtras? extraMap;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
final shelfsdk.BookMinified? bookDetailsCached;
@override
Widget build(BuildContext context) {
@ -438,7 +394,8 @@ class _BookTitle extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: HeroTagPrefixes.bookTitle +
tag:
HeroTagPrefixes.bookTitle +
// itemId +
(extraMap?.heroTagSuffix ?? ''),
child: Text(
@ -449,7 +406,7 @@ class _BookTitle extends StatelessWidget {
// pauseBetween: 150.ms,
// numberOfReps: 3,
style: themeData.textTheme.headlineLarge,
itemBookMetadata?.title ?? bookDetailsCached?.metadata.title ?? '',
itemBookMetadata?.title ?? extraMap?.book?.metadata.title ?? '',
),
),
// subtitle if available
@ -457,7 +414,7 @@ class _BookTitle extends StatelessWidget {
? const SizedBox.shrink()
: Text(
style: themeData.textTheme.titleSmall?.copyWith(
color: themeData.colorScheme.onSurface.withOpacity(0.8),
color: themeData.colorScheme.onSurface.withValues(alpha: 0.8),
),
itemBookMetadata?.subtitle ?? '',
),
@ -468,7 +425,6 @@ class _BookTitle extends StatelessWidget {
class _BookAuthors extends StatelessWidget {
const _BookAuthors({
super.key,
required this.itemBookMetadata,
required this.bookDetailsCached,
});
@ -482,7 +438,7 @@ class _BookAuthors extends StatelessWidget {
String generateAuthorsString() {
final authors = (itemBookMetadata)?.authors ?? [];
if (authors.isEmpty) {
return (bookDetailsCached?.metadata as shelfsdk.BookMetadataMinified?)
return (bookDetailsCached?.metadata.asBookMetadataMinified)
?.authorName ??
'';
}

View file

@ -1,20 +1,62 @@
import 'package:flutter/material.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
class LibraryItemMetadata extends StatelessWidget {
const LibraryItemMetadata({
super.key,
required this.item,
this.itemBookMetadata,
this.bookDetailsCached,
});
class LibraryItemMetadata extends HookConsumerWidget {
const LibraryItemMetadata({super.key, required this.id});
final shelfsdk.LibraryItemExpanded item;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
final shelfsdk.BookMinified? bookDetailsCached;
final String id;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final item = ref.watch(libraryItemProvider(id)).value;
/// formats the duration of the book as `10h 30m`
///
/// will add up all the durations of the audio files first
/// then convert them to the given format
String? getDurationFormatted() {
final book = (item?.media.asBookExpanded);
if (book == null) {
return null;
}
final duration = book.audioFiles
.map((e) => e.duration)
.reduce((value, element) => value + element);
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
return '${hours}h ${minutes}m';
}
/// will return the size of the book in MB
///
/// will add up all the sizes of the audio files first
/// then convert them to MB
String? getSizeFormatted() {
final book = (item?.media.asBookExpanded);
if (book == null) {
return null;
}
final size = book.audioFiles
.map((e) => e.metadata.size)
.reduce((value, element) => value + element);
return '${size / 1024 ~/ 1024} MB';
}
/// will return the codec and bitrate of the book
String? getCodecAndBitrate() {
final book = (item?.media.asBookExpanded);
if (book == null) {
return null;
}
final codec = book.audioFiles.first.codec.toUpperCase();
// final bitrate = book.audioFiles.first.bitRate;
return codec;
}
final itemBookMetadata = item?.media.metadata.asBookMetadataExpanded;
final children = [
// duration of the book
_MetadataItem(
@ -27,7 +69,8 @@ class LibraryItemMetadata extends StatelessWidget {
),
_MetadataItem(
title: 'Published',
value: itemBookMetadata?.publishedDate ??
value:
itemBookMetadata?.publishedDate ??
itemBookMetadata?.publishedYear ??
'Unknown',
),
@ -42,75 +85,27 @@ class LibraryItemMetadata extends StatelessWidget {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// alternate between metadata and vertical divider
children: List.generate(
children.length * 2 - 1,
(index) {
if (index.isEven) {
return children[index ~/ 2];
}
return VerticalDivider(
indent: 6,
endIndent: 6,
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
);
},
),
children: List.generate(children.length * 2 - 1, (index) {
if (index.isEven) {
return children[index ~/ 2];
}
return VerticalDivider(
indent: 6,
endIndent: 6,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
);
}),
),
),
);
}
/// formats the duration of the book as `10h 30m`
///
/// will add up all the durations of the audio files first
/// then convert them to the given format
String? getDurationFormatted() {
final book = (item.media as shelfsdk.BookExpanded?);
if (book == null) {
return null;
}
final duration = book.audioFiles
.map((e) => e.duration)
.reduce((value, element) => value + element);
final hours = duration.inHours;
final minutes = duration.inMinutes.remainder(60);
return '${hours}h ${minutes}m';
}
/// will return the size of the book in MB
///
/// will add up all the sizes of the audio files first
/// then convert them to MB
String? getSizeFormatted() {
final book = (item.media as shelfsdk.BookExpanded?);
if (book == null) {
return null;
}
final size = book.audioFiles
.map((e) => e.metadata.size)
.reduce((value, element) => value + element);
return '${size / 1024 ~/ 1024} MB';
}
/// will return the codec and bitrate of the book
String? getCodecAndBitrate() {
final book = (item.media as shelfsdk.BookExpanded?);
if (book == null) {
return null;
}
final codec = book.audioFiles.first.codec.toUpperCase();
// final bitrate = book.audioFiles.first.bitRate;
return codec;
}
}
/// key-value pair to display as column
class _MetadataItem extends StatelessWidget {
const _MetadataItem({
super.key,
required this.title,
required this.value,
});
const _MetadataItem({required this.title, required this.value});
final String title;
final String value;
@ -124,7 +119,7 @@ class _MetadataItem extends StatelessWidget {
children: [
Text(
style: themeData.textTheme.titleMedium?.copyWith(
color: themeData.colorScheme.onSurface.withOpacity(0.90),
color: themeData.colorScheme.onSurface.withValues(alpha: 0.90),
),
value,
maxLines: 1,
@ -132,7 +127,7 @@ class _MetadataItem extends StatelessWidget {
),
Text(
style: themeData.textTheme.bodySmall?.copyWith(
color: themeData.colorScheme.onSurface.withOpacity(0.7),
color: themeData.colorScheme.onSurface.withValues(alpha: 0.7),
),
title,
maxLines: 1,

View file

@ -3,109 +3,114 @@ import 'dart:math';
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/features/item_viewer/view/library_item_sliver_app_bar.dart';
import 'package:vaani/features/player/providers/player_form.dart';
import 'package:vaani/features/player/view/mini_player_bottom_padding.dart';
import 'package:vaani/router/models/library_item_extras.dart';
import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/widgets/expandable_description.dart';
import 'package:vaani/theme/theme_from_cover_provider.dart';
import 'library_item_actions.dart';
import 'library_item_hero_section.dart';
import 'library_item_metadata.dart';
class LibraryItemPage extends HookConsumerWidget {
const LibraryItemPage({
super.key,
required this.itemId,
this.extra,
});
const LibraryItemPage({super.key, required this.itemId, this.extra});
final String itemId;
final Object? extra;
static const double _showFabThreshold = 300.0;
@override
Widget build(BuildContext context, WidgetRef ref) {
final extraMap =
extra is LibraryItemExtras ? extra as LibraryItemExtras : null;
final bookDetailsCached = extraMap?.book;
final providedCacheImage = extraMap?.coverImage != null
? Image.memory(extraMap!.coverImage!)
final additionalItemData = extra is LibraryItemExtras
? extra as LibraryItemExtras
: null;
final scrollController = useScrollController();
final showFab = useState(false);
final itemFromApi = ref.watch(libraryItemProvider(itemId));
// Effect to listen to scroll changes and update FAB visibility
useEffect(() {
void listener() {
if (!scrollController.hasClients) {
return; // Ensure controller is attached
}
final shouldShow = scrollController.offset > _showFabThreshold;
// Update state only if it changes and widget is still mounted
if (showFab.value != shouldShow && context.mounted) {
showFab.value = shouldShow;
}
}
var itemBookMetadata =
itemFromApi.valueOrNull?.media.metadata.asBookMetadataExpanded;
scrollController.addListener(listener);
// Initial check in case the view starts scrolled (less likely but safe)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients && context.mounted) {
listener();
}
});
final useMaterialThemeOnItemPage =
ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage;
AsyncValue<ColorScheme?> coverColorScheme = const AsyncValue.loading();
if (useMaterialThemeOnItemPage) {
coverColorScheme = ref.watch(
themeOfLibraryItemProvider(
itemFromApi.valueOrNull,
brightness: Theme.of(context).brightness,
),
);
debugPrint('ColorScheme: ${coverColorScheme.valueOrNull}');
} else {
debugPrint('useMaterialThemeOnItemPage is false');
// AsyncValue<ColorScheme?> coverColorScheme = const AsyncValue.loading();
// Cleanup: remove the listener when the widget is disposed
return () => scrollController.removeListener(listener);
}, [scrollController]); // Re-run effect if scrollController changes
// --- FAB Scroll-to-Top Logic ---
void scrollToTop() {
if (scrollController.hasClients) {
scrollController.animateTo(
0.0, // Target offset (top)
duration: 300.ms,
curve: Curves.easeInOut,
);
}
}
return ThemeProvider(
initTheme: Theme.of(context),
duration: 200.ms,
child: ThemeSwitchingArea(
child: Scaffold(
floatingActionButton: AnimatedSwitcher(
duration: 250.ms,
// A common transition for FABs (fade + scale)
transitionBuilder: (Widget child, Animation<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(
controller: scrollController,
slivers: [
const LibraryItemSliverAppBar(),
LibraryItemSliverAppBar(
id: itemId,
scrollController: scrollController,
),
SliverPadding(
padding: const EdgeInsets.all(8),
sliver: LibraryItemHeroSection(
itemId: itemId,
extraMap: extraMap,
providedCacheImage: providedCacheImage,
item: itemFromApi,
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
coverColorScheme: coverColorScheme,
extraMap: additionalItemData,
),
),
// a horizontal display with dividers of metadata
SliverToBoxAdapter(
child: itemFromApi.valueOrNull != null
? LibraryItemMetadata(
item: itemFromApi.valueOrNull!,
itemBookMetadata: itemBookMetadata,
bookDetailsCached: bookDetailsCached,
)
: const SizedBox.shrink(),
),
SliverToBoxAdapter(child: LibraryItemMetadata(id: itemId)),
// a row of actions like play, download, share, etc
SliverToBoxAdapter(
child: itemFromApi.valueOrNull != null
? LibraryItemActions(item: itemFromApi.valueOrNull!)
: const SizedBox.shrink(),
),
SliverToBoxAdapter(child: LibraryItemActions(id: itemId)),
// a expandable section for book description
SliverToBoxAdapter(
child:
itemFromApi.valueOrNull?.media.metadata.description != null
? ExpandableDescription(
title: 'About the Book',
content: itemFromApi
.valueOrNull!.media.metadata.description!,
)
: const SizedBox.shrink(),
),
SliverToBoxAdapter(child: LibraryItemDescription(id: itemId)),
// a padding at the bottom to make sure the last item is not hidden by mini player
const SliverToBoxAdapter(
child: SizedBox(height: playerMinHeight),
),
const SliverToBoxAdapter(child: MiniPlayerBottomPadding()),
],
),
),
@ -114,20 +119,42 @@ class LibraryItemPage extends HookConsumerWidget {
}
}
class LibraryItemDescription extends HookConsumerWidget {
const LibraryItemDescription({super.key, required this.id});
final String id;
@override
Widget build(BuildContext context, WidgetRef ref) {
final item = ref.watch(libraryItemProvider(id)).value;
if (item == null) {
return const SizedBox();
}
return ExpandableDescription(
title: 'About the Book',
content: item.media.metadata.description ?? 'Sorry, no description found',
);
}
}
/// Calculate the width of the book cover based on the screen size
double calculateWidth(
BuildContext context,
BoxConstraints constraints, {
/// width ratio of the cover image to the available width
double widthRatio = 0.4,
/// height ratio of the cover image to the available height
double maxHeightToUse = 0.25,
}) {
final availHeight =
min(constraints.maxHeight, MediaQuery.of(context).size.height);
final availWidth =
min(constraints.maxWidth, MediaQuery.of(context).size.width);
final availHeight = min(
constraints.maxHeight,
MediaQuery.of(context).size.height,
);
final availWidth = min(
constraints.maxWidth,
MediaQuery.of(context).size.width,
);
// make the width widthRatio of the available width
var width = availWidth * widthRatio;

View file

@ -1,23 +1,78 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider;
class LibraryItemSliverAppBar extends StatelessWidget {
class LibraryItemSliverAppBar extends HookConsumerWidget {
const LibraryItemSliverAppBar({
super.key,
required this.id,
required this.scrollController,
});
final String id;
final ScrollController scrollController;
static const double _showTitleThreshold = kToolbarHeight * 0.5;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final item = ref.watch(libraryItemProvider(id)).value;
final showTitle = useState(false);
useEffect(() {
void listener() {
final shouldShow =
scrollController.hasClients &&
scrollController.offset > _showTitleThreshold;
if (showTitle.value != shouldShow) {
showTitle.value = shouldShow;
}
}
scrollController.addListener(listener);
// Trigger listener once initially in case the view starts scrolled
// (though unlikely for this specific use case, it's good practice)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
listener();
}
});
return () => scrollController.removeListener(listener);
}, [scrollController]);
return SliverAppBar(
backgroundColor: Colors.transparent,
elevation: 0,
floating: true,
floating: false,
pinned: true,
primary: true,
snap: true,
actions: [
// cast button
IconButton(onPressed: () {}, icon: const Icon(Icons.cast)),
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
// IconButton(
// icon: const Icon(Icons.cast),
// onPressed: () {
// // Handle search action
// },
// ),
],
title: AnimatedSwitcher(
duration: const Duration(milliseconds: 150),
child: showTitle.value
? Text(
// Use a Key to help AnimatedSwitcher differentiate widgets
key: const ValueKey('title-text'),
item?.media.metadata.title ?? '',
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium,
)
: const SizedBox(
// Also give it a key for differentiation
key: ValueKey('empty-title'),
width: 0, // Ensure it takes no space if possible
height: 0,
),
),
centerTitle: false,
);
}
}

View file

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

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';
@freezed
class Flow with _$Flow {
sealed class Flow with _$Flow {
const factory Flow({
required Uri serverUri,
required String state,

View file

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

View file

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

View file

@ -6,219 +6,167 @@ part of 'oauth_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$loginInExchangeForCodeHash() =>
r'e931254959d9eb8196439c6b0c884c26cbe17c2f';
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
@ProviderFor(OauthFlows)
final oauthFlowsProvider = OauthFlowsProvider._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// the code returned by the server in exchange for the verifier
///
/// Copied from [loginInExchangeForCode].
@ProviderFor(loginInExchangeForCode)
const loginInExchangeForCodeProvider = LoginInExchangeForCodeFamily();
/// the code returned by the server in exchange for the verifier
///
/// Copied from [loginInExchangeForCode].
class LoginInExchangeForCodeFamily extends Family<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,
final class OauthFlowsProvider
extends $NotifierProvider<OauthFlows, Map<State, Flow>> {
OauthFlowsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'oauthFlowsProvider',
isAutoDispose: false,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
),
);
}
$allTransitiveDependencies: null,
);
@override
AutoDisposeFutureProviderElement<String?> createElement() {
return _LoginInExchangeForCodeProviderElement(this);
String debugGetCreateSourceHash() => _$oauthFlowsHash();
@$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
bool operator ==(Object other) {
return other is LoginInExchangeForCodeProvider &&
other.oauthState == oauthState &&
other.code == code &&
other.responseHandler == responseHandler;
other.argument == argument;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, oauthState.hashCode);
hash = _SystemHash.combine(hash, code.hashCode);
hash = _SystemHash.combine(hash, responseHandler.hashCode);
return _SystemHash.finish(hash);
return argument.hashCode;
}
}
mixin LoginInExchangeForCodeRef on AutoDisposeFutureProviderRef<String?> {
/// The parameter `oauthState` of this provider.
String get oauthState;
String _$loginInExchangeForCodeHash() =>
r'bfc3945529048a0f536052fd5579b76457560fcd';
/// The parameter `code` of this provider.
String get code;
/// the code returned by the server in exchange for the verifier
/// The parameter `responseHandler` of this provider.
ErrorResponseHandler? get responseHandler;
final class LoginInExchangeForCodeFamily extends $Family
with
$FunctionalFamilyOverride<
FutureOr<String?>,
({State oauthState, Code code, ErrorResponseHandler? responseHandler})
> {
LoginInExchangeForCodeFamily._()
: super(
retry: null,
name: r'loginInExchangeForCodeProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// the code returned by the server in exchange for the verifier
LoginInExchangeForCodeProvider call({
required State oauthState,
required Code code,
ErrorResponseHandler? responseHandler,
}) => LoginInExchangeForCodeProvider._(
argument: (
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
),
from: this,
);
@override
String toString() => r'loginInExchangeForCodeProvider';
}
class _LoginInExchangeForCodeProviderElement
extends AutoDisposeFutureProviderElement<String?>
with LoginInExchangeForCodeRef {
_LoginInExchangeForCodeProviderElement(super.provider);
@override
String get oauthState =>
(origin as LoginInExchangeForCodeProvider).oauthState;
@override
String get code => (origin as LoginInExchangeForCodeProvider).code;
@override
ErrorResponseHandler? get responseHandler =>
(origin as LoginInExchangeForCodeProvider).responseHandler;
}
String _$oauthFlowsHash() => r'4e278baa0bf26f2a10694ca2caadb68dd5b6156f';
/// See also [OauthFlows].
@ProviderFor(OauthFlows)
final oauthFlowsProvider =
NotifierProvider<OauthFlows, Map<State, Flow>>.internal(
OauthFlows.new,
name: r'oauthFlowsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$oauthFlowsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$OauthFlows = Notifier<Map<State, Flow>>;
// 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

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

View file

@ -9,101 +9,120 @@ import 'package:vaani/shared/utils.dart';
import 'package:vaani/shared/widgets/add_new_server.dart';
class OnboardingSinglePage extends HookConsumerWidget {
const OnboardingSinglePage({
super.key,
});
const OnboardingSinglePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final apiSettings = ref.watch(apiSettingsProvider);
final serverUriController = useTextEditingController(
text: apiSettings.activeServer?.serverUrl.toString() ?? '',
);
var audiobookshelfUri = makeBaseUrl(serverUriController.text);
final canUserLogin = useState(apiSettings.activeServer != null);
fadeSlideTransitionBuilder(
Widget child,
Animation<double> animation,
) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.3),
end: const Offset(0, 0),
).animate(animation),
child: child,
),
);
}
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Welcome to Vaani',
style: Theme.of(context).textTheme.headlineSmall,
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Center(
child: SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: 600,
minWidth: constraints.maxWidth < 600
? constraints.maxWidth
: 0,
),
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 20.0),
child: SafeArea(child: OnboardingBody()),
),
),
),
),
const SizedBox.square(
dimension: 16.0,
),
Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedSwitcher(
duration: 500.ms,
transitionBuilder: fadeSlideTransitionBuilder,
child: canUserLogin.value
? Text(
'Server connected, please login',
key: const ValueKey('connected'),
style: Theme.of(context).textTheme.bodyMedium,
)
: Text(
'Please enter the URL of your AudiobookShelf Server',
key: const ValueKey('not_connected'),
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: AddNewServer(
controller: serverUriController,
allowEmpty: true,
onPressed: () {
canUserLogin.value = serverUriController.text.isNotEmpty;
},
),
),
AnimatedSwitcher(
duration: 500.ms,
transitionBuilder: fadeSlideTransitionBuilder,
child: canUserLogin.value
? UserLoginWidget(
server: audiobookshelfUri,
)
// ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0)
: const RedirectToABS().animate().fadeIn().slideY(
curve: Curves.easeInOut,
duration: 500.ms,
),
),
],
);
},
),
);
}
}
Widget fadeSlideTransitionBuilder(Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: SlideTransition(
position: Tween<Offset>(
begin: const Offset(0, 0.3),
end: const Offset(0, 0),
).animate(animation),
child: child,
),
);
}
class OnboardingBody extends HookConsumerWidget {
const OnboardingBody({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final apiSettings = ref.watch(apiSettingsProvider);
final serverUriController = useTextEditingController(
text: apiSettings.activeServer?.serverUrl.toString() ?? 'https://',
);
var audiobookshelfUri = makeBaseUrl(serverUriController.text);
final canUserLogin = useState(apiSettings.activeServer != null);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Welcome to Vaani',
style: Theme.of(context).textTheme.headlineSmall,
),
),
const SizedBox.square(dimension: 16.0),
Padding(
padding: const EdgeInsets.all(8.0),
child: AnimatedSwitcher(
duration: 500.ms,
transitionBuilder: fadeSlideTransitionBuilder,
child: canUserLogin.value
? Text(
'Server connected, please login',
key: const ValueKey('connected'),
style: Theme.of(context).textTheme.bodyMedium,
)
: Text(
'Please enter the URL of your AudiobookShelf Server',
key: const ValueKey('not_connected'),
style: Theme.of(context).textTheme.bodyMedium,
),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: AddNewServer(
controller: serverUriController,
allowEmpty: true,
onPressed: () {
canUserLogin.value = serverUriController.text.isNotEmpty;
},
),
),
const SizedBox.square(dimension: 16.0),
AnimatedSwitcher(
duration: 500.ms,
transitionBuilder: fadeSlideTransitionBuilder,
child: canUserLogin.value
? UserLoginWidget(server: audiobookshelfUri)
// ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0)
: const RedirectToABS().animate().fadeIn().slideY(
curve: Curves.easeInOut,
duration: 500.ms,
),
),
],
);
}
}
class RedirectToABS extends StatelessWidget {
const RedirectToABS({
super.key,
});
const RedirectToABS({super.key});
@override
Widget build(BuildContext context) {
@ -119,18 +138,14 @@ class RedirectToABS extends StatelessWidget {
isSemanticButton: false,
style: ButtonStyle(
elevation: WidgetStateProperty.all(0),
padding: WidgetStateProperty.all(
const EdgeInsets.all(0),
),
padding: WidgetStateProperty.all(const EdgeInsets.all(0)),
),
onPressed: () async {
// open the github page
// ignore: avoid_print
print('Opening the github page');
await handleLaunchUrl(
Uri.parse(
'https://www.audiobookshelf.org',
),
Uri.parse('https://www.audiobookshelf.org'),
);
},
child: const Text('Click here'),

View file

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

View file

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

View file

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

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