Compare commits

..

No commits in common. "main" and "v0.0.3" have entirely different histories.
main ... v0.0.3

219 changed files with 10661 additions and 17347 deletions

3
.fvmrc
View file

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

View file

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

View file

@ -1,218 +0,0 @@
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

75
.github/workflows/flutter_release.yaml vendored Normal file
View file

@ -0,0 +1,75 @@
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 }}

View file

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

3
.gitmodules vendored
View file

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

1
.vscode/launch.json vendored
View file

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

28
.vscode/settings.json vendored
View file

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

View file

@ -1,181 +0,0 @@
# 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! 🌟

View file

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

View file

@ -1,221 +0,0 @@
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,38 +6,17 @@
Client for [Audiobookshelf](https://github.com/advplyr/audiobookshelf) server made with Flutter. 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 --> <!-- a github image with link to releases for download -->
[<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://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://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 ## Screencaps
https://github.com/user-attachments/assets/2ac9ace2-4a3c-40fc-adde-55914e4cf62d https://github.com/user-attachments/assets/2ac9ace2-4a3c-40fc-adde-55914e4cf62d
| <img src="images/screenshots/android/home.jpg" width="200" /> | <img src="images/screenshots/android/bookview.jpg" width="200" /> | <img src="images/screenshots/android/player.jpg" width="200" /> | |<img src="images/screenshots/android/home.jpg" width="200" />|<img src="images/screenshots/android/bookview.jpg" width="200" />|<img src="images/screenshots/android/player.jpg" width="200" />|
| :-----------------------------------------------------------: | :---------------------------------------------------------------: | :-------------------------------------------------------------: | |:---:|:---:|:---:|
| Home | Book View | Player | |Home|Book View|Player|
Currently, the app is in development and is not ready for production use. Currently, the app is in development and is not ready for production use.
Plan is to have support for android, and desktop. Plan is to have support for android, ios, and desktop.

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

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

View file

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

View file

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

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

View file

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

View file

@ -1,8 +0,0 @@
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

View file

@ -1,45 +0,0 @@
# 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.)

View file

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

View file

@ -1,2 +0,0 @@
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

View file

@ -1,38 +0,0 @@
# 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

View file

@ -1,48 +0,0 @@
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

@ -1,10 +0,0 @@
<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.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 746 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 KiB

View file

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

View file

@ -1 +0,0 @@
Vaani

View file

@ -1,18 +0,0 @@
<?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>

View file

@ -1,36 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.4 KiB

View file

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

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

View file

@ -0,0 +1,28 @@
// 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

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

View file

@ -6,94 +6,167 @@ part of 'image_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND String _$coverImageHash() => r'702afafa217dfcbb290837caf21cc1ef54defd55';
// ignore_for_file: type=lint, type=warning
@ProviderFor(CoverImage) /// Copied from Dart SDK
final coverImageProvider = CoverImageFamily._(); class _SystemHash {
_SystemHash._();
final class CoverImageProvider static int combine(int hash, int value) {
extends $StreamNotifierProvider<CoverImage, Uint8List> { // ignore: parameter_assignments
CoverImageProvider._({ hash = 0x1fffffff & (hash + value);
required CoverImageFamily super.from, // ignore: parameter_assignments
required String super.argument, hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
}) : super( return hash ^ (hash >> 6);
retry: null, }
name: r'coverImageProvider',
isAutoDispose: false, static int finish(int hash) {
dependencies: null, // ignore: parameter_assignments
$allTransitiveDependencies: null, hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
); // ignore: parameter_assignments
hash = hash ^ (hash >> 11);
@override return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
String debugGetCreateSourceHash() => _$coverImageHash(); }
}
@override
String toString() { abstract class _$CoverImage extends BuildlessStreamNotifier<Uint8List> {
return r'coverImageProvider' late final LibraryItem libraryItem;
''
'($argument)'; Stream<Uint8List> build(
LibraryItem libraryItem,
);
}
/// See also [CoverImage].
@ProviderFor(CoverImage)
const coverImageProvider = CoverImageFamily();
/// See also [CoverImage].
class CoverImageFamily extends Family<AsyncValue<Uint8List>> {
/// See also [CoverImage].
const CoverImageFamily();
/// See also [CoverImage].
CoverImageProvider call(
LibraryItem libraryItem,
) {
return CoverImageProvider(
libraryItem,
);
} }
@$internal
@override @override
CoverImage create() => CoverImage(); 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);
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is CoverImageProvider && other.argument == argument; return other is CoverImageProvider && other.libraryItem == libraryItem;
} }
@override @override
int get hashCode { int get hashCode {
return argument.hashCode; var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, libraryItem.hashCode);
return _SystemHash.finish(hash);
} }
} }
String _$coverImageHash() => r'89cc4783cbc76bb41beae34384d92fb277135c75'; mixin CoverImageRef on StreamNotifierProviderRef<Uint8List> {
/// The parameter `libraryItem` of this provider.
final class CoverImageFamily extends $Family LibraryItem get libraryItem;
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
String toString() => r'coverImageProvider';
} }
abstract class _$CoverImage extends $StreamNotifier<Uint8List> { class _CoverImageProviderElement
late final _$args = ref.$arg as String; extends StreamNotifierProviderElement<CoverImage, Uint8List>
String get itemId => _$args; with CoverImageRef {
_CoverImageProviderElement(super.provider);
Stream<Uint8List> build(String itemId);
@$mustCallSuper
@override @override
void runBuild() { LibraryItem get libraryItem => (origin as CoverImageProvider).libraryItem;
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'); final _logger = Logger('LibraryItemProvider');
/// provides the library item for the given id /// provides the library item for the given id
@Riverpod(keepAlive: true) @riverpod
class LibraryItem extends _$LibraryItem { class LibraryItem extends _$LibraryItem {
@override @override
Stream<shelfsdk.LibraryItemExpanded> build(String id) async* { Stream<shelfsdk.LibraryItemExpanded> build(String id) async* {
@ -22,12 +22,11 @@ class LibraryItem extends _$LibraryItem {
_logger.fine('LibraryItemProvider fetching library item: $id'); _logger.fine('LibraryItemProvider fetching library item: $id');
// ! this is a mock delay // ! this is a mock delay
// await Future.delayed(const Duration(seconds: 150)); // await Future.delayed(const Duration(seconds: 10));
// look for the item in the cache // look for the item in the cache
final key = CacheKey.libraryItem(id); final key = CacheKey.libraryItem(id);
final cachedFile = final cachedFile = await apiResponseCacheManager.getFileFromMemory(key) ??
await apiResponseCacheManager.getFileFromMemory(key) ??
await apiResponseCacheManager.getFileFromCache(key); await apiResponseCacheManager.getFileFromCache(key);
if (cachedFile != null) { if (cachedFile != null) {
_logger.fine( _logger.fine(

View file

@ -6,112 +6,183 @@ part of 'library_item_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND String _$libraryItemHash() => r'fa3f8309349c5b1b777f1bc919616e51c3f5b520';
// ignore_for_file: type=lint, type=warning
/// provides the library item for the given id
@ProviderFor(LibraryItem) /// Copied from Dart SDK
final libraryItemProvider = LibraryItemFamily._(); class _SystemHash {
_SystemHash._();
/// provides the library item for the given id static int combine(int hash, int value) {
final class LibraryItemProvider // ignore: parameter_assignments
extends $StreamNotifierProvider<LibraryItem, shelfsdk.LibraryItemExpanded> { hash = 0x1fffffff & (hash + value);
/// provides the library item for the given id // ignore: parameter_assignments
LibraryItemProvider._({ hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
required LibraryItemFamily super.from, return hash ^ (hash >> 6);
required String super.argument, }
}) : super(
retry: null, static int finish(int hash) {
name: r'libraryItemProvider', // ignore: parameter_assignments
isAutoDispose: false, hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
dependencies: null, // ignore: parameter_assignments
$allTransitiveDependencies: null, hash = hash ^ (hash >> 11);
); return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
@override }
String debugGetCreateSourceHash() => _$libraryItemHash();
abstract class _$LibraryItem
@override extends BuildlessAutoDisposeStreamNotifier<shelfsdk.LibraryItemExpanded> {
String toString() { late final String id;
return r'libraryItemProvider'
'' Stream<shelfsdk.LibraryItemExpanded> build(
'($argument)'; String id,
);
}
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
@ProviderFor(LibraryItem)
const libraryItemProvider = LibraryItemFamily();
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
class LibraryItemFamily
extends Family<AsyncValue<shelfsdk.LibraryItemExpanded>> {
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
const LibraryItemFamily();
/// provides the library item for the given id
///
/// Copied from [LibraryItem].
LibraryItemProvider call(
String id,
) {
return LibraryItemProvider(
id,
);
} }
@$internal
@override @override
LibraryItem create() => LibraryItem(); 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);
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is LibraryItemProvider && other.argument == argument; return other is LibraryItemProvider && other.id == id;
} }
@override @override
int get hashCode { int get hashCode {
return argument.hashCode; var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, id.hashCode);
return _SystemHash.finish(hash);
} }
} }
String _$libraryItemHash() => r'a3cfa7f912e9498a70b5782899018b6964d6445c'; mixin LibraryItemRef
on AutoDisposeStreamNotifierProviderRef<shelfsdk.LibraryItemExpanded> {
/// provides the library item for the given id /// The parameter `id` of this provider.
String get 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 toString() => r'libraryItemProvider';
} }
/// provides the library item for the given id class _LibraryItemProviderElement
extends AutoDisposeStreamNotifierProviderElement<LibraryItem,
shelfsdk.LibraryItemExpanded> with LibraryItemRef {
_LibraryItemProviderElement(super.provider);
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 @override
void runBuild() { String get id => (origin as LibraryItemProvider).id;
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

@ -1,60 +0,0 @@
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

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

View file

@ -6,78 +6,25 @@ part of 'server_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
// 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
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,
);
@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() => String _$audiobookShelfServerHash() =>
r'144817dcb3704b80c5b60763167fcf932f00c29c'; r'f0d645bb42233c59886bc43fdc473897484ceca1';
/// provides with a set of servers added by the user /// 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,
);
abstract class _$AudiobookShelfServer typedef _$AudiobookShelfServer
extends $Notifier<Set<model.AudiobookShelfServer>> { = AutoDisposeNotifier<Set<model.AudiobookShelfServer>>;
Set<model.AudiobookShelfServer> build(); // ignore_for_file: type=lint
@$mustCallSuper // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@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,4 +10,5 @@ class HeroTagPrefixes {
static const String bookTitle = 'book_title_'; static const String bookTitle = 'book_title_';
static const String narratorName = 'narrator_name_'; static const String narratorName = 'narrator_name_';
static const String libraryItemPlayButton = 'library_item_play_button_'; static const String libraryItemPlayButton = 'library_item_play_button_';
} }

View file

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

1009
lib/db/cache/schemas/image.g.dart vendored Normal file

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -0,0 +1,496 @@
// 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_plus_secure/hive_plus_secure.dart'; import 'package:hive/hive.dart';
import 'package:vaani/features/per_book_settings/models/book_settings.dart'; import 'package:vaani/features/per_book_settings/models/book_settings.dart';
import 'package:vaani/settings/models/models.dart'; import 'package:vaani/settings/models/models.dart';

View file

@ -1,6 +1,5 @@
// download manager to handle download tasks of files // download manager to handle download tasks of files
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
@ -8,7 +7,6 @@ import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/shared/extensions/model_conversions.dart'; import 'package:vaani/shared/extensions/model_conversions.dart';
import 'package:vaani/shared/extensions/obfuscation.dart';
final _logger = Logger('AudiobookDownloadManager'); final _logger = Logger('AudiobookDownloadManager');
final tq = MemoryTaskQueue(); final tq = MemoryTaskQueue();
@ -36,13 +34,10 @@ class AudiobookDownloadManager {
FileDownloader().addTaskQueue(tq); FileDownloader().addTaskQueue(tq);
_logger.fine( _logger.fine('initialized with baseUrl: $baseUrl, token: $token');
'initialized with baseUrl: ${Uri.parse(baseUrl).obfuscate()} and token: ${token.obfuscate()}',
);
_logger.fine( _logger.fine(
'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause', 'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause',
); );
initDownloadManager();
} }
// the base url for the audio files // the base url for the audio files
@ -60,20 +55,17 @@ class AudiobookDownloadManager {
// whether to allow pausing of downloads // whether to allow pausing of downloads
final bool allowPause; final bool allowPause;
final StreamController<TaskUpdate> _taskStatusController = // final List<DownloadTask> _downloadTasks = [];
StreamController.broadcast();
Stream<TaskUpdate> get taskUpdateStream => _taskStatusController.stream;
late StreamSubscription<TaskUpdate> _updatesSubscription;
Future<void> queueAudioBookDownload(LibraryItemExpanded item) async { Future<void> queueAudioBookDownload(LibraryItemExpanded item) async {
_logger.info('queuing download for item: ${item.id}'); _logger.info('queuing download for item: ${item.toJson()}');
// create a download task for each file in the item // create a download task for each file in the item
final directory = await getApplicationSupportDirectory(); final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) { for (final file in item.libraryFiles) {
// check if the file is already downloaded // check if the file is already downloaded
if (isFileDownloaded(constructFilePath(directory, item, file))) { if (isFileDownloaded(
constructFilePath(directory, item, file),
)) {
_logger.info('file already downloaded: ${file.metadata.filename}'); _logger.info('file already downloaded: ${file.metadata.filename}');
continue; continue;
} }
@ -88,56 +80,61 @@ class AudiobookDownloadManager {
allowPause: allowPause, allowPause: allowPause,
group: item.id, group: item.id,
baseDirectory: downloadDirectory, baseDirectory: downloadDirectory,
updates: Updates.statusAndProgress,
// metaData: token // metaData: token
); );
// _downloadTasks.add(task); // _downloadTasks.add(task);
tq.add(task); tq.add(task);
_logger.info('queued task: ${task.taskId}'); _logger.info('queued task: ${task.toJson()}');
} }
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( String constructFilePath(
Directory directory, Directory directory,
LibraryItemExpanded item, LibraryItemExpanded item,
LibraryFile file, LibraryFile file,
) => '${directory.path}/${item.relPath}/${file.metadata.filename}'; ) =>
'${directory.path}/${item.relPath}/${file.metadata.filename}';
// void startDownload() {
// for (final task in _downloadTasks) {
// _logger.fine('enqueuing task: $task');
// FileDownloader().enqueue(task);
// }
// }
void dispose() { void dispose() {
_updatesSubscription.cancel(); // tq.removeAll();
FileDownloader().destroy(); _logger.fine('disposed');
_logger.fine('Destroyed download manager');
}
bool isItemDownloading(String id) {
return tq.enqueued.any((task) => task.group == id);
} }
bool isFileDownloaded(String filePath) { bool isFileDownloaded(String filePath) {
return File(filePath).existsSync(); // Check if the file exists
} final fileExists = File(filePath).existsSync();
Future<List<LibraryFile>> getDownloadedFilesMetadata( return fileExists;
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 { Future<bool> isItemDownloaded(LibraryItemExpanded item) async {
@ -148,12 +145,11 @@ class AudiobookDownloadManager {
return false; return false;
} }
} }
_logger.info('all files downloaded for item id: ${item.id}');
return true; return true;
} }
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async { Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
_logger.info('deleting downloaded item with id: ${item.id}');
final directory = await getApplicationSupportDirectory(); final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) { for (final file in item.libraryFiles) {
final filePath = constructFilePath(directory, item, file); final filePath = constructFilePath(directory, item, file);
@ -164,7 +160,7 @@ class AudiobookDownloadManager {
} }
} }
Future<List<Uri>> getDownloadedFilesUri(LibraryItemExpanded item) async { Future<List<Uri>> getDownloadedFiles(LibraryItemExpanded item) async {
final directory = await getApplicationSupportDirectory(); final directory = await getApplicationSupportDirectory();
final files = <Uri>[]; final files = <Uri>[];
for (final file in item.libraryFiles) { for (final file in item.libraryFiles) {
@ -176,28 +172,14 @@ class AudiobookDownloadManager {
return files; return files;
} }
}
Future<void> initDownloadManager() async {
// initialize the download manager Future<void> initDownloadManager() async {
_logger.info('Initializing download manager'); // initialize the download manager
final fileDownloader = FileDownloader(); var fileDownloader = FileDownloader();
fileDownloader.configureNotification(
_logger.info('Configuring Notification'); running: const TaskNotification('Downloading', 'file: {filename}'),
fileDownloader.configureNotification( progressBar: true,
running: const TaskNotification('Downloading', 'file: {filename}'), );
progressBar: true, await fileDownloader.trackTasks();
);
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,18 +1,13 @@
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/features/downloads/core/download_manager.dart'
import 'package:vaani/features/downloads/core/download_manager.dart' as core; as core;
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/item_files.dart';
part 'download_manager.g.dart'; part 'download_manager.g.dart';
final _logger = Logger('AudiobookDownloadManagerProvider');
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class SimpleDownloadManager extends _$SimpleDownloadManager { class SimpleDownloadManager extends _$SimpleDownloadManager {
@override @override
@ -30,107 +25,27 @@ class SimpleDownloadManager extends _$SimpleDownloadManager {
core.tq.maxConcurrent = downloadSettings.maxConcurrent; core.tq.maxConcurrent = downloadSettings.maxConcurrent;
core.tq.maxConcurrentByHost = downloadSettings.maxConcurrentByHost; core.tq.maxConcurrentByHost = downloadSettings.maxConcurrentByHost;
core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup; core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup;
ref.onDispose(() { ref.onDispose(() {
_logger.info('disposing download manager');
manager.dispose(); manager.dispose();
}); });
_logger.config('initialized download manager');
return manager; return manager;
} }
} }
@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 @riverpod
class IsItemDownloading extends _$IsItemDownloading { FutureOr<List<TaskRecord>> downloadHistory(
@override DownloadHistoryRef ref, {
bool build(String id) { String? group,
final manager = ref.watch(downloadManagerProvider); }) async {
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); return await FileDownloader().database.allRecords(group: group);
} }
@riverpod @Riverpod(keepAlive: false)
class IsItemDownloaded extends _$IsItemDownloaded { FutureOr<bool> downloadStatus(
@override DownloadStatusRef ref,
FutureOr<bool> build(LibraryItemExpanded item) { LibraryItemExpanded item,
final manager = ref.watch(downloadManagerProvider); ) async {
return manager.isItemDownloaded(item); final manager = ref.read(simpleDownloadManagerProvider);
} return manager.isItemDownloaded(item);
} }

View file

@ -6,480 +6,301 @@ part of 'download_manager.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND String _$downloadHistoryHash() => r'76c449e8abfa61d57566991686f534a06dc7fef7';
// ignore_for_file: type=lint, type=warning
@ProviderFor(SimpleDownloadManager) /// Copied from Dart SDK
final simpleDownloadManagerProvider = SimpleDownloadManagerProvider._(); class _SystemHash {
_SystemHash._();
final class SimpleDownloadManagerProvider static int combine(int hash, int value) {
extends // ignore: parameter_assignments
$NotifierProvider< hash = 0x1fffffff & (hash + value);
SimpleDownloadManager, // ignore: parameter_assignments
core.AudiobookDownloadManager hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
> { return hash ^ (hash >> 6);
SimpleDownloadManagerProvider._() }
: super(
from: null, static int finish(int hash) {
argument: null, // ignore: parameter_assignments
retry: null, hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
name: r'simpleDownloadManagerProvider', // ignore: parameter_assignments
isAutoDispose: false, hash = hash ^ (hash >> 11);
dependencies: null, return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
$allTransitiveDependencies: null, }
); }
/// 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 @override
String debugGetCreateSourceHash() => _$simpleDownloadManagerHash(); DownloadHistoryProvider getProviderOverride(
covariant DownloadHistoryProvider provider,
) {
return call(
group: provider.group,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@$internal
@override @override
SimpleDownloadManager create() => SimpleDownloadManager(); Iterable<ProviderOrFamily>? get dependencies => _dependencies;
/// {@macro riverpod.override_with_value} static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
Override overrideWithValue(core.AudiobookDownloadManager value) {
return $ProviderOverride( @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, origin: this,
providerOverride: $SyncValueProvider<core.AudiobookDownloadManager>( override: DownloadHistoryProvider._internal(
value, (ref) => create(ref as DownloadHistoryRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
group: group,
), ),
); );
} }
@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() => String _$simpleDownloadManagerHash() =>
r'8ab13f06ec5f2f73b73064bd285813dc890b7f36'; r'cec95717c86e422f88f78aa014d29e800e5a2089';
abstract class _$SimpleDownloadManager /// See also [SimpleDownloadManager].
extends $Notifier<core.AudiobookDownloadManager> { @ProviderFor(SimpleDownloadManager)
core.AudiobookDownloadManager build(); final simpleDownloadManagerProvider = NotifierProvider<SimpleDownloadManager,
@$mustCallSuper core.AudiobookDownloadManager>.internal(
@override SimpleDownloadManager.new,
void runBuild() { name: r'simpleDownloadManagerProvider',
final ref = debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
this.ref ? null
as $Ref< : _$simpleDownloadManagerHash,
core.AudiobookDownloadManager, dependencies: null,
core.AudiobookDownloadManager allTransitiveDependencies: null,
>; );
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
core.AudiobookDownloadManager,
core.AudiobookDownloadManager
>,
core.AudiobookDownloadManager,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(DownloadManager) typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
final downloadManagerProvider = DownloadManagerProvider._(); // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
final class 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,7 +11,10 @@ class DownloadsPage extends HookConsumerWidget {
final downloadHistory = ref.watch(downloadHistoryProvider()); final downloadHistory = ref.watch(downloadHistoryProvider());
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Downloads')), appBar: AppBar(
title: const Text('Downloads'),
backgroundColor: Colors.transparent,
),
body: Center( body: Center(
// history of downloads // history of downloads
child: downloadHistory.when( child: downloadHistory.when(

View file

@ -6,64 +6,24 @@ part of 'search_controller.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// The controller for the search bar.
@ProviderFor(GlobalSearchController)
final globalSearchControllerProvider = GlobalSearchControllerProvider._();
/// The controller for the search bar.
final class GlobalSearchControllerProvider
extends $NotifierProvider<GlobalSearchController, Raw<SearchController>> {
/// The controller for the search bar.
GlobalSearchControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'globalSearchControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$globalSearchControllerHash();
@$internal
@override
GlobalSearchController create() => GlobalSearchController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Raw<SearchController> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Raw<SearchController>>(value),
);
}
}
String _$globalSearchControllerHash() => String _$globalSearchControllerHash() =>
r'd854ace6f2e00a10fc33aba63051375f82ad1b10'; r'd854ace6f2e00a10fc33aba63051375f82ad1b10';
/// The controller for the search bar. /// The controller for the search bar.
///
/// Copied from [GlobalSearchController].
@ProviderFor(GlobalSearchController)
final globalSearchControllerProvider =
NotifierProvider<GlobalSearchController, Raw<SearchController>>.internal(
GlobalSearchController.new,
name: r'globalSearchControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$globalSearchControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
abstract class _$GlobalSearchController typedef _$GlobalSearchController = Notifier<Raw<SearchController>>;
extends $Notifier<Raw<SearchController>> { // ignore_for_file: type=lint
Raw<SearchController> build(); // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@$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,4 +1,3 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
@ -9,7 +8,7 @@ part 'search_result_provider.g.dart';
/// The provider for the search result. /// The provider for the search result.
@riverpod @riverpod
FutureOr<LibrarySearchResponse?> searchResult( FutureOr<LibrarySearchResponse?> searchResult(
Ref ref, SearchResultRef ref,
String query, { String query, {
int limit = 25, int limit = 25,
}) async { }) async {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,62 +1,20 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
import 'package:vaani/api/library_item_provider.dart';
import 'package:vaani/shared/extensions/model_conversions.dart';
class LibraryItemMetadata extends HookConsumerWidget { class LibraryItemMetadata extends StatelessWidget {
const LibraryItemMetadata({super.key, required this.id}); const LibraryItemMetadata({
super.key,
required this.item,
this.itemBookMetadata,
this.bookDetailsCached,
});
final String id; final shelfsdk.LibraryItemExpanded item;
final shelfsdk.BookMetadataExpanded? itemBookMetadata;
final shelfsdk.BookMinified? bookDetailsCached;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
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 = [ final children = [
// duration of the book // duration of the book
_MetadataItem( _MetadataItem(
@ -69,8 +27,7 @@ class LibraryItemMetadata extends HookConsumerWidget {
), ),
_MetadataItem( _MetadataItem(
title: 'Published', title: 'Published',
value: value: itemBookMetadata?.publishedDate ??
itemBookMetadata?.publishedDate ??
itemBookMetadata?.publishedYear ?? itemBookMetadata?.publishedYear ??
'Unknown', 'Unknown',
), ),
@ -85,27 +42,75 @@ class LibraryItemMetadata extends HookConsumerWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, mainAxisAlignment: MainAxisAlignment.spaceEvenly,
// alternate between metadata and vertical divider // alternate between metadata and vertical divider
children: List.generate(children.length * 2 - 1, (index) { children: List.generate(
if (index.isEven) { children.length * 2 - 1,
return children[index ~/ 2]; (index) {
} if (index.isEven) {
return VerticalDivider( return children[index ~/ 2];
indent: 6, }
endIndent: 6, return VerticalDivider(
color: Theme.of( indent: 6,
context, endIndent: 6,
).colorScheme.onSurface.withValues(alpha: 0.6), color: Theme.of(context).colorScheme.onSurface.withOpacity(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 /// key-value pair to display as column
class _MetadataItem extends StatelessWidget { class _MetadataItem extends StatelessWidget {
const _MetadataItem({required this.title, required this.value}); const _MetadataItem({
super.key,
required this.title,
required this.value,
});
final String title; final String title;
final String value; final String value;
@ -119,7 +124,7 @@ class _MetadataItem extends StatelessWidget {
children: [ children: [
Text( Text(
style: themeData.textTheme.titleMedium?.copyWith( style: themeData.textTheme.titleMedium?.copyWith(
color: themeData.colorScheme.onSurface.withValues(alpha: 0.90), color: themeData.colorScheme.onSurface.withOpacity(0.90),
), ),
value, value,
maxLines: 1, maxLines: 1,
@ -127,7 +132,7 @@ class _MetadataItem extends StatelessWidget {
), ),
Text( Text(
style: themeData.textTheme.bodySmall?.copyWith( style: themeData.textTheme.bodySmall?.copyWith(
color: themeData.colorScheme.onSurface.withValues(alpha: 0.7), color: themeData.colorScheme.onSurface.withOpacity(0.7),
), ),
title, title,
maxLines: 1, maxLines: 1,

View file

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

View file

@ -1,78 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider;
class LibraryItemSliverAppBar extends HookConsumerWidget { class LibraryItemSliverAppBar extends StatelessWidget {
const LibraryItemSliverAppBar({ const LibraryItemSliverAppBar({
super.key, super.key,
required this.id,
required this.scrollController,
}); });
final String id;
final ScrollController scrollController;
static const double _showTitleThreshold = kToolbarHeight * 0.5;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
final item = ref.watch(libraryItemProvider(id)).value;
final showTitle = useState(false);
useEffect(() {
void listener() {
final shouldShow =
scrollController.hasClients &&
scrollController.offset > _showTitleThreshold;
if (showTitle.value != shouldShow) {
showTitle.value = shouldShow;
}
}
scrollController.addListener(listener);
// Trigger listener once initially in case the view starts scrolled
// (though unlikely for this specific use case, it's good practice)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (scrollController.hasClients) {
listener();
}
});
return () => scrollController.removeListener(listener);
}, [scrollController]);
return SliverAppBar( return SliverAppBar(
backgroundColor: Colors.transparent,
elevation: 0, elevation: 0,
floating: false, floating: true,
pinned: true,
primary: true, primary: true,
snap: true,
actions: [ actions: [
// IconButton( // cast button
// icon: const Icon(Icons.cast), IconButton(onPressed: () {}, icon: const Icon(Icons.cast)),
// onPressed: () { IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
// // 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,81 +1,47 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:vaani/api/library_provider.dart' show currentLibraryProvider; import 'package:vaani/router/router.dart';
import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'
show showLibrarySwitcher;
import 'package:vaani/router/router.dart' show Routes;
import 'package:vaani/shared/icons/abs_icons.dart' show AbsIcons;
import 'package:vaani/shared/widgets/not_implemented.dart'
show showNotImplementedToast;
class LibraryBrowserPage extends HookConsumerWidget { class LibraryBrowserPage extends HookConsumerWidget {
const LibraryBrowserPage({super.key}); const LibraryBrowserPage({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final currentLibrary = ref.watch(currentLibraryProvider).value;
// Determine the icon to use, with a fallback
final IconData libraryIconData =
AbsIcons.getIconByName(currentLibrary?.icon) ?? Icons.library_books;
// Determine the title text
final String appBarTitle = '${currentLibrary?.name ?? 'Your'} Library';
return Scaffold( return Scaffold(
// Use CustomScrollView to enable slivers appBar: AppBar(
body: CustomScrollView( title: const Text('Library'),
slivers: <Widget>[ backgroundColor: Colors.transparent,
SliverAppBar( ),
pinned: true, // a list redirecting to authors, genres, and series pages
// floating: true, // Optional: uncomment if you want floating behavior body: ListView(
// snap: children: [
// true, // Optional: uncomment if you want snapping behavior (usually with floating: true) ListTile(
leading: IconButton( title: const Text('Authors'),
icon: Icon(libraryIconData), leading: const Icon(Icons.person),
tooltip: 'Switch Library', // Helpful tooltip for users trailing: const Icon(Icons.chevron_right),
onPressed: () { onTap: () {},
showLibrarySwitcher(context, ref);
},
),
title: Text(appBarTitle),
), ),
SliverList( ListTile(
delegate: SliverChildListDelegate([ title: const Text('Genres'),
ListTile( leading: const Icon(Icons.category),
title: const Text('Authors'), trailing: const Icon(Icons.chevron_right),
leading: const Icon(Icons.person), onTap: () {},
trailing: const Icon(Icons.chevron_right), ),
onTap: () { ListTile(
showNotImplementedToast(context); title: const Text('Series'),
}, leading: const Icon(Icons.list),
), trailing: const Icon(Icons.chevron_right),
ListTile( onTap: () {},
title: const Text('Genres'), ),
leading: const Icon(Icons.category), // Downloads
trailing: const Icon(Icons.chevron_right), ListTile(
onTap: () { title: const Text('Downloads'),
showNotImplementedToast(context); leading: const Icon(Icons.download),
}, trailing: const Icon(Icons.chevron_right),
), onTap: () {
ListTile( GoRouter.of(context).pushNamed(Routes.downloads.name);
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

@ -1,36 +0,0 @@
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

@ -1,90 +0,0 @@
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

@ -1,53 +0,0 @@
// 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

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

View file

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

View file

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

View file

@ -6,167 +6,219 @@ part of 'oauth_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND String _$loginInExchangeForCodeHash() =>
// ignore_for_file: type=lint, type=warning r'e931254959d9eb8196439c6b0c884c26cbe17c2f';
@ProviderFor(OauthFlows) /// Copied from Dart SDK
final oauthFlowsProvider = OauthFlowsProvider._(); class _SystemHash {
_SystemHash._();
final class OauthFlowsProvider static int combine(int hash, int value) {
extends $NotifierProvider<OauthFlows, Map<State, Flow>> { // ignore: parameter_assignments
OauthFlowsProvider._() hash = 0x1fffffff & (hash + value);
: super( // ignore: parameter_assignments
from: null, hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
argument: null, return hash ^ (hash >> 6);
retry: null,
name: r'oauthFlowsProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
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'; static int finish(int hash) {
// ignore: parameter_assignments
abstract class _$OauthFlows extends $Notifier<Map<State, Flow>> { hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
Map<State, Flow> build(); // ignore: parameter_assignments
@$mustCallSuper hash = hash ^ (hash >> 11);
@override return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
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 /// the code returned by the server in exchange for the verifier
///
/// Copied from [loginInExchangeForCode].
@ProviderFor(loginInExchangeForCode) @ProviderFor(loginInExchangeForCode)
final loginInExchangeForCodeProvider = LoginInExchangeForCodeFamily._(); const loginInExchangeForCodeProvider = LoginInExchangeForCodeFamily();
/// the code returned by the server in exchange for the verifier /// the code returned by the server in exchange for the verifier
///
final class LoginInExchangeForCodeProvider /// Copied from [loginInExchangeForCode].
extends $FunctionalProvider<AsyncValue<String?>, String?, FutureOr<String?>> class LoginInExchangeForCodeFamily extends Family<AsyncValue<String?>> {
with $FutureModifier<String?>, $FutureProvider<String?> {
/// the code returned by the server in exchange for the verifier /// the code returned by the server in exchange for the verifier
LoginInExchangeForCodeProvider._({ ///
required LoginInExchangeForCodeFamily super.from, /// Copied from [loginInExchangeForCode].
required ({ const LoginInExchangeForCodeFamily();
State oauthState,
Code code,
ErrorResponseHandler? responseHandler,
})
super.argument,
}) : super(
retry: null,
name: r'loginInExchangeForCodeProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override /// the code returned by the server in exchange for the verifier
String debugGetCreateSourceHash() => _$loginInExchangeForCodeHash(); ///
/// Copied from [loginInExchangeForCode].
@override LoginInExchangeForCodeProvider call({
String toString() { required String oauthState,
return r'loginInExchangeForCodeProvider' required String code,
'' ErrorResponseHandler? responseHandler,
'$argument'; }) {
return LoginInExchangeForCodeProvider(
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
);
} }
@$internal
@override @override
$FutureProviderElement<String?> $createElement($ProviderPointer pointer) => LoginInExchangeForCodeProvider getProviderOverride(
$FutureProviderElement(pointer); covariant LoginInExchangeForCodeProvider provider,
) {
return call(
oauthState: provider.oauthState,
code: provider.code,
responseHandler: provider.responseHandler,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override @override
FutureOr<String?> create(Ref ref) { Iterable<ProviderOrFamily>? get dependencies => _dependencies;
final argument =
this.argument static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
as ({
State oauthState, @override
Code code, Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
ErrorResponseHandler? responseHandler, _allTransitiveDependencies;
});
return loginInExchangeForCode( @override
ref, String? get name => r'loginInExchangeForCodeProvider';
oauthState: argument.oauthState, }
code: argument.code,
responseHandler: argument.responseHandler, /// the code returned by the server in exchange for the verifier
///
/// Copied from [loginInExchangeForCode].
class LoginInExchangeForCodeProvider
extends AutoDisposeFutureProvider<String?> {
/// the code returned by the server in exchange for the verifier
///
/// Copied from [loginInExchangeForCode].
LoginInExchangeForCodeProvider({
required String oauthState,
required String code,
ErrorResponseHandler? responseHandler,
}) : this._internal(
(ref) => loginInExchangeForCode(
ref as LoginInExchangeForCodeRef,
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
),
from: loginInExchangeForCodeProvider,
name: r'loginInExchangeForCodeProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$loginInExchangeForCodeHash,
dependencies: LoginInExchangeForCodeFamily._dependencies,
allTransitiveDependencies:
LoginInExchangeForCodeFamily._allTransitiveDependencies,
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
);
LoginInExchangeForCodeProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.oauthState,
required this.code,
required this.responseHandler,
}) : super.internal();
final String oauthState;
final String code;
final ErrorResponseHandler? responseHandler;
@override
Override overrideWith(
FutureOr<String?> Function(LoginInExchangeForCodeRef provider) create,
) {
return ProviderOverride(
origin: this,
override: LoginInExchangeForCodeProvider._internal(
(ref) => create(ref as LoginInExchangeForCodeRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
),
); );
} }
@override
AutoDisposeFutureProviderElement<String?> createElement() {
return _LoginInExchangeForCodeProviderElement(this);
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is LoginInExchangeForCodeProvider && return other is LoginInExchangeForCodeProvider &&
other.argument == argument; other.oauthState == oauthState &&
other.code == code &&
other.responseHandler == responseHandler;
} }
@override @override
int get hashCode { int get hashCode {
return argument.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);
} }
} }
String _$loginInExchangeForCodeHash() => mixin LoginInExchangeForCodeRef on AutoDisposeFutureProviderRef<String?> {
r'bfc3945529048a0f536052fd5579b76457560fcd'; /// The parameter `oauthState` of this provider.
String get oauthState;
/// the code returned by the server in exchange for the verifier /// The parameter `code` of this provider.
String get code;
final class LoginInExchangeForCodeFamily extends $Family /// The parameter `responseHandler` of this provider.
with ErrorResponseHandler? get responseHandler;
$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 class _LoginInExchangeForCodeProviderElement
extends AutoDisposeFutureProviderElement<String?>
LoginInExchangeForCodeProvider call({ with LoginInExchangeForCodeRef {
required State oauthState, _LoginInExchangeForCodeProviderElement(super.provider);
required Code code,
ErrorResponseHandler? responseHandler,
}) => LoginInExchangeForCodeProvider._(
argument: (
oauthState: oauthState,
code: code,
responseHandler: responseHandler,
),
from: this,
);
@override @override
String toString() => r'loginInExchangeForCodeProvider'; String get oauthState =>
(origin as LoginInExchangeForCodeProvider).oauthState;
@override
String get code => (origin as LoginInExchangeForCodeProvider).code;
@override
ErrorResponseHandler? get responseHandler =>
(origin as LoginInExchangeForCodeProvider).responseHandler;
} }
String _$oauthFlowsHash() => r'4e278baa0bf26f2a10694ca2caadb68dd5b6156f';
/// See also [OauthFlows].
@ProviderFor(OauthFlows)
final oauthFlowsProvider =
NotifierProvider<OauthFlows, Map<State, Flow>>.internal(
OauthFlows.new,
name: r'oauthFlowsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$oauthFlowsHash,
dependencies: null,
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,7 +27,9 @@ class CallbackPage extends HookConsumerWidget {
// check if the state is in the flows // check if the state is in the flows
if (!flows.containsKey(state)) { if (!flows.containsKey(state)) {
return const _SomethingWentWrong(message: 'State not found'); return const _SomethingWentWrong(
message: 'State not found',
);
} }
// get the token // get the token
@ -43,21 +45,26 @@ class CallbackPage extends HookConsumerWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( Text('Contacting server...\nPlease wait\n\nGot:'
'Contacting server...\nPlease wait\n\nGot:' '\nState: $state\nCode: $code'),
'\nState: $state\nCode: $code',
),
loginAuthToken.when( loginAuthToken.when(
data: (authenticationToken) { data: (authenticationToken) {
if (authenticationToken == null) { if (authenticationToken == null) {
handleServerError(context, serverErrorResponse); handleServerError(
context,
serverErrorResponse,
);
return const BackToLoginButton(); return const BackToLoginButton();
} }
return Text('Token: $authenticationToken'); return Text('Token: $authenticationToken');
}, },
loading: () => const CircularProgressIndicator(), loading: () => const CircularProgressIndicator(),
error: (error, _) { error: (error, _) {
handleServerError(context, serverErrorResponse, e: error); handleServerError(
context,
serverErrorResponse,
e: error,
);
return Column( return Column(
children: [ children: [
Text('Error with OAuth flow: $error'), Text('Error with OAuth flow: $error'),
@ -74,7 +81,9 @@ class CallbackPage extends HookConsumerWidget {
} }
class BackToLoginButton extends StatelessWidget { class BackToLoginButton extends StatelessWidget {
const BackToLoginButton({super.key}); const BackToLoginButton({
super.key,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -88,7 +97,10 @@ class BackToLoginButton extends StatelessWidget {
} }
class _SomethingWentWrong extends StatelessWidget { class _SomethingWentWrong extends StatelessWidget {
const _SomethingWentWrong({this.message = 'Error with OAuth flow'}); const _SomethingWentWrong({
super.key,
this.message = 'Error with OAuth flow',
});
final String message; final String message;
@ -98,7 +110,10 @@ class _SomethingWentWrong extends StatelessWidget {
body: Center( body: Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [Text(message), const BackToLoginButton()], children: [
Text(message),
const BackToLoginButton(),
],
), ),
), ),
); );

View file

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

View file

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

View file

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

View file

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

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