Compare commits
48 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07aea41c6e | ||
|
|
8485a26f1a | ||
|
|
19046d92d9 | ||
|
|
4619657f00 | ||
|
|
db20682004 | ||
|
|
5c7be5cbe4 | ||
|
|
25c3346941 | ||
|
|
23e5d73bea | ||
|
|
bae99292a2 | ||
|
|
25be7fda03 | ||
|
|
c8767b4e1e | ||
|
|
ad0cd6e2ad | ||
|
|
2cb00c451e | ||
|
|
c3d3a3900d | ||
|
|
5f85df4d19 | ||
|
|
5986482baf | ||
|
|
37c44f1c6b | ||
|
|
b0ea9e14d2 | ||
|
|
de7c3359f7 | ||
|
|
39d051746b | ||
|
|
4ebf46d2fd | ||
|
|
4af16ac5b4 | ||
|
|
28ceca5408 | ||
|
|
4663ff9094 | ||
|
|
412c212118 | ||
|
|
edf7b2790f | ||
|
|
2fd4650bb8 | ||
|
|
e7946feca1 | ||
|
|
997d3eb5e4 | ||
|
|
77f7a7e3b5 | ||
|
|
6c50821682 | ||
|
|
247413def0 | ||
|
|
2a715f6fa8 | ||
|
|
c2cf999398 | ||
|
|
3488ae97fb | ||
|
|
11b768d41c | ||
|
|
630219dfbe | ||
|
|
781e266a5f | ||
|
|
5b896e8b09 | ||
|
|
09eafb2c28 | ||
|
|
e8903081b7 | ||
|
|
747dbdb46f | ||
|
|
e7205ed874 | ||
|
|
ff83c2cc63 | ||
|
|
758e4cdc83 | ||
|
|
fa815ae206 | ||
|
|
eda45efbce | ||
|
|
33c57da78f |
3
.fvmrc
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"flutter": "3.32.0"
|
||||||
|
}
|
||||||
46
.github/actions/flutter-setup/action.yaml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
# .github/actions/flutter-setup/action.yml
|
||||||
|
name: "Flutter Setup Composite Action"
|
||||||
|
description: "Checks out code, sets up Java/Flutter, caches, and runs pub get"
|
||||||
|
|
||||||
|
# Define inputs for customization (optional, but good practice)
|
||||||
|
inputs:
|
||||||
|
flutter-channel:
|
||||||
|
description: "Flutter channel to use (stable, beta, dev, master)"
|
||||||
|
required: false
|
||||||
|
default: "stable"
|
||||||
|
java-version:
|
||||||
|
description: "Java version to set up"
|
||||||
|
required: false
|
||||||
|
default: "17"
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: "composite" # Specify this is a composite action
|
||||||
|
steps:
|
||||||
|
- name: Set up Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: "temurin"
|
||||||
|
java-version: ${{ inputs.java-version }}
|
||||||
|
|
||||||
|
- name: Set up Flutter SDK
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: ${{ inputs.flutter-channel }}
|
||||||
|
flutter-version-file: pubspec.yaml
|
||||||
|
cache: true # Cache Flutter SDK itself
|
||||||
|
|
||||||
|
- name: Cache Flutter dependencies
|
||||||
|
id: cache-pub
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ env.FLUTTER_HOME }}/.pub-cache
|
||||||
|
key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-flutter-pub-
|
||||||
|
|
||||||
|
- name: Get Flutter dependencies
|
||||||
|
run: flutter pub get
|
||||||
|
# Use shell: bash for potential cross-platform compatibility in complex commands
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# Add other common setup steps if needed
|
||||||
8
.github/release-drafter.yml
vendored
|
|
@ -40,7 +40,7 @@ template: |
|
||||||
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
|
**Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
|
||||||
|
|
||||||
exclude-labels:
|
exclude-labels:
|
||||||
- "skip-changelog"
|
- "skip changelog"
|
||||||
|
|
||||||
exclude-contributors:
|
exclude-contributors:
|
||||||
- "Dr-Blank"
|
- "Dr-Blank"
|
||||||
|
|
@ -55,15 +55,15 @@ autolabeler:
|
||||||
branch:
|
branch:
|
||||||
- '/feature\/.+/'
|
- '/feature\/.+/'
|
||||||
title:
|
title:
|
||||||
- "/feat(ure)?/i"
|
- "/^feat(ure)?/i"
|
||||||
body:
|
body:
|
||||||
- "/JIRA-[0-9]{1,4}/"
|
- "/JIRA-[0-9]{1,4}/"
|
||||||
- label: "chore"
|
- label: "chore"
|
||||||
title:
|
title:
|
||||||
- "/chore/i"
|
- "/^chore\b/i"
|
||||||
- label: "ui"
|
- label: "ui"
|
||||||
title:
|
title:
|
||||||
- "/^ui\b/i"
|
- "/^ui\b/i"
|
||||||
- label: "refactor"
|
- label: "refactor"
|
||||||
title:
|
title:
|
||||||
- "/refactor/i"
|
- "/^refactor/i"
|
||||||
|
|
|
||||||
218
.github/workflows/flutter-ci.yaml
vendored
Normal file
|
|
@ -0,0 +1,218 @@
|
||||||
|
name: Flutter CI & Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
tags: ["v*.*.*"]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
|
||||||
|
- name: Setup Flutter Environment
|
||||||
|
uses: ./.github/actions/flutter-setup # Path to the composite action directory
|
||||||
|
# Pass inputs if needed (optional, using defaults here)
|
||||||
|
# with:
|
||||||
|
# flutter-channel: 'stable'
|
||||||
|
# java-version: '17'
|
||||||
|
# Debug: Echo current directory contents
|
||||||
|
- name: List root directory contents
|
||||||
|
run: |
|
||||||
|
pwd
|
||||||
|
ls -la
|
||||||
|
|
||||||
|
# Debug: Recursive directory structure
|
||||||
|
- name: Show full directory structure
|
||||||
|
run: |
|
||||||
|
echo "Full directory structure:"
|
||||||
|
tree -L 3
|
||||||
|
|
||||||
|
# Debug: Submodule status and details
|
||||||
|
- name: Check submodule status
|
||||||
|
run: |
|
||||||
|
echo "Submodule status:"
|
||||||
|
git submodule status
|
||||||
|
|
||||||
|
echo "\nSubmodule details:"
|
||||||
|
git submodule foreach 'echo $path: && pwd && ls -la'
|
||||||
|
|
||||||
|
# - name: Run static analysis
|
||||||
|
# run: flutter analyze
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: |
|
||||||
|
dart format -o none --set-exit-if-changed lib/
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: flutter test
|
||||||
|
|
||||||
|
build_android:
|
||||||
|
name: Build Android APKs
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Setup Flutter Environment
|
||||||
|
uses: ./.github/actions/flutter-setup # Path to the composite action directory
|
||||||
|
with:
|
||||||
|
flutter-channel: stable
|
||||||
|
java-version: 17
|
||||||
|
|
||||||
|
- name: Accept Android SDK Licenses
|
||||||
|
run: |
|
||||||
|
yes | sudo $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses
|
||||||
|
|
||||||
|
- name: Decode android/upload.jks
|
||||||
|
run: echo "${{ secrets.UPLOAD_KEYSTORE_JKS }}" | base64 --decode > android/upload.jks
|
||||||
|
|
||||||
|
- name: Decode android/key.properties
|
||||||
|
run: echo "${{ secrets.KEY_PROPERTIES }}" | base64 --decode > android/key.properties
|
||||||
|
|
||||||
|
- name: Build APKs
|
||||||
|
run: flutter build apk --release --split-per-abi
|
||||||
|
|
||||||
|
- name: Build Universal APK
|
||||||
|
run: flutter build apk --release
|
||||||
|
|
||||||
|
- name: Rename Universal APK
|
||||||
|
run: mv build/app/outputs/flutter-apk/{app-release,app-universal-release}.apk
|
||||||
|
|
||||||
|
- name: Build App Bundle
|
||||||
|
run: flutter build appbundle --release
|
||||||
|
|
||||||
|
- name: Upload Android APK Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: android-release-artifacts
|
||||||
|
path: |
|
||||||
|
build/app/outputs/flutter-apk/*-release*.apk
|
||||||
|
build/app/outputs/bundle/release/*.aab
|
||||||
|
|
||||||
|
build_linux:
|
||||||
|
needs: test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Setup Flutter Environment
|
||||||
|
uses: ./.github/actions/flutter-setup # Path to the composite action directory
|
||||||
|
|
||||||
|
- name: Install Linux dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev locate libfuse2
|
||||||
|
# Download and install appimagetool
|
||||||
|
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
|
||||||
|
chmod +x appimagetool-x86_64.AppImage
|
||||||
|
sudo mv appimagetool-x86_64.AppImage /usr/local/bin/appimagetool
|
||||||
|
shell: bash
|
||||||
|
- name: setup fastforge
|
||||||
|
run: |
|
||||||
|
dart pub global activate fastforge
|
||||||
|
- name: Build Linux AppImage and deb
|
||||||
|
run: fastforge package --platform linux --targets deb,appimage
|
||||||
|
|
||||||
|
- name: Rename Linux Artifacts
|
||||||
|
run: |
|
||||||
|
# Find and rename .deb file
|
||||||
|
DEB_FILE=$(find dist/ -name "*.deb" -type f)
|
||||||
|
if [ -n "$DEB_FILE" ]; then
|
||||||
|
mv "$DEB_FILE" dist/vaani-linux-amd64.deb
|
||||||
|
echo "Renamed DEB: $DEB_FILE to dist/vaani-linux-amd64.deb"
|
||||||
|
else
|
||||||
|
echo "Error: .deb file not found in dist/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find and rename .AppImage file
|
||||||
|
APPIMAGE_FILE=$(find dist/ -name "*.AppImage" -type f)
|
||||||
|
if [ -n "$APPIMAGE_FILE" ]; then
|
||||||
|
mv "$APPIMAGE_FILE" dist/vaani-linux-amd64.AppImage
|
||||||
|
echo "Renamed AppImage: $APPIMAGE_FILE to dist/vaani-linux-amd64.AppImage"
|
||||||
|
else
|
||||||
|
echo "Error: .AppImage file not found in dist/"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload Linux Artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux-release-artifacts
|
||||||
|
path: |
|
||||||
|
dist/vaani-linux-amd64.deb
|
||||||
|
dist/vaani-linux-amd64.AppImage
|
||||||
|
|
||||||
|
# Job 4: Create GitHub Release (NEW - runs only on tag pushes)
|
||||||
|
create_release:
|
||||||
|
name: Create GitHub Release
|
||||||
|
needs: [build_android, build_linux] # Depends on successful builds
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write # Need write access to create release
|
||||||
|
# <<< CONDITION: Only run this job if the trigger was a tag starting with 'v'
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# No checkout needed if only downloading artifacts and using context variables
|
||||||
|
# - name: Checkout repository
|
||||||
|
# uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Download artifacts created earlier IN THIS SAME WORKFLOW RUN
|
||||||
|
- name: Download Android Artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: android-release-artifacts
|
||||||
|
path: ./release-artifacts/android
|
||||||
|
|
||||||
|
- name: Download Linux Artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux-release-artifacts
|
||||||
|
path: ./release-artifacts/linux
|
||||||
|
|
||||||
|
- name: List downloaded files (for debugging)
|
||||||
|
run: ls -R ./release-artifacts
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# Extract version info from the tag
|
||||||
|
- name: Extract Version from Tag
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||||
|
VERSION=${TAG_NAME#v}
|
||||||
|
echo "tag=${TAG_NAME}" >> $GITHUB_OUTPUT
|
||||||
|
echo "version=${VERSION}" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# Generate release notes (optional, consider its configuration for tags)
|
||||||
|
- name: Generate Release Notes
|
||||||
|
id: generate_release_notes
|
||||||
|
uses: release-drafter/release-drafter@v6
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Create the GitHub Release using downloaded artifacts
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: ncipollo/release-action@v1
|
||||||
|
with:
|
||||||
|
artifacts: "./release-artifacts/**/*" # Use downloaded artifacts
|
||||||
|
name: Release v${{ steps.version.outputs.version }}
|
||||||
|
tag: ${{ github.ref }}
|
||||||
|
body: ${{ steps.generate_release_notes.outputs.body }}
|
||||||
|
# token: ${{ secrets.GITHUB_TOKEN }} # Usually inferred
|
||||||
84
.github/workflows/flutter_release.yaml
vendored
|
|
@ -1,84 +0,0 @@
|
||||||
name: Flutter Release Workflow
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v**"
|
|
||||||
# manually trigger a release if needed
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
permissions:
|
|
||||||
# write permission is required to create a github release
|
|
||||||
contents: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Checkout shelfsdk
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: Dr-Blank/shelfsdk
|
|
||||||
path: ./shelfsdk
|
|
||||||
|
|
||||||
- name: Set Up Java
|
|
||||||
uses: actions/setup-java@v3.12.0
|
|
||||||
with:
|
|
||||||
distribution: "oracle"
|
|
||||||
java-version: "17"
|
|
||||||
|
|
||||||
- name: Set up Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: "stable"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
# - name: Run tests
|
|
||||||
# run: flutter test
|
|
||||||
|
|
||||||
- name: Decode android/upload.jks
|
|
||||||
run: echo "${{ secrets.UPLOAD_KEYSTORE_JKS }}" | base64 --decode > android/upload.jks
|
|
||||||
|
|
||||||
- name: Decode android/key.properties
|
|
||||||
run: echo "${{ secrets.KEY_PROPERTIES }}" | base64 --decode > android/key.properties
|
|
||||||
|
|
||||||
- name: Build APKs
|
|
||||||
run: flutter build apk --release --split-per-abi
|
|
||||||
|
|
||||||
- name: Build Universal APK
|
|
||||||
run: flutter build apk --release
|
|
||||||
|
|
||||||
- name: Rename Universal APK
|
|
||||||
run: mv build/app/outputs/flutter-apk/{app-release,app-release-universal}.apk
|
|
||||||
|
|
||||||
- name: Build App Bundle
|
|
||||||
run: flutter build appbundle --release
|
|
||||||
|
|
||||||
- name: version
|
|
||||||
id: version
|
|
||||||
run: |
|
|
||||||
tag=${GITHUB_REF/refs\/tags\//}
|
|
||||||
version=${tag#v}
|
|
||||||
major=${version%%.*}
|
|
||||||
echo "tag=${tag}" >> $GITHUB_OUTPUT
|
|
||||||
echo "version=${version}" >> $GITHUB_OUTPUT
|
|
||||||
echo "major=${major}" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Generate Release Notes
|
|
||||||
id: generate_release_notes
|
|
||||||
uses: release-drafter/release-drafter@v6
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
|
||||||
|
|
||||||
- name: Create GitHub Release
|
|
||||||
uses: ncipollo/release-action@v1
|
|
||||||
with:
|
|
||||||
artifacts: "build/app/outputs/flutter-apk/*-release*.apk,build/app/outputs/bundle/release/*.aab"
|
|
||||||
name: v${{ steps.version.outputs.version }}
|
|
||||||
tag: ${{ github.ref }}
|
|
||||||
body: ${{ steps.generate_release_notes.outputs.body }}
|
|
||||||
53
.github/workflows/flutter_test.yaml
vendored
|
|
@ -1,53 +0,0 @@
|
||||||
name: Flutter Test
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Decode android/upload.jks
|
|
||||||
run: echo "${{ secrets.UPLOAD_KEYSTORE_JKS }}" | base64 --decode > android/upload.jks
|
|
||||||
|
|
||||||
- name: Decode android/key.properties
|
|
||||||
run: echo "${{ secrets.KEY_PROPERTIES }}" | base64 --decode > android/key.properties
|
|
||||||
|
|
||||||
- name: Checkout shelfsdk
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
repository: Dr-Blank/shelfsdk
|
|
||||||
path: ./shelfsdk
|
|
||||||
|
|
||||||
- name: Set up Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: "stable"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: flutter pub get
|
|
||||||
|
|
||||||
- name: Run tests
|
|
||||||
run: flutter test
|
|
||||||
|
|
||||||
- name: Set Up Java
|
|
||||||
uses: actions/setup-java@v3.12.0
|
|
||||||
with:
|
|
||||||
distribution: "oracle"
|
|
||||||
java-version: "17"
|
|
||||||
|
|
||||||
- name: Build APK
|
|
||||||
run: flutter build apk --release
|
|
||||||
|
|
||||||
- name: Upload APKs
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: app-release
|
|
||||||
path: build/app/outputs/flutter-apk/*.apk
|
|
||||||
130
.github/workflows/prepare-release.yaml
vendored
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
# .github/workflows/prepare-release.yml
|
||||||
|
name: Prepare Release (using Cider)
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
bump_type:
|
||||||
|
description: "Type of version bump (patch, minor, major)"
|
||||||
|
required: true
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- patch
|
||||||
|
- minor
|
||||||
|
- major
|
||||||
|
default: "patch"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write # NEEDED to commit, push, and tag
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump_version_and_tag:
|
||||||
|
name: Bump Version and Tag using Cider
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
# Use a PAT if pushing to protected branches is restricted for GITHUB_TOKEN
|
||||||
|
token: ${{ secrets.PAT_TOKEN }} # Create PAT with repo scope
|
||||||
|
# token: ${{ secrets.GITHUB_TOKEN }} # this does not trigger other workflows
|
||||||
|
|
||||||
|
# Setup Flutter/Dart environment needed to run dart pub global activate
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable" # Or match your project's channel
|
||||||
|
flutter-version-file: pubspec.yaml
|
||||||
|
|
||||||
|
- name: Install Cider
|
||||||
|
run: dart pub global activate cider
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
# Add pub global bin to PATH for this job
|
||||||
|
- name: Add pub global bin to PATH
|
||||||
|
run: echo "$HOME/.pub-cache/bin" >> $GITHUB_PATH
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Configure Git
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Bump version using Cider
|
||||||
|
id: bump
|
||||||
|
run: |
|
||||||
|
echo "Current version:"
|
||||||
|
grep '^version:' pubspec.yaml
|
||||||
|
|
||||||
|
# Run cider to bump version and build number
|
||||||
|
# Cider modifies pubspec.yaml in place
|
||||||
|
cider bump ${{ github.event.inputs.bump_type }} --bump-build
|
||||||
|
|
||||||
|
echo "New version (after cider bump):"
|
||||||
|
# Read the *new* version directly from the modified file
|
||||||
|
new_version_line=$(grep '^version:' pubspec.yaml)
|
||||||
|
# Extract just the version string (e.g., 1.2.3+4)
|
||||||
|
new_version=$(echo "$new_version_line" | sed 's/version: *//')
|
||||||
|
|
||||||
|
echo "$new_version_line"
|
||||||
|
echo "Extracted new version: $new_version"
|
||||||
|
|
||||||
|
if [[ -z "$new_version" ]]; then
|
||||||
|
echo "Error: Could not extract new version after cider bump."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create tag name (e.g., v1.2.3 - usually tags don't include build number)
|
||||||
|
# Extract version part before '+' for the tag
|
||||||
|
version_for_tag=$(echo "$new_version" | cut -d'+' -f1)
|
||||||
|
new_tag="v$version_for_tag"
|
||||||
|
echo "New tag: $new_tag"
|
||||||
|
|
||||||
|
# Set outputs for later steps
|
||||||
|
echo "new_version=$new_version" >> $GITHUB_OUTPUT
|
||||||
|
echo "new_tag=$new_tag" >> $GITHUB_OUTPUT
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Commit version bump
|
||||||
|
run: |
|
||||||
|
# Add pubspec.yaml. Add CHANGELOG.md if cider modifies it and you want to commit it.
|
||||||
|
git add pubspec.yaml
|
||||||
|
# git add CHANGELOG.md # Uncomment if needed
|
||||||
|
|
||||||
|
# Check if there are changes to commit
|
||||||
|
if git diff --staged --quiet; then
|
||||||
|
echo "No changes detected in pubspec.yaml (or CHANGELOG.md) to commit."
|
||||||
|
else
|
||||||
|
# Use the version *without* build number for the commit message usually
|
||||||
|
git commit -m "chore(release): bump version to ${{ steps.bump.outputs.new_tag }}"
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Create Git tag
|
||||||
|
# Only run if the commit step actually committed something (check git status)
|
||||||
|
# or simply run always, it won't hurt if the commit was skipped
|
||||||
|
run: |
|
||||||
|
git tag ${{ steps.bump.outputs.new_tag }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Push changes and tag
|
||||||
|
run: |
|
||||||
|
# Push the commit first (e.g., to main branch - adjust if needed)
|
||||||
|
# Handle potential conflicts if main changed since checkout? (More advanced setup)
|
||||||
|
# Check if there are commits to push before pushing branch
|
||||||
|
if ! git diff --quiet HEAD^ HEAD; then
|
||||||
|
echo "Pushing commit to main..."
|
||||||
|
git push origin HEAD:main
|
||||||
|
else
|
||||||
|
echo "No new commits to push to main."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Always push the tag
|
||||||
|
echo "Pushing tag ${{ steps.bump.outputs.new_tag }}..."
|
||||||
|
git push origin ${{ steps.bump.outputs.new_tag }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Output New Tag
|
||||||
|
run: echo "Successfully tagged release ${{ steps.bump.outputs.new_tag }}"
|
||||||
9
.gitignore
vendored
|
|
@ -30,6 +30,7 @@ migrate_working_dir/
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
|
dist/
|
||||||
|
|
||||||
# Symbolication related
|
# Symbolication related
|
||||||
app.*.symbols
|
app.*.symbols
|
||||||
|
|
@ -41,6 +42,10 @@ app.*.map.json
|
||||||
/android/app/debug
|
/android/app/debug
|
||||||
/android/app/profile
|
/android/app/profile
|
||||||
/android/app/release
|
/android/app/release
|
||||||
|
/android/app/.cxx/
|
||||||
|
|
||||||
# separate git repo for api sdk
|
# secret keys
|
||||||
/shelfsdk
|
/secrets
|
||||||
|
|
||||||
|
# FVM Version Cache
|
||||||
|
.fvm/
|
||||||
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "shelfsdk"]
|
||||||
|
path = shelfsdk
|
||||||
|
url = https://github.com/Dr-Blank/shelfsdk
|
||||||
1
.vscode/launch.json
vendored
|
|
@ -7,6 +7,7 @@
|
||||||
{
|
{
|
||||||
"name": "vaani",
|
"name": "vaani",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
|
"program": "lib/main.dart",
|
||||||
"type": "dart"
|
"type": "dart"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
28
.vscode/settings.json
vendored
|
|
@ -1,27 +1,35 @@
|
||||||
{
|
{
|
||||||
"workbench.colorCustomizations": {
|
"cmake.configureOnOpen": false,
|
||||||
"activityBar.background": "#5A1021",
|
|
||||||
"titleBar.activeBackground": "#7E162E",
|
|
||||||
"titleBar.activeForeground": "#FEFBFC"
|
|
||||||
},
|
|
||||||
"files.exclude": {
|
|
||||||
"**/*.freezed.dart": true,
|
|
||||||
"**/*.g.dart": true
|
|
||||||
},
|
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"audioplayers",
|
"audioplayers",
|
||||||
"autolabeler",
|
"autolabeler",
|
||||||
"Autovalidate",
|
"Autovalidate",
|
||||||
|
"Checkmark",
|
||||||
|
"Debounceable",
|
||||||
"deeplinking",
|
"deeplinking",
|
||||||
"fullscreen",
|
"fullscreen",
|
||||||
"Lerp",
|
"Lerp",
|
||||||
"miniplayer",
|
"miniplayer",
|
||||||
"mocktail",
|
"mocktail",
|
||||||
|
"nodename",
|
||||||
|
"numberpicker",
|
||||||
"riverpod",
|
"riverpod",
|
||||||
|
"Schyler",
|
||||||
"shelfsdk",
|
"shelfsdk",
|
||||||
|
"sysname",
|
||||||
"tapable",
|
"tapable",
|
||||||
"unfocus",
|
"unfocus",
|
||||||
|
"utsname",
|
||||||
"Vaani"
|
"Vaani"
|
||||||
],
|
],
|
||||||
"cmake.configureOnOpen": false
|
"dart.flutterSdkPath": ".fvm/versions/3.32.0",
|
||||||
|
"files.exclude": {
|
||||||
|
"**/*.freezed.dart": true,
|
||||||
|
"**/*.g.dart": true
|
||||||
|
},
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"activityBar.background": "#5A1021",
|
||||||
|
"titleBar.activeBackground": "#7E162E",
|
||||||
|
"titleBar.activeForeground": "#FEFBFC"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
181
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
# Contributing to Vaani
|
||||||
|
|
||||||
|
## Welcome Contributors! 🚀
|
||||||
|
|
||||||
|
We appreciate your interest in contributing to Vaani. This guide will help you navigate the contribution process effectively.
|
||||||
|
|
||||||
|
## How to Contribute
|
||||||
|
|
||||||
|
### Reporting Bugs 🐞
|
||||||
|
|
||||||
|
1. **Check Existing Issues**:
|
||||||
|
- Search through the [GitHub Issues](https://github.com/Dr-Blank/Vaani/issues)
|
||||||
|
|
||||||
|
2. **Create a Detailed Bug Report**:
|
||||||
|
- Provide:
|
||||||
|
* Exact steps to reproduce
|
||||||
|
* Relevant error logs or screenshots
|
||||||
|
|
||||||
|
### Submodule Contribution Workflow 🧩
|
||||||
|
|
||||||
|
#### Understanding Vaani's Submodule Structure
|
||||||
|
|
||||||
|
Vaani uses Git submodules to manage interconnected components. This means each submodule is a separate Git repository nested within the main project.
|
||||||
|
|
||||||
|
#### Working with Submodules
|
||||||
|
|
||||||
|
1. **Identifying Submodules**:
|
||||||
|
- List all submodules in the project
|
||||||
|
```bash
|
||||||
|
git submodule status
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Initializing Submodules**:
|
||||||
|
```bash
|
||||||
|
# Ensure all submodules are initialized
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Contributing to a Specific Submodule**:
|
||||||
|
|
||||||
|
a. **Navigate to Submodule Directory**:
|
||||||
|
```bash
|
||||||
|
cd path/to/submodule
|
||||||
|
```
|
||||||
|
|
||||||
|
b. **Create a Separate Branch**:
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/your-submodule-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
c. **Make and Commit Changes**:
|
||||||
|
```bash
|
||||||
|
# Stage changes
|
||||||
|
git add .
|
||||||
|
|
||||||
|
# Commit with descriptive message
|
||||||
|
git commit -m "feat(submodule): describe specific change"
|
||||||
|
```
|
||||||
|
|
||||||
|
d. **Push Submodule Changes**:
|
||||||
|
```bash
|
||||||
|
git push origin feature/your-submodule-feature
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Updating Submodule References**:
|
||||||
|
After making changes to a submodule:
|
||||||
|
```bash
|
||||||
|
# From the main repository root
|
||||||
|
git add path/to/submodule
|
||||||
|
git commit -m "Update submodule reference to latest changes"
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Pulling Latest Submodule Changes**:
|
||||||
|
```bash
|
||||||
|
# Update all submodules
|
||||||
|
git submodule update --recursive --remote
|
||||||
|
|
||||||
|
# Or update a specific submodule
|
||||||
|
git submodule update --remote path/to/specific/submodule
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Submodule Contribution Best Practices
|
||||||
|
|
||||||
|
- Always work in a feature branch within the submodule
|
||||||
|
- Ensure submodule changes do not break the main application
|
||||||
|
- Write tests for submodule-specific changes
|
||||||
|
- Update documentation if the submodule's interface changes
|
||||||
|
- Create a pull request for the submodule first, then update the main project's submodule reference
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
|
||||||
|
#### Setting Up the Development Environment
|
||||||
|
|
||||||
|
1. **Prerequisites**:
|
||||||
|
- [Git](https://git-scm.com/)
|
||||||
|
- [Flutter SDK](https://flutter.dev/)
|
||||||
|
- Recommended IDE: [VS Code](https://code.visualstudio.com/)
|
||||||
|
|
||||||
|
2. **Repository Setup**:
|
||||||
|
|
||||||
|
1. [Fork the repo](https://github.com/Dr-Blank/Vaani/fork)
|
||||||
|
1. Clone the forked repository to your local machine
|
||||||
|
```bash
|
||||||
|
# Fork the main repository on GitHub
|
||||||
|
git clone --recursive https://github.com/[YOUR_USERNAME]/Vaani.git
|
||||||
|
cd Vaani
|
||||||
|
|
||||||
|
# Initialize and update submodules
|
||||||
|
git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Install dependencies for the main app and submodules
|
||||||
|
flutter pub get
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Coding Standards
|
||||||
|
|
||||||
|
1. **Code Style**:
|
||||||
|
- Follow [Flutter's style guide](https://dart.dev/guides/language/effective-dart/style)
|
||||||
|
- Use `dart format` and `flutter analyze`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dart format .
|
||||||
|
flutter analyze
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Testing**:
|
||||||
|
- Write unit and widget tests
|
||||||
|
- Ensure tests pass for both the main app and submodules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pull Request Process
|
||||||
|
|
||||||
|
1. **Branch Naming**:
|
||||||
|
- Use descriptive branch names
|
||||||
|
- Prefix with feature/, bugfix/, or docs/
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b feature/add-accessibility-support
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Commit Messages**:
|
||||||
|
- Use clear, concise descriptions
|
||||||
|
- Reference issue numbers when applicable
|
||||||
|
- Follow conventional commits format:
|
||||||
|
`<type>(scope): <description>`
|
||||||
|
|
||||||
|
3. **Pull Request Guidelines**:
|
||||||
|
- Clearly describe the purpose of your changes
|
||||||
|
- Include screenshots for visual changes
|
||||||
|
- Specify if changes affect specific submodules
|
||||||
|
- Ensure all CI checks pass
|
||||||
|
|
||||||
|
### Signing the app
|
||||||
|
|
||||||
|
once the keystore is created, you can sign the app with the keystore.
|
||||||
|
|
||||||
|
but for github action you need to make a base64 encoded string of the keystore.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# convert keystore to base64
|
||||||
|
cat android/key.properties | base64 > key.base64
|
||||||
|
|
||||||
|
# convert keystore to base64
|
||||||
|
cat android/upload.jks | base64 > keystore.base64
|
||||||
|
```
|
||||||
|
|
||||||
|
## Communication
|
||||||
|
|
||||||
|
* [Open an Issue](https://github.com/Dr-Blank/Vaani/issues)
|
||||||
|
* [Discussion Forum](https://github.com/Dr-Blank/Vaani/discussions)
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
* Be respectful and inclusive
|
||||||
|
* Constructive feedback is welcome
|
||||||
|
* Collaborate and support fellow contributors
|
||||||
|
|
||||||
|
Happy Contributing! 🌟
|
||||||
3
Gemfile
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
gem "fastlane"
|
||||||
221
Gemfile.lock
Normal file
|
|
@ -0,0 +1,221 @@
|
||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
CFPropertyList (3.0.7)
|
||||||
|
base64
|
||||||
|
nkf
|
||||||
|
rexml
|
||||||
|
addressable (2.8.7)
|
||||||
|
public_suffix (>= 2.0.2, < 7.0)
|
||||||
|
artifactory (3.0.17)
|
||||||
|
atomos (0.1.3)
|
||||||
|
aws-eventstream (1.3.0)
|
||||||
|
aws-partitions (1.1018.0)
|
||||||
|
aws-sdk-core (3.214.0)
|
||||||
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
|
aws-sigv4 (~> 1.9)
|
||||||
|
jmespath (~> 1, >= 1.6.1)
|
||||||
|
aws-sdk-kms (1.96.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sdk-s3 (1.176.0)
|
||||||
|
aws-sdk-core (~> 3, >= 3.210.0)
|
||||||
|
aws-sdk-kms (~> 1)
|
||||||
|
aws-sigv4 (~> 1.5)
|
||||||
|
aws-sigv4 (1.10.1)
|
||||||
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
|
babosa (1.0.4)
|
||||||
|
base64 (0.2.0)
|
||||||
|
claide (1.1.0)
|
||||||
|
colored (1.2)
|
||||||
|
colored2 (3.1.2)
|
||||||
|
commander (4.6.0)
|
||||||
|
highline (~> 2.0.0)
|
||||||
|
declarative (0.0.20)
|
||||||
|
digest-crc (0.6.5)
|
||||||
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
|
domain_name (0.6.20240107)
|
||||||
|
dotenv (2.8.1)
|
||||||
|
emoji_regex (3.2.3)
|
||||||
|
excon (0.112.0)
|
||||||
|
faraday (1.10.4)
|
||||||
|
faraday-em_http (~> 1.0)
|
||||||
|
faraday-em_synchrony (~> 1.0)
|
||||||
|
faraday-excon (~> 1.1)
|
||||||
|
faraday-httpclient (~> 1.0)
|
||||||
|
faraday-multipart (~> 1.0)
|
||||||
|
faraday-net_http (~> 1.0)
|
||||||
|
faraday-net_http_persistent (~> 1.0)
|
||||||
|
faraday-patron (~> 1.0)
|
||||||
|
faraday-rack (~> 1.0)
|
||||||
|
faraday-retry (~> 1.0)
|
||||||
|
ruby2_keywords (>= 0.0.4)
|
||||||
|
faraday-cookie_jar (0.0.7)
|
||||||
|
faraday (>= 0.8.0)
|
||||||
|
http-cookie (~> 1.0.0)
|
||||||
|
faraday-em_http (1.0.0)
|
||||||
|
faraday-em_synchrony (1.0.0)
|
||||||
|
faraday-excon (1.1.0)
|
||||||
|
faraday-httpclient (1.0.1)
|
||||||
|
faraday-multipart (1.0.4)
|
||||||
|
multipart-post (~> 2)
|
||||||
|
faraday-net_http (1.0.2)
|
||||||
|
faraday-net_http_persistent (1.2.0)
|
||||||
|
faraday-patron (1.0.0)
|
||||||
|
faraday-rack (1.0.0)
|
||||||
|
faraday-retry (1.0.3)
|
||||||
|
faraday_middleware (1.2.1)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
fastimage (2.3.1)
|
||||||
|
fastlane (2.225.0)
|
||||||
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
|
addressable (>= 2.8, < 3.0.0)
|
||||||
|
artifactory (~> 3.0)
|
||||||
|
aws-sdk-s3 (~> 1.0)
|
||||||
|
babosa (>= 1.0.3, < 2.0.0)
|
||||||
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
|
colored (~> 1.2)
|
||||||
|
commander (~> 4.6)
|
||||||
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
|
excon (>= 0.71.0, < 1.0.0)
|
||||||
|
faraday (~> 1.0)
|
||||||
|
faraday-cookie_jar (~> 0.0.6)
|
||||||
|
faraday_middleware (~> 1.0)
|
||||||
|
fastimage (>= 2.1.0, < 3.0.0)
|
||||||
|
fastlane-sirp (>= 1.0.0)
|
||||||
|
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||||
|
google-apis-androidpublisher_v3 (~> 0.3)
|
||||||
|
google-apis-playcustomapp_v1 (~> 0.1)
|
||||||
|
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||||
|
google-cloud-storage (~> 1.31)
|
||||||
|
highline (~> 2.0)
|
||||||
|
http-cookie (~> 1.0.5)
|
||||||
|
json (< 3.0.0)
|
||||||
|
jwt (>= 2.1.0, < 3)
|
||||||
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
|
naturally (~> 2.2)
|
||||||
|
optparse (>= 0.1.1, < 1.0.0)
|
||||||
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
|
rubyzip (>= 2.0.0, < 3.0.0)
|
||||||
|
security (= 0.1.5)
|
||||||
|
simctl (~> 1.6.3)
|
||||||
|
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||||
|
terminal-table (~> 3)
|
||||||
|
tty-screen (>= 0.6.3, < 1.0.0)
|
||||||
|
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||||
|
word_wrap (~> 1.0.0)
|
||||||
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
|
xcpretty (~> 0.3.0)
|
||||||
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
|
fastlane-sirp (1.0.0)
|
||||||
|
sysrandom (~> 1.0)
|
||||||
|
gh_inspector (1.1.3)
|
||||||
|
google-apis-androidpublisher_v3 (0.54.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-core (0.11.3)
|
||||||
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
httpclient (>= 2.8.1, < 3.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
representable (~> 3.0)
|
||||||
|
retriable (>= 2.0, < 4.a)
|
||||||
|
rexml
|
||||||
|
google-apis-iamcredentials_v1 (0.17.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-playcustomapp_v1 (0.13.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-apis-storage_v1 (0.31.0)
|
||||||
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
|
google-cloud-core (1.7.1)
|
||||||
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
|
google-cloud-errors (~> 1.0)
|
||||||
|
google-cloud-env (1.6.0)
|
||||||
|
faraday (>= 0.17.3, < 3.0)
|
||||||
|
google-cloud-errors (1.4.0)
|
||||||
|
google-cloud-storage (1.47.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
digest-crc (~> 0.4)
|
||||||
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
|
google-apis-storage_v1 (~> 0.31.0)
|
||||||
|
google-cloud-core (~> 1.6)
|
||||||
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
|
mini_mime (~> 1.0)
|
||||||
|
googleauth (1.8.1)
|
||||||
|
faraday (>= 0.17.3, < 3.a)
|
||||||
|
jwt (>= 1.4, < 3.0)
|
||||||
|
multi_json (~> 1.11)
|
||||||
|
os (>= 0.9, < 2.0)
|
||||||
|
signet (>= 0.16, < 2.a)
|
||||||
|
highline (2.0.3)
|
||||||
|
http-cookie (1.0.8)
|
||||||
|
domain_name (~> 0.5)
|
||||||
|
httpclient (2.8.3)
|
||||||
|
jmespath (1.6.2)
|
||||||
|
json (2.9.0)
|
||||||
|
jwt (2.9.3)
|
||||||
|
base64
|
||||||
|
mini_magick (4.13.2)
|
||||||
|
mini_mime (1.1.5)
|
||||||
|
multi_json (1.15.0)
|
||||||
|
multipart-post (2.4.1)
|
||||||
|
nanaimo (0.4.0)
|
||||||
|
naturally (2.2.1)
|
||||||
|
nkf (0.2.0)
|
||||||
|
optparse (0.6.0)
|
||||||
|
os (1.1.4)
|
||||||
|
plist (3.7.1)
|
||||||
|
public_suffix (6.0.1)
|
||||||
|
rake (13.2.1)
|
||||||
|
representable (3.2.0)
|
||||||
|
declarative (< 0.1.0)
|
||||||
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
|
uber (< 0.2.0)
|
||||||
|
retriable (3.1.2)
|
||||||
|
rexml (3.3.9)
|
||||||
|
rouge (2.0.7)
|
||||||
|
ruby2_keywords (0.0.5)
|
||||||
|
rubyzip (2.3.2)
|
||||||
|
security (0.1.5)
|
||||||
|
signet (0.19.0)
|
||||||
|
addressable (~> 2.8)
|
||||||
|
faraday (>= 0.17.5, < 3.a)
|
||||||
|
jwt (>= 1.5, < 3.0)
|
||||||
|
multi_json (~> 1.10)
|
||||||
|
simctl (1.6.10)
|
||||||
|
CFPropertyList
|
||||||
|
naturally
|
||||||
|
sysrandom (1.0.5)
|
||||||
|
terminal-notifier (2.0.0)
|
||||||
|
terminal-table (3.0.2)
|
||||||
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
|
trailblazer-option (0.1.2)
|
||||||
|
tty-cursor (0.7.1)
|
||||||
|
tty-screen (0.8.2)
|
||||||
|
tty-spinner (0.9.3)
|
||||||
|
tty-cursor (~> 0.7)
|
||||||
|
uber (0.1.0)
|
||||||
|
unicode-display_width (2.6.0)
|
||||||
|
word_wrap (1.0.0)
|
||||||
|
xcodeproj (1.27.0)
|
||||||
|
CFPropertyList (>= 2.3.3, < 4.0)
|
||||||
|
atomos (~> 0.1.3)
|
||||||
|
claide (>= 1.0.2, < 2.0)
|
||||||
|
colored2 (~> 3.1)
|
||||||
|
nanaimo (~> 0.4.0)
|
||||||
|
rexml (>= 3.3.6, < 4.0)
|
||||||
|
xcpretty (0.3.0)
|
||||||
|
rouge (~> 2.0.7)
|
||||||
|
xcpretty-travis-formatter (1.0.1)
|
||||||
|
xcpretty (~> 0.2, >= 0.0.7)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
x64-mingw-ucrt
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
fastlane
|
||||||
|
|
||||||
|
BUNDLED WITH
|
||||||
|
2.5.23
|
||||||
20
README.md
|
|
@ -18,19 +18,25 @@ Client for [Audiobookshelf](https://github.com/advplyr/audiobookshelf) server ma
|
||||||
### Android
|
### Android
|
||||||
|
|
||||||
<!-- a github image with link to releases for download -->
|
<!-- a github image with link to releases for download -->
|
||||||
[<img src="https://github.com/NeoApplications/Neo-Backup/raw/main/badge_github.png" alt="Get it on GitHub" height="80">](https://github.com/Dr-Blank/Vaani/releases/latest/download/app-release-universal.apk) [<img src="https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png" alt="Get it on Obtainium" height="80">](http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/Dr-Blank/Vaani)
|
[<img src="https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png" alt="Get it on Obtainium" height="80">](http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/Dr-Blank/Vaani)
|
||||||
|
[<img src="https://play.google.com/intl/en_us/badges/static/images/badges/en_badge_web_generic.png" alt="Get it on Google Play" height="80">](https://play.google.com/store/apps/details?id=dr.blank.vaani)
|
||||||
|
[<img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" alt="Get it on IzzyOnDroid" height="80">](https://apt.izzysoft.de/fdroid/index/apk/dr.blank.vaani)
|
||||||
|
[<img src="https://github.com/NeoApplications/Neo-Backup/raw/main/badge_github.png" alt="Get it on GitHub" height="80">](https://github.com/Dr-Blank/Vaani/releases/latest/download/app-universal-release.apk)
|
||||||
|
|
||||||
Playstore App is in closed testing. To join testing
|
*<small>Play Store version is paid if you want to support the development.</small>*
|
||||||
1. [Join the Google Group](https://groups.google.com/g/vaani-app)
|
|
||||||
2. [Join on Android](https://play.google.com/store/apps/details?id=dr.blank.vaani) Or [Join on Web](https://play.google.com/apps/testing/dr.blank.vaani)
|
### Linux
|
||||||
|
|
||||||
|
[<img src="https://img.shields.io/badge/.deb-Download-blue" alt="Download Linux (.deb)" height="30">](https://github.com/Dr-Blank/Vaani/releases/latest/download/vaani-linux-amd64.deb)
|
||||||
|
[<img src="https://img.shields.io/badge/AppImage-Download-blue" alt="Download Linux (AppImage)" height="30">](https://github.com/Dr-Blank/Vaani/releases/latest/download/vaani-linux-amd64.AppImage)
|
||||||
|
|
||||||
## Screencaps
|
## Screencaps
|
||||||
|
|
||||||
https://github.com/user-attachments/assets/2ac9ace2-4a3c-40fc-adde-55914e4cf62d
|
https://github.com/user-attachments/assets/2ac9ace2-4a3c-40fc-adde-55914e4cf62d
|
||||||
|
|
||||||
|<img src="images/screenshots/android/home.jpg" width="200" />|<img src="images/screenshots/android/bookview.jpg" width="200" />|<img src="images/screenshots/android/player.jpg" width="200" />|
|
| <img src="images/screenshots/android/home.jpg" width="200" /> | <img src="images/screenshots/android/bookview.jpg" width="200" /> | <img src="images/screenshots/android/player.jpg" width="200" /> |
|
||||||
|:---:|:---:|:---:|
|
| :-----------------------------------------------------------: | :---------------------------------------------------------------: | :-------------------------------------------------------------: |
|
||||||
|Home|Book View|Player|
|
| Home | Book View | Player |
|
||||||
|
|
||||||
Currently, the app is in development and is not ready for production use.
|
Currently, the app is in development and is not ready for production use.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,10 @@ linter:
|
||||||
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
|
||||||
require_trailing_commas: true
|
require_trailing_commas: true
|
||||||
analyzer:
|
analyzer:
|
||||||
|
exclude:
|
||||||
|
- '**.freezed.dart'
|
||||||
|
- '**.g.dart'
|
||||||
|
- '**.gr.dart'
|
||||||
errors:
|
errors:
|
||||||
invalid_annotation_target: ignore
|
invalid_annotation_target: ignore
|
||||||
plugins:
|
plugins:
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,11 @@ if (keystorePropertiesFile.exists()) {
|
||||||
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
|
||||||
|
|
@ -46,6 +50,15 @@ 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"
|
||||||
|
|
@ -80,3 +93,11 @@ flutter {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {}
|
dependencies {}
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/78626580/how-to-resolve-app-execution-failure-due-to-androidx-corecore1-15-0-alpha
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy {
|
||||||
|
force "androidx.core:core:1.13.1"
|
||||||
|
force "androidx.core:core-ktx:1.13.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,10 +7,13 @@
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<application
|
<application
|
||||||
android:label="Vaani"
|
android:label="Vaani"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher">
|
||||||
<!-- android:name=".MainActivity" -->
|
<!-- android:name=".MainActivity" -->
|
||||||
<activity
|
<activity
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ pluginManagement {
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
|
||||||
id "com.android.application" version "7.3.0" apply false
|
id "com.android.application" version '8.10.0' apply false
|
||||||
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
|
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
BIN
assets/fonts/AbsIcons.ttf
Normal file
8
distribute_options.yaml
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
output: dist/
|
||||||
|
releases:
|
||||||
|
- name: dev
|
||||||
|
jobs:
|
||||||
|
- name: release-dev-linux-deb
|
||||||
|
package:
|
||||||
|
platform: linux
|
||||||
|
target: deb
|
||||||
8
docs/images_and_logos.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
this is how i converted my png to svg
|
||||||
|
|
||||||
|
`convert -background White vaani_logo.png vaani_logo.pbm`
|
||||||
|
|
||||||
|
|
||||||
|
`potrace -b svg -i vaani_logo.pbm -o vaani_logo.svg`
|
||||||
|
|
||||||
|
`-i` flag was needed so that it took white as the svgs and black as background
|
||||||
45
docs/linux_build_guide.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Linux Build Guide
|
||||||
|
|
||||||
|
## Determining Package Size
|
||||||
|
To determine the installed size for your Linux package configuration, you can use the following script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Build the Linux app
|
||||||
|
flutter build linux
|
||||||
|
|
||||||
|
# Get size in KB and add 17% buffer for runtime dependencies
|
||||||
|
SIZE_KB=$(du -sk build/linux/x64/release/bundle | cut -f1)
|
||||||
|
BUFFER_SIZE_KB=$(($SIZE_KB + ($SIZE_KB * 17 / 100)))
|
||||||
|
|
||||||
|
echo "Actual bundle size: $SIZE_KB KB"
|
||||||
|
echo "Recommended installed_size (with 17% buffer): $BUFFER_SIZE_KB KB"
|
||||||
|
```
|
||||||
|
|
||||||
|
Save this as `get_package_size.sh` in your project root and make it executable:
|
||||||
|
```bash
|
||||||
|
chmod +x get_package_size.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
1. Run the script:
|
||||||
|
```bash
|
||||||
|
./get_package_size.sh
|
||||||
|
```
|
||||||
|
2. Use the output value for `installed_size` in your `linux/packaging/deb/make_config.yaml` file:
|
||||||
|
```yaml
|
||||||
|
installed_size: 75700 # Replace with the value from the script
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why add a buffer?
|
||||||
|
The 17% buffer is added to account for:
|
||||||
|
- Runtime dependencies
|
||||||
|
- Future updates
|
||||||
|
- Potential additional assets
|
||||||
|
- Prevent installation issues on systems with limited space
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- The installed size should be specified in kilobytes (KB)
|
||||||
|
- Always round up the buffer size to be safe
|
||||||
|
- Re-run this script after significant changes to your app (new assets, dependencies, etc.)
|
||||||
2
docs/linux_deeplink.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
to test deeplink
|
||||||
|
`xdg-open vaani://test?code=123&state=abc`
|
||||||
2
fastlane/Appfile
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
json_key_file("./secrets/play-store-credentials.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one
|
||||||
|
package_name("dr.blank.vaani") # e.g. com.krausefx.app
|
||||||
38
fastlane/Fastfile
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# This file contains the fastlane.tools configuration
|
||||||
|
# You can find the documentation at https://docs.fastlane.tools
|
||||||
|
#
|
||||||
|
# For a list of all available actions, check out
|
||||||
|
#
|
||||||
|
# https://docs.fastlane.tools/actions
|
||||||
|
#
|
||||||
|
# For a list of all available plugins, check out
|
||||||
|
#
|
||||||
|
# https://docs.fastlane.tools/plugins/available-plugins
|
||||||
|
#
|
||||||
|
|
||||||
|
# Uncomment the line if you want fastlane to automatically update itself
|
||||||
|
# update_fastlane
|
||||||
|
|
||||||
|
default_platform(:android)
|
||||||
|
|
||||||
|
platform :android do
|
||||||
|
desc "Runs all the tests"
|
||||||
|
lane :test do
|
||||||
|
gradle(task: "test", project_dir: 'android/')
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Submit a new Beta Build to Crashlytics Beta"
|
||||||
|
lane :beta do
|
||||||
|
gradle(task: "clean assembleRelease", project_dir: 'android/')
|
||||||
|
crashlytics
|
||||||
|
|
||||||
|
# sh "your_script.sh"
|
||||||
|
# You can also use other beta testing services here
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Deploy a new version to the Google Play"
|
||||||
|
lane :deploy do
|
||||||
|
gradle(task: "clean assembleRelease", project_dir: 'android/')
|
||||||
|
upload_to_play_store
|
||||||
|
end
|
||||||
|
end
|
||||||
48
fastlane/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
fastlane documentation
|
||||||
|
----
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
Make sure you have the latest version of the Xcode command line tools installed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xcode-select --install
|
||||||
|
```
|
||||||
|
|
||||||
|
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
|
||||||
|
|
||||||
|
# Available Actions
|
||||||
|
|
||||||
|
## Android
|
||||||
|
|
||||||
|
### android test
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android test
|
||||||
|
```
|
||||||
|
|
||||||
|
Runs all the tests
|
||||||
|
|
||||||
|
### android beta
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android beta
|
||||||
|
```
|
||||||
|
|
||||||
|
Submit a new Beta Build to Crashlytics Beta
|
||||||
|
|
||||||
|
### android deploy
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane android deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
Deploy a new version to the Google Play
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||||
|
|
||||||
|
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
|
||||||
|
|
||||||
|
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||||
10
fastlane/metadata/android/en-US/full_description.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<i>Vaani</i> is a client for your (self-hosted) <a href='https://github.com/advplyr/audiobookshelf'>Audiobookshelf</a> server.
|
||||||
|
|
||||||
|
<b>Features:</b>
|
||||||
|
|
||||||
|
- Functional Player: Speed Control, Sleep Timer, Shake to Control Player
|
||||||
|
- Save data with Offline listening and caching
|
||||||
|
- Material Design
|
||||||
|
- Extensive Settings to customize the every tiny detail
|
||||||
|
|
||||||
|
Note: you need an Audiobookshelf server setup for this app to work. Please see https://www.audiobookshelf.org/ on how to setup one if not already.
|
||||||
BIN
fastlane/metadata/android/en-US/images/featureGraphic.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
fastlane/metadata/android/en-US/images/icon.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 746 KiB |
|
After Width: | Height: | Size: 304 KiB |
|
After Width: | Height: | Size: 284 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 223 KiB |
|
After Width: | Height: | Size: 345 KiB |
1
fastlane/metadata/android/en-US/short_description.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Beautiful, Fast and Functional Audiobook Player for your Audiobookshelf server.
|
||||||
1
fastlane/metadata/android/en-US/title.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Vaani
|
||||||
0
fastlane/metadata/android/en-US/video.txt
Normal file
18
fastlane/report.xml
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="fastlane.lanes">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.0039338">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
|
<testcase classname="fastlane.lanes" name="1: test" time="10.3552991">
|
||||||
|
|
||||||
|
</testcase>
|
||||||
|
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
36
images/vaani_logo.svg
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="192.000000pt" height="192.000000pt"
|
||||||
|
viewBox="0 0 192.000000 192.000000" preserveAspectRatio="xMidYMid meet">
|
||||||
|
<metadata>
|
||||||
|
Created by potrace 1.16, written by Peter Selinger 2001-2019
|
||||||
|
</metadata>
|
||||||
|
<g transform="translate(0.000000,192.000000) scale(0.100000,-0.100000)" fill="#ffffff" stroke="none">
|
||||||
|
<path d="M602 1678 c-18 -18 -17 -689 1 -741 17 -48 30 -61 55 -53 25 8 38 57
|
||||||
|
23 85 -6 11 -11 75 -11 144 0 284 -13 548 -27 563 -18 17 -25 18 -41 2z" />
|
||||||
|
<path d="M897 1683 c-4 -3 -7 -176 -7 -383 l0 -377 23 -22 c30 -28 53 -19 67
|
||||||
|
25 10 29 9 39 -4 59 -14 21 -16 69 -16 353 0 242 -3 331 -12 340 -13 13 -41
|
||||||
|
16 -51 5z" />
|
||||||
|
<path d="M1329 1668 c-8 -15 -10 -104 -7 -311 4 -288 4 -289 27 -308 16 -13
|
||||||
|
28 -16 39 -10 13 8 15 46 13 312 -1 169 -7 311 -12 321 -13 25 -46 23 -60 -4z" />
|
||||||
|
<path d="M1035 1568 c-3 -7 -6 -123 -8 -258 l-2 -245 27 -3 c36 -4 36 -5 43
|
||||||
|
266 7 234 4 252 -37 252 -10 0 -21 -6 -23 -12z" />
|
||||||
|
<path d="M1474 1467 c-2 -7 -3 -60 -2 -118 3 -100 4 -104 26 -107 42 -6 49 10
|
||||||
|
45 105 -5 123 -8 133 -39 133 -13 0 -27 -6 -30 -13z" />
|
||||||
|
<path d="M443 1408 c-13 -23 -3 -251 13 -275 28 -45 88 -19 64 27 -6 10 -10
|
||||||
|
64 -10 120 0 56 -5 110 -10 121 -12 21 -45 25 -57 7z" />
|
||||||
|
<path d="M777 1407 c-12 -9 -16 -39 -19 -132 l-3 -120 30 0 c37 0 43 18 44
|
||||||
|
147 1 85 -1 99 -18 107 -13 7 -23 7 -34 -2z" />
|
||||||
|
<path d="M1182 1398 c-8 -8 -12 -48 -12 -115 0 -93 2 -103 21 -113 39 -21 49
|
||||||
|
2 49 111 0 75 -4 101 -16 113 -19 19 -26 20 -42 4z" />
|
||||||
|
<path d="M1505 1048 c-11 -6 -25 -14 -31 -17 -56 -30 -192 -131 -223 -164 -57
|
||||||
|
-62 -134 -166 -158 -214 -12 -23 -26 -50 -32 -60 -9 -17 -30 -66 -60 -138
|
||||||
|
l-12 -29 -21 34 c-114 189 -151 238 -243 316 -44 38 -107 95 -141 127 -33 31
|
||||||
|
-66 57 -73 57 -8 0 -30 8 -50 19 -20 10 -55 27 -79 37 -39 16 -44 16 -58 2
|
||||||
|
-29 -29 -8 -52 84 -93 51 -23 106 -57 132 -81 25 -23 65 -58 90 -78 59 -49
|
||||||
|
152 -147 186 -199 15 -23 35 -53 44 -67 10 -14 35 -63 56 -108 28 -61 45 -84
|
||||||
|
61 -88 28 -7 49 8 57 41 8 33 106 246 128 279 7 11 27 40 43 66 51 79 135 167
|
||||||
|
192 202 30 18 67 42 81 53 15 11 44 29 65 39 31 16 37 24 35 45 -3 29 -38 38
|
||||||
|
-73 19z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -2,12 +2,15 @@
|
||||||
|
|
||||||
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';
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
part 'api_provider.g.dart';
|
part 'api_provider.g.dart';
|
||||||
|
|
@ -32,7 +35,7 @@ Uri makeBaseUrl(String address) {
|
||||||
|
|
||||||
/// get the api instance for the given base url
|
/// get the api instance for the given base url
|
||||||
@riverpod
|
@riverpod
|
||||||
AudiobookshelfApi audiobookshelfApi(AudiobookshelfApiRef ref, Uri? baseUrl) {
|
AudiobookshelfApi audiobookshelfApi(Ref ref, Uri? baseUrl) {
|
||||||
// try to get the base url from app settings
|
// try to get the base url from app settings
|
||||||
final apiSettings = ref.watch(apiSettingsProvider);
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
baseUrl ??= apiSettings.activeServer?.serverUrl;
|
baseUrl ??= apiSettings.activeServer?.serverUrl;
|
||||||
|
|
@ -45,10 +48,10 @@ AudiobookshelfApi audiobookshelfApi(AudiobookshelfApiRef ref, Uri? baseUrl) {
|
||||||
///
|
///
|
||||||
/// if the user is not authenticated throw an error
|
/// if the user is not authenticated throw an error
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) {
|
AudiobookshelfApi authenticatedApi(Ref ref) {
|
||||||
final apiSettings = ref.watch(apiSettingsProvider);
|
final user = ref.watch(apiSettingsProvider.select((s) => s.activeUser));
|
||||||
final user = apiSettings.activeUser;
|
|
||||||
if (user == null) {
|
if (user == null) {
|
||||||
|
_logger.severe('No active user can not provide authenticated api');
|
||||||
throw StateError('No active user');
|
throw StateError('No active user');
|
||||||
}
|
}
|
||||||
return AudiobookshelfApi(
|
return AudiobookshelfApi(
|
||||||
|
|
@ -59,7 +62,7 @@ AudiobookshelfApi authenticatedApi(AuthenticatedApiRef ref) {
|
||||||
|
|
||||||
/// ping the server to check if it is reachable
|
/// ping the server to check if it is reachable
|
||||||
@riverpod
|
@riverpod
|
||||||
FutureOr<bool> isServerAlive(IsServerAliveRef ref, String address) async {
|
FutureOr<bool> isServerAlive(Ref ref, String address) async {
|
||||||
if (address.isEmpty) {
|
if (address.isEmpty) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +80,7 @@ FutureOr<bool> isServerAlive(IsServerAliveRef ref, String address) async {
|
||||||
/// fetch status of server
|
/// fetch status of server
|
||||||
@riverpod
|
@riverpod
|
||||||
FutureOr<ServerStatusResponse?> serverStatus(
|
FutureOr<ServerStatusResponse?> serverStatus(
|
||||||
ServerStatusRef ref,
|
Ref ref,
|
||||||
Uri baseUrl, [
|
Uri baseUrl, [
|
||||||
ResponseErrorHandler? responseErrorHandler,
|
ResponseErrorHandler? responseErrorHandler,
|
||||||
]) async {
|
]) async {
|
||||||
|
|
@ -97,17 +100,28 @@ class PersonalizedView extends _$PersonalizedView {
|
||||||
final api = ref.watch(authenticatedApiProvider);
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
final apiSettings = ref.watch(apiSettingsProvider);
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
final user = apiSettings.activeUser;
|
final user = apiSettings.activeUser;
|
||||||
|
if (user == null) {
|
||||||
|
_logger.warning('no active user');
|
||||||
|
yield [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (apiSettings.activeLibraryId == null) {
|
if (apiSettings.activeLibraryId == null) {
|
||||||
// set it to default user library by logging in and getting the library id
|
// set it to default user library by logging in and getting the library id
|
||||||
final login =
|
final login = await ref.read(loginProvider().future);
|
||||||
await api.login(username: user!.username!, password: user.password!);
|
if (login == null) {
|
||||||
|
_logger.shout('failed to login, not building personalized view');
|
||||||
|
yield [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
apiSettings.copyWith(activeLibraryId: login!.userDefaultLibraryId),
|
apiSettings.copyWith(activeLibraryId: login.userDefaultLibraryId),
|
||||||
);
|
);
|
||||||
|
yield [];
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
// try to find in cache
|
// try to find in cache
|
||||||
// final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}';
|
// final cacheKey = 'personalizedView:${apiSettings.activeLibraryId}';
|
||||||
var key = 'personalizedView:${apiSettings.activeLibraryId! + user!.id!}';
|
final key = 'personalizedView:${apiSettings.activeLibraryId! + user.id}';
|
||||||
final cachedRes = await apiResponseCacheManager.getFileFromMemory(
|
final cachedRes = await apiResponseCacheManager.getFileFromMemory(
|
||||||
key,
|
key,
|
||||||
) ??
|
) ??
|
||||||
|
|
@ -127,7 +141,7 @@ class PersonalizedView extends _$PersonalizedView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ! exagerated delay
|
// ! exaggerated delay
|
||||||
// await Future.delayed(const Duration(seconds: 2));
|
// await Future.delayed(const Duration(seconds: 2));
|
||||||
final res = await api.libraries
|
final res = await api.libraries
|
||||||
.getPersonalized(libraryId: apiSettings.activeLibraryId!);
|
.getPersonalized(libraryId: apiSettings.activeLibraryId!);
|
||||||
|
|
@ -151,6 +165,7 @@ class PersonalizedView extends _$PersonalizedView {
|
||||||
// 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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -158,7 +173,7 @@ class PersonalizedView extends _$PersonalizedView {
|
||||||
/// fetch continue listening audiobooks
|
/// fetch continue listening audiobooks
|
||||||
@riverpod
|
@riverpod
|
||||||
FutureOr<GetUserSessionsResponse> fetchContinueListening(
|
FutureOr<GetUserSessionsResponse> fetchContinueListening(
|
||||||
FetchContinueListeningRef ref,
|
Ref ref,
|
||||||
) async {
|
) async {
|
||||||
final api = ref.watch(authenticatedApiProvider);
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
final res = await api.me.getSessions();
|
final res = await api.me.getSessions();
|
||||||
|
|
@ -170,9 +185,50 @@ FutureOr<GetUserSessionsResponse> fetchContinueListening(
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
FutureOr<User> me(
|
FutureOr<User> me(
|
||||||
MeRef ref,
|
Ref ref,
|
||||||
) async {
|
) async {
|
||||||
final api = ref.watch(authenticatedApiProvider);
|
final api = ref.watch(authenticatedApiProvider);
|
||||||
final res = await api.me.getUser();
|
final errorResponseHandler = ErrorResponseHandler();
|
||||||
return res!;
|
final res = await api.me.getUser(
|
||||||
|
responseErrorHandler: errorResponseHandler.storeError,
|
||||||
|
);
|
||||||
|
if (res == null) {
|
||||||
|
_logger.severe(
|
||||||
|
'me failed, got response: ${errorResponseHandler.response.obfuscate()}',
|
||||||
|
);
|
||||||
|
throw StateError('me failed');
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
FutureOr<LoginResponse?> login(
|
||||||
|
Ref ref, {
|
||||||
|
AuthenticatedUser? user,
|
||||||
|
}) async {
|
||||||
|
if (user == null) {
|
||||||
|
// try to get the user from settings
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
user = apiSettings.activeUser;
|
||||||
|
if (user == null) {
|
||||||
|
_logger.severe('no active user to login');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_logger.fine('no user provided, using active user: ${user.obfuscate()}');
|
||||||
|
}
|
||||||
|
final api = ref.watch(audiobookshelfApiProvider(user.server.serverUrl));
|
||||||
|
api.token = user.authToken;
|
||||||
|
var errorResponseHandler = ErrorResponseHandler();
|
||||||
|
_logger.fine('logging in with authenticated api');
|
||||||
|
final res = await api.misc.authorize(
|
||||||
|
responseErrorHandler: errorResponseHandler.storeError,
|
||||||
|
);
|
||||||
|
if (res == null) {
|
||||||
|
_logger.severe(
|
||||||
|
'login failed, got response: ${errorResponseHandler.response.obfuscate()}',
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
_logger.fine('login response: ${res.obfuscate()}');
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'api_provider.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$audiobookshelfApiHash() => r'2c310ea77fea9918ccf96180a92075acd037bd95';
|
String _$audiobookshelfApiHash() => r'f23a06c404e11867a7f796877eaca99b8ff25458';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
@ -154,6 +154,8 @@ class AudiobookshelfApiProvider extends AutoDisposeProvider<AudiobookshelfApi> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin AudiobookshelfApiRef on AutoDisposeProviderRef<AudiobookshelfApi> {
|
mixin AudiobookshelfApiRef on AutoDisposeProviderRef<AudiobookshelfApi> {
|
||||||
/// The parameter `baseUrl` of this provider.
|
/// The parameter `baseUrl` of this provider.
|
||||||
Uri? get baseUrl;
|
Uri? get baseUrl;
|
||||||
|
|
@ -168,7 +170,7 @@ class _AudiobookshelfApiProviderElement
|
||||||
Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl;
|
Uri? get baseUrl => (origin as AudiobookshelfApiProvider).baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$authenticatedApiHash() => r'f555efb6eede590b5a8d60cad2e6bfc2847e2d14';
|
String _$authenticatedApiHash() => r'284be2c39823c20fb70035a136c430862c28fa27';
|
||||||
|
|
||||||
/// get the api instance for the authenticated user
|
/// get the api instance for the authenticated user
|
||||||
///
|
///
|
||||||
|
|
@ -186,8 +188,10 @@ final authenticatedApiProvider = Provider<AudiobookshelfApi>.internal(
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef AuthenticatedApiRef = ProviderRef<AudiobookshelfApi>;
|
typedef AuthenticatedApiRef = ProviderRef<AudiobookshelfApi>;
|
||||||
String _$isServerAliveHash() => r'6ff90b6e0febd2cd4a4d3a5209a59afc778cd3b6';
|
String _$isServerAliveHash() => r'bb3a53cae1eb64b8760a56864feed47b7a3f1c29';
|
||||||
|
|
||||||
/// ping the server to check if it is reachable
|
/// ping the server to check if it is reachable
|
||||||
///
|
///
|
||||||
|
|
@ -314,6 +318,8 @@ class IsServerAliveProvider extends AutoDisposeFutureProvider<bool> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin IsServerAliveRef on AutoDisposeFutureProviderRef<bool> {
|
mixin IsServerAliveRef on AutoDisposeFutureProviderRef<bool> {
|
||||||
/// The parameter `address` of this provider.
|
/// The parameter `address` of this provider.
|
||||||
String get address;
|
String get address;
|
||||||
|
|
@ -327,7 +333,7 @@ class _IsServerAliveProviderElement
|
||||||
String get address => (origin as IsServerAliveProvider).address;
|
String get address => (origin as IsServerAliveProvider).address;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$serverStatusHash() => r'd7079e19e68f5f61b0afa0f73a2af8807c4b3cf6';
|
String _$serverStatusHash() => r'2d9c5d6f970caec555e5322d43a388ea8572619f';
|
||||||
|
|
||||||
/// fetch status of server
|
/// fetch status of server
|
||||||
///
|
///
|
||||||
|
|
@ -467,6 +473,8 @@ class ServerStatusProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin ServerStatusRef on AutoDisposeFutureProviderRef<ServerStatusResponse?> {
|
mixin ServerStatusRef on AutoDisposeFutureProviderRef<ServerStatusResponse?> {
|
||||||
/// The parameter `baseUrl` of this provider.
|
/// The parameter `baseUrl` of this provider.
|
||||||
Uri get baseUrl;
|
Uri get baseUrl;
|
||||||
|
|
@ -488,7 +496,7 @@ class _ServerStatusProviderElement
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$fetchContinueListeningHash() =>
|
String _$fetchContinueListeningHash() =>
|
||||||
r'f65fe3ac3a31b8ac074330525c5d2cc4b526802d';
|
r'50aeb77369eda38d496b2f56f3df2aea135dab45';
|
||||||
|
|
||||||
/// fetch continue listening audiobooks
|
/// fetch continue listening audiobooks
|
||||||
///
|
///
|
||||||
|
|
@ -505,9 +513,11 @@ final fetchContinueListeningProvider =
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef FetchContinueListeningRef
|
typedef FetchContinueListeningRef
|
||||||
= AutoDisposeFutureProviderRef<GetUserSessionsResponse>;
|
= AutoDisposeFutureProviderRef<GetUserSessionsResponse>;
|
||||||
String _$meHash() => r'bdc664c4fd867ad13018fa769ce7a6913248c44f';
|
String _$meHash() => r'b3b6d6d940b465c60d0c29cd6e81ba2fcccab186';
|
||||||
|
|
||||||
/// See also [me].
|
/// See also [me].
|
||||||
@ProviderFor(me)
|
@ProviderFor(me)
|
||||||
|
|
@ -520,8 +530,139 @@ final meProvider = AutoDisposeFutureProvider<User>.internal(
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef MeRef = AutoDisposeFutureProviderRef<User>;
|
typedef MeRef = AutoDisposeFutureProviderRef<User>;
|
||||||
String _$personalizedViewHash() => r'4c392ece4650bdc36d7195a0ddb8810e8fe4caa9';
|
String _$loginHash() => r'99410c2bed9c8f412c7b47c4e655db64e0054be2';
|
||||||
|
|
||||||
|
/// See also [login].
|
||||||
|
@ProviderFor(login)
|
||||||
|
const loginProvider = LoginFamily();
|
||||||
|
|
||||||
|
/// See also [login].
|
||||||
|
class LoginFamily extends Family<AsyncValue<LoginResponse?>> {
|
||||||
|
/// See also [login].
|
||||||
|
const LoginFamily();
|
||||||
|
|
||||||
|
/// See also [login].
|
||||||
|
LoginProvider call({
|
||||||
|
AuthenticatedUser? user,
|
||||||
|
}) {
|
||||||
|
return LoginProvider(
|
||||||
|
user: user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
LoginProvider getProviderOverride(
|
||||||
|
covariant LoginProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
user: provider.user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<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'loginProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [login].
|
||||||
|
class LoginProvider extends AutoDisposeFutureProvider<LoginResponse?> {
|
||||||
|
/// See also [login].
|
||||||
|
LoginProvider({
|
||||||
|
AuthenticatedUser? user,
|
||||||
|
}) : this._internal(
|
||||||
|
(ref) => login(
|
||||||
|
ref as LoginRef,
|
||||||
|
user: user,
|
||||||
|
),
|
||||||
|
from: loginProvider,
|
||||||
|
name: r'loginProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$loginHash,
|
||||||
|
dependencies: LoginFamily._dependencies,
|
||||||
|
allTransitiveDependencies: LoginFamily._allTransitiveDependencies,
|
||||||
|
user: user,
|
||||||
|
);
|
||||||
|
|
||||||
|
LoginProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.user,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final AuthenticatedUser? user;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<LoginResponse?> Function(LoginRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: LoginProvider._internal(
|
||||||
|
(ref) => create(ref as LoginRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
user: user,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<LoginResponse?> createElement() {
|
||||||
|
return _LoginProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is LoginProvider && other.user == user;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, user.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin LoginRef on AutoDisposeFutureProviderRef<LoginResponse?> {
|
||||||
|
/// The parameter `user` of this provider.
|
||||||
|
AuthenticatedUser? get user;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LoginProviderElement
|
||||||
|
extends AutoDisposeFutureProviderElement<LoginResponse?> with LoginRef {
|
||||||
|
_LoginProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
AuthenticatedUser? get user => (origin as LoginProvider).user;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$personalizedViewHash() => r'425e89d99d7e4712b4d6a688f3a12442bd66584f';
|
||||||
|
|
||||||
/// fetch the personalized view
|
/// fetch the personalized view
|
||||||
///
|
///
|
||||||
|
|
@ -540,4 +681,4 @@ final personalizedViewProvider =
|
||||||
|
|
||||||
typedef _$PersonalizedView = AutoDisposeStreamNotifier<List<Shelf>>;
|
typedef _$PersonalizedView = AutoDisposeStreamNotifier<List<Shelf>>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
|
||||||
|
|
||||||
part of 'authenticated_user_provider.dart';
|
|
||||||
|
|
||||||
// **************************************************************************
|
|
||||||
// RiverpodGenerator
|
|
||||||
// **************************************************************************
|
|
||||||
|
|
||||||
String _$authenticatedUserHash() => r'1983527595207c63a12bc84cf0bf1a3c1d729506';
|
|
||||||
|
|
||||||
/// provides with a set of authenticated users
|
|
||||||
///
|
|
||||||
/// Copied from [AuthenticatedUser].
|
|
||||||
@ProviderFor(AuthenticatedUser)
|
|
||||||
final authenticatedUserProvider = AutoDisposeNotifierProvider<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
|
|
||||||
|
|
@ -8,15 +8,15 @@ import 'package:vaani/settings/models/audiobookshelf_server.dart';
|
||||||
import 'package:vaani/settings/models/authenticated_user.dart' as model;
|
import 'package:vaani/settings/models/authenticated_user.dart' as model;
|
||||||
import 'package:vaani/shared/extensions/obfuscation.dart';
|
import 'package:vaani/shared/extensions/obfuscation.dart';
|
||||||
|
|
||||||
part 'authenticated_user_provider.g.dart';
|
part 'authenticated_users_provider.g.dart';
|
||||||
|
|
||||||
final _box = AvailableHiveBoxes.authenticatedUserBox;
|
final _box = AvailableHiveBoxes.authenticatedUserBox;
|
||||||
|
|
||||||
final _logger = Logger('authenticated_user_provider');
|
final _logger = Logger('authenticated_users_provider');
|
||||||
|
|
||||||
/// provides with a set of authenticated users
|
/// provides with a set of authenticated users
|
||||||
@riverpod
|
@riverpod
|
||||||
class AuthenticatedUser extends _$AuthenticatedUser {
|
class AuthenticatedUsers extends _$AuthenticatedUsers {
|
||||||
@override
|
@override
|
||||||
Set<model.AuthenticatedUser> build() {
|
Set<model.AuthenticatedUser> build() {
|
||||||
ref.listenSelf((_, __) {
|
ref.listenSelf((_, __) {
|
||||||
|
|
@ -56,6 +56,7 @@ class AuthenticatedUser extends _$AuthenticatedUser {
|
||||||
|
|
||||||
void addUser(model.AuthenticatedUser user, {bool setActive = false}) {
|
void addUser(model.AuthenticatedUser user, {bool setActive = false}) {
|
||||||
state = state..add(user);
|
state = state..add(user);
|
||||||
|
ref.invalidateSelf();
|
||||||
if (setActive) {
|
if (setActive) {
|
||||||
final apiSettings = ref.read(apiSettingsProvider);
|
final apiSettings = ref.read(apiSettingsProvider);
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
|
@ -82,9 +83,12 @@ class AuthenticatedUser extends _$AuthenticatedUser {
|
||||||
// also remove the user from the active user
|
// also remove the user from the active user
|
||||||
final apiSettings = ref.read(apiSettingsProvider);
|
final apiSettings = ref.read(apiSettingsProvider);
|
||||||
if (apiSettings.activeUser == user) {
|
if (apiSettings.activeUser == user) {
|
||||||
|
// replace the active user with the first user in the list
|
||||||
|
// or null if there are no users left
|
||||||
|
final newActiveUser = state.isNotEmpty ? state.first : null;
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
apiSettings.copyWith(
|
apiSettings.copyWith(
|
||||||
activeUser: null,
|
activeUser: newActiveUser,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
30
lib/api/authenticated_users_provider.g.dart
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'authenticated_users_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$authenticatedUsersHash() =>
|
||||||
|
r'5fdd472f62fc3b73ff8417cdce9f02e86c33d00f';
|
||||||
|
|
||||||
|
/// provides with a set of authenticated users
|
||||||
|
///
|
||||||
|
/// Copied from [AuthenticatedUsers].
|
||||||
|
@ProviderFor(AuthenticatedUsers)
|
||||||
|
final authenticatedUsersProvider = AutoDisposeNotifierProvider<
|
||||||
|
AuthenticatedUsers, Set<model.AuthenticatedUser>>.internal(
|
||||||
|
AuthenticatedUsers.new,
|
||||||
|
name: r'authenticatedUsersProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$authenticatedUsersHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$AuthenticatedUsers
|
||||||
|
= 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, deprecated_member_use_from_same_package
|
||||||
|
|
@ -155,6 +155,8 @@ class CoverImageProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin CoverImageRef on StreamNotifierProviderRef<Uint8List> {
|
mixin CoverImageRef on StreamNotifierProviderRef<Uint8List> {
|
||||||
/// The parameter `itemId` of this provider.
|
/// The parameter `itemId` of this provider.
|
||||||
String get itemId;
|
String get itemId;
|
||||||
|
|
@ -169,4 +171,4 @@ class _CoverImageProviderElement
|
||||||
String get itemId => (origin as CoverImageProvider).itemId;
|
String get itemId => (origin as CoverImageProvider).itemId;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,8 @@ class LibraryItemProvider extends StreamNotifierProviderImpl<LibraryItem,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin LibraryItemRef
|
mixin LibraryItemRef
|
||||||
on StreamNotifierProviderRef<shelfsdk.LibraryItemExpanded> {
|
on StreamNotifierProviderRef<shelfsdk.LibraryItemExpanded> {
|
||||||
/// The parameter `id` of this provider.
|
/// The parameter `id` of this provider.
|
||||||
|
|
@ -184,4 +186,4 @@ class _LibraryItemProviderElement extends StreamNotifierProviderElement<
|
||||||
String get id => (origin as LibraryItemProvider).id;
|
String get id => (origin as LibraryItemProvider).id;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
58
lib/api/library_provider.dart
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart' show Ref;
|
||||||
|
import 'package:logging/logging.dart' show Logger;
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
import 'package:shelfsdk/audiobookshelf_api.dart' show Library;
|
||||||
|
import 'package:vaani/api/api_provider.dart' show authenticatedApiProvider;
|
||||||
|
import 'package:vaani/settings/api_settings_provider.dart'
|
||||||
|
show apiSettingsProvider;
|
||||||
|
part 'library_provider.g.dart';
|
||||||
|
|
||||||
|
final _logger = Logger('LibraryProvider');
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
Future<Library?> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
192
lib/api/library_provider.g.dart
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'library_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
String _$libraryHash() => r'f8a34100acb58f02fa958c71a629577bf815710e';
|
||||||
|
|
||||||
|
/// Copied from Dart SDK
|
||||||
|
class _SystemHash {
|
||||||
|
_SystemHash._();
|
||||||
|
|
||||||
|
static int combine(int hash, int value) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + value);
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||||
|
return hash ^ (hash >> 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int finish(int hash) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
hash = hash ^ (hash >> 11);
|
||||||
|
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [library].
|
||||||
|
@ProviderFor(library)
|
||||||
|
const libraryProvider = LibraryFamily();
|
||||||
|
|
||||||
|
/// See also [library].
|
||||||
|
class LibraryFamily extends Family<AsyncValue<Library?>> {
|
||||||
|
/// See also [library].
|
||||||
|
const LibraryFamily();
|
||||||
|
|
||||||
|
/// See also [library].
|
||||||
|
LibraryProvider call(
|
||||||
|
String id,
|
||||||
|
) {
|
||||||
|
return LibraryProvider(
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
LibraryProvider getProviderOverride(
|
||||||
|
covariant LibraryProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static const Iterable<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'libraryProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [library].
|
||||||
|
class LibraryProvider extends AutoDisposeFutureProvider<Library?> {
|
||||||
|
/// See also [library].
|
||||||
|
LibraryProvider(
|
||||||
|
String id,
|
||||||
|
) : this._internal(
|
||||||
|
(ref) => library(
|
||||||
|
ref as LibraryRef,
|
||||||
|
id,
|
||||||
|
),
|
||||||
|
from: libraryProvider,
|
||||||
|
name: r'libraryProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$libraryHash,
|
||||||
|
dependencies: LibraryFamily._dependencies,
|
||||||
|
allTransitiveDependencies: LibraryFamily._allTransitiveDependencies,
|
||||||
|
id: id,
|
||||||
|
);
|
||||||
|
|
||||||
|
LibraryProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.id,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(
|
||||||
|
FutureOr<Library?> Function(LibraryRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: LibraryProvider._internal(
|
||||||
|
(ref) => create(ref as LibraryRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
id: id,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeFutureProviderElement<Library?> createElement() {
|
||||||
|
return _LibraryProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is LibraryProvider && other.id == id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, id.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
mixin LibraryRef on AutoDisposeFutureProviderRef<Library?> {
|
||||||
|
/// The parameter `id` of this provider.
|
||||||
|
String get id;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LibraryProviderElement extends AutoDisposeFutureProviderElement<Library?>
|
||||||
|
with LibraryRef {
|
||||||
|
_LibraryProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get id => (origin as LibraryProvider).id;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$currentLibraryHash() => r'658498a531e04a01e2b3915a3319101285601118';
|
||||||
|
|
||||||
|
/// See also [currentLibrary].
|
||||||
|
@ProviderFor(currentLibrary)
|
||||||
|
final currentLibraryProvider = AutoDisposeFutureProvider<Library?>.internal(
|
||||||
|
currentLibrary,
|
||||||
|
name: r'currentLibraryProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$currentLibraryHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef CurrentLibraryRef = AutoDisposeFutureProviderRef<Library?>;
|
||||||
|
String _$librariesHash() => r'95ebd4d1ac0cc2acf7617dc22895eff0ca30600f';
|
||||||
|
|
||||||
|
/// See also [Libraries].
|
||||||
|
@ProviderFor(Libraries)
|
||||||
|
final librariesProvider =
|
||||||
|
AutoDisposeAsyncNotifierProvider<Libraries, List<Library>>.internal(
|
||||||
|
Libraries.new,
|
||||||
|
name: r'librariesProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product') ? null : _$librariesHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$Libraries = AutoDisposeAsyncNotifier<List<Library>>;
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import 'package:collection/collection.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:vaani/api/authenticated_user_provider.dart';
|
import 'package:vaani/api/authenticated_users_provider.dart';
|
||||||
import 'package:vaani/db/storage.dart';
|
import 'package:vaani/db/storage.dart';
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
import 'package:vaani/settings/api_settings_provider.dart';
|
||||||
import 'package:vaani/settings/models/audiobookshelf_server.dart' as model;
|
import 'package:vaani/settings/models/audiobookshelf_server.dart' as model;
|
||||||
|
|
@ -50,7 +49,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
|
||||||
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()}');
|
_logger.info('found servers in box: ${foundServers.obfuscate()}');
|
||||||
return foundServers.whereNotNull().toSet();
|
return foundServers.nonNulls.toSet();
|
||||||
} else {
|
} else {
|
||||||
_logger.info('no settings found in box');
|
_logger.info('no settings found in box');
|
||||||
return {};
|
return {};
|
||||||
|
|
@ -89,7 +88,7 @@ class AudiobookShelfServer extends _$AudiobookShelfServer {
|
||||||
}
|
}
|
||||||
// remove the users of this server
|
// remove the users of this server
|
||||||
if (removeUsers) {
|
if (removeUsers) {
|
||||||
ref.read(authenticatedUserProvider.notifier).removeUsersOfServer(server);
|
ref.read(authenticatedUsersProvider.notifier).removeUsersOfServer(server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ part of 'server_provider.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$audiobookShelfServerHash() =>
|
String _$audiobookShelfServerHash() =>
|
||||||
r'0084fb72c4c54323207928b95716cfd9ca496c11';
|
r'31a96b431221965cd586aad670a32ca901539e41';
|
||||||
|
|
||||||
/// provides with a set of servers added by the user
|
/// provides with a set of servers added by the user
|
||||||
///
|
///
|
||||||
|
|
@ -27,4 +27,4 @@ final audiobookShelfServerProvider = AutoDisposeNotifierProvider<
|
||||||
typedef _$AudiobookShelfServer
|
typedef _$AudiobookShelfServer
|
||||||
= AutoDisposeNotifier<Set<model.AudiobookShelfServer>>;
|
= AutoDisposeNotifier<Set<model.AudiobookShelfServer>>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,4 @@ class HeroTagPrefixes {
|
||||||
static const String bookTitle = 'book_title_';
|
static const String bookTitle = 'book_title_';
|
||||||
static const String narratorName = 'narrator_name_';
|
static const String narratorName = 'narrator_name_';
|
||||||
static const String libraryItemPlayButton = 'library_item_play_button_';
|
static const String libraryItemPlayButton = 'library_item_play_button_';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
|
|
@ -122,7 +123,7 @@ class ItemDownloadProgress extends _$ItemDownloadProgress {
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
FutureOr<List<TaskRecord>> downloadHistory(
|
FutureOr<List<TaskRecord>> downloadHistory(
|
||||||
DownloadHistoryRef ref, {
|
Ref ref, {
|
||||||
String? group,
|
String? group,
|
||||||
}) async {
|
}) async {
|
||||||
return await FileDownloader().database.allRecords(group: group);
|
return await FileDownloader().database.allRecords(group: group);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'download_manager.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$downloadHistoryHash() => r'76c449e8abfa61d57566991686f534a06dc7fef7';
|
String _$downloadHistoryHash() => r'4d8b84e30f7ff5ae69d23c8e03ff24af1234a1ad';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
@ -143,6 +143,8 @@ class DownloadHistoryProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin DownloadHistoryRef on AutoDisposeFutureProviderRef<List<TaskRecord>> {
|
mixin DownloadHistoryRef on AutoDisposeFutureProviderRef<List<TaskRecord>> {
|
||||||
/// The parameter `group` of this provider.
|
/// The parameter `group` of this provider.
|
||||||
String? get group;
|
String? get group;
|
||||||
|
|
@ -318,6 +320,8 @@ class IsItemDownloadingProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin IsItemDownloadingRef on AutoDisposeNotifierProviderRef<bool> {
|
mixin IsItemDownloadingRef on AutoDisposeNotifierProviderRef<bool> {
|
||||||
/// The parameter `id` of this provider.
|
/// The parameter `id` of this provider.
|
||||||
String get id;
|
String get id;
|
||||||
|
|
@ -463,6 +467,8 @@ class ItemDownloadProgressProvider extends AutoDisposeAsyncNotifierProviderImpl<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin ItemDownloadProgressRef on AutoDisposeAsyncNotifierProviderRef<double?> {
|
mixin ItemDownloadProgressRef on AutoDisposeAsyncNotifierProviderRef<double?> {
|
||||||
/// The parameter `id` of this provider.
|
/// The parameter `id` of this provider.
|
||||||
String get id;
|
String get id;
|
||||||
|
|
@ -607,6 +613,8 @@ class IsItemDownloadedProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin IsItemDownloadedRef on AutoDisposeAsyncNotifierProviderRef<bool> {
|
mixin IsItemDownloadedRef on AutoDisposeAsyncNotifierProviderRef<bool> {
|
||||||
/// The parameter `item` of this provider.
|
/// The parameter `item` of this provider.
|
||||||
LibraryItemExpanded get item;
|
LibraryItemExpanded get item;
|
||||||
|
|
@ -621,4 +629,4 @@ class _IsItemDownloadedProviderElement
|
||||||
LibraryItemExpanded get item => (origin as IsItemDownloadedProvider).item;
|
LibraryItemExpanded get item => (origin as IsItemDownloadedProvider).item;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ class DownloadsPage extends HookConsumerWidget {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Downloads'),
|
title: const Text('Downloads'),
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
),
|
),
|
||||||
body: Center(
|
body: Center(
|
||||||
// history of downloads
|
// history of downloads
|
||||||
|
|
|
||||||
|
|
@ -26,4 +26,4 @@ final globalSearchControllerProvider =
|
||||||
|
|
||||||
typedef _$GlobalSearchController = Notifier<Raw<SearchController>>;
|
typedef _$GlobalSearchController = Notifier<Raw<SearchController>>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart';
|
||||||
|
|
@ -8,7 +9,7 @@ part 'search_result_provider.g.dart';
|
||||||
/// The provider for the search result.
|
/// The provider for the search result.
|
||||||
@riverpod
|
@riverpod
|
||||||
FutureOr<LibrarySearchResponse?> searchResult(
|
FutureOr<LibrarySearchResponse?> searchResult(
|
||||||
SearchResultRef ref,
|
Ref ref,
|
||||||
String query, {
|
String query, {
|
||||||
int limit = 25,
|
int limit = 25,
|
||||||
}) async {
|
}) async {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'search_result_provider.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$searchResultHash() => r'9baa643cce24f3a5e022f42202e423373939ef95';
|
String _$searchResultHash() => r'33785de298ad0d53c9d21e8fec88ba2f22f1363f';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
@ -167,6 +167,8 @@ class SearchResultProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin SearchResultRef on AutoDisposeFutureProviderRef<LibrarySearchResponse?> {
|
mixin SearchResultRef on AutoDisposeFutureProviderRef<LibrarySearchResponse?> {
|
||||||
/// The parameter `query` of this provider.
|
/// The parameter `query` of this provider.
|
||||||
String get query;
|
String get query;
|
||||||
|
|
@ -186,4 +188,4 @@ class _SearchResultProviderElement
|
||||||
int get limit => (origin as SearchResultProvider).limit;
|
int get limit => (origin as SearchResultProvider).limit;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ class ExplorePage extends HookConsumerWidget {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Explore'),
|
title: const Text('Explore'),
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
),
|
),
|
||||||
body: const MySearchBar(),
|
body: const MySearchBar(),
|
||||||
);
|
);
|
||||||
|
|
@ -98,8 +97,10 @@ class MySearchBar extends HookConsumerWidget {
|
||||||
// opacity: 0.5 for the hint text
|
// opacity: 0.5 for the hint text
|
||||||
hintStyle: WidgetStatePropertyAll(
|
hintStyle: WidgetStatePropertyAll(
|
||||||
Theme.of(context).textTheme.bodyMedium!.copyWith(
|
Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||||
color:
|
color: Theme.of(context)
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.5),
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
textInputAction: TextInputAction.search,
|
textInputAction: TextInputAction.search,
|
||||||
|
|
|
||||||
|
|
@ -425,7 +425,6 @@ class DownloadSheet extends HookConsumerWidget {
|
||||||
|
|
||||||
class _LibraryItemPlayButton extends HookConsumerWidget {
|
class _LibraryItemPlayButton extends HookConsumerWidget {
|
||||||
const _LibraryItemPlayButton({
|
const _LibraryItemPlayButton({
|
||||||
super.key,
|
|
||||||
required this.item,
|
required this.item,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
import 'package:vaani/shared/extensions/duration_format.dart';
|
import 'package:vaani/shared/extensions/duration_format.dart';
|
||||||
import 'package:vaani/shared/extensions/model_conversions.dart';
|
import 'package:vaani/shared/extensions/model_conversions.dart';
|
||||||
import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
|
import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
|
||||||
import 'package:vaani/theme/theme_from_cover_provider.dart';
|
import 'package:vaani/theme/providers/theme_from_cover_provider.dart';
|
||||||
|
|
||||||
class LibraryItemHeroSection extends HookConsumerWidget {
|
class LibraryItemHeroSection extends HookConsumerWidget {
|
||||||
const LibraryItemHeroSection({
|
const LibraryItemHeroSection({
|
||||||
|
|
@ -78,7 +78,6 @@ class LibraryItemHeroSection extends HookConsumerWidget {
|
||||||
|
|
||||||
class _BookDetails extends HookConsumerWidget {
|
class _BookDetails extends HookConsumerWidget {
|
||||||
const _BookDetails({
|
const _BookDetails({
|
||||||
super.key,
|
|
||||||
required this.id,
|
required this.id,
|
||||||
this.extraMap,
|
this.extraMap,
|
||||||
});
|
});
|
||||||
|
|
@ -136,7 +135,6 @@ class _BookDetails extends HookConsumerWidget {
|
||||||
|
|
||||||
class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
||||||
const _LibraryItemProgressIndicator({
|
const _LibraryItemProgressIndicator({
|
||||||
super.key,
|
|
||||||
required this.id,
|
required this.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -201,8 +199,10 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
||||||
'${remainingTime.smartBinaryFormat} left',
|
'${remainingTime.smartBinaryFormat} left',
|
||||||
|
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
color:
|
color: Theme.of(context)
|
||||||
Theme.of(context).colorScheme.onSurface.withOpacity(0.75),
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withValues(alpha: 0.75),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -213,7 +213,6 @@ class _LibraryItemProgressIndicator extends HookConsumerWidget {
|
||||||
|
|
||||||
class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
|
class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
|
||||||
const _HeroSectionSubLabelWithIcon({
|
const _HeroSectionSubLabelWithIcon({
|
||||||
super.key,
|
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.text,
|
required this.text,
|
||||||
});
|
});
|
||||||
|
|
@ -230,7 +229,7 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
|
||||||
ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage;
|
ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage;
|
||||||
final color = useMaterialThemeOnItemPage
|
final color = useMaterialThemeOnItemPage
|
||||||
? themeData.colorScheme.primary
|
? themeData.colorScheme.primary
|
||||||
: themeData.colorScheme.onSurface.withOpacity(0.75);
|
: themeData.colorScheme.onSurface.withValues(alpha: 0.75);
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
padding: const EdgeInsets.only(bottom: 8.0),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
|
@ -260,7 +259,6 @@ class _HeroSectionSubLabelWithIcon extends HookConsumerWidget {
|
||||||
|
|
||||||
class _BookSeries extends StatelessWidget {
|
class _BookSeries extends StatelessWidget {
|
||||||
const _BookSeries({
|
const _BookSeries({
|
||||||
super.key,
|
|
||||||
required this.itemBookMetadata,
|
required this.itemBookMetadata,
|
||||||
required this.bookDetailsCached,
|
required this.bookDetailsCached,
|
||||||
});
|
});
|
||||||
|
|
@ -306,7 +304,6 @@ class _BookSeries extends StatelessWidget {
|
||||||
|
|
||||||
class _BookNarrators extends StatelessWidget {
|
class _BookNarrators extends StatelessWidget {
|
||||||
const _BookNarrators({
|
const _BookNarrators({
|
||||||
super.key,
|
|
||||||
required this.itemBookMetadata,
|
required this.itemBookMetadata,
|
||||||
required this.bookDetailsCached,
|
required this.bookDetailsCached,
|
||||||
});
|
});
|
||||||
|
|
@ -342,7 +339,6 @@ class _BookNarrators extends StatelessWidget {
|
||||||
|
|
||||||
class _BookCover extends HookConsumerWidget {
|
class _BookCover extends HookConsumerWidget {
|
||||||
const _BookCover({
|
const _BookCover({
|
||||||
super.key,
|
|
||||||
required this.itemId,
|
required this.itemId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -353,16 +349,17 @@ class _BookCover extends HookConsumerWidget {
|
||||||
final coverImage = ref.watch(coverImageProvider(itemId));
|
final coverImage = ref.watch(coverImageProvider(itemId));
|
||||||
final themeData = Theme.of(context);
|
final themeData = Theme.of(context);
|
||||||
// final item = ref.watch(libraryItemProvider(itemId));
|
// final item = ref.watch(libraryItemProvider(itemId));
|
||||||
final useMaterialThemeOnItemPage =
|
final themeSettings = ref.watch(appSettingsProvider).themeSettings;
|
||||||
ref.watch(appSettingsProvider).themeSettings.useMaterialThemeOnItemPage;
|
|
||||||
|
|
||||||
ColorScheme? coverColorScheme;
|
ColorScheme? coverColorScheme;
|
||||||
if (useMaterialThemeOnItemPage) {
|
if (themeSettings.useMaterialThemeOnItemPage) {
|
||||||
coverColorScheme = ref
|
coverColorScheme = ref
|
||||||
.watch(
|
.watch(
|
||||||
themeOfLibraryItemProvider(
|
themeOfLibraryItemProvider(
|
||||||
itemId,
|
itemId,
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
|
highContrast: themeSettings.highContrast ||
|
||||||
|
MediaQuery.of(context).highContrast,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.valueOrNull;
|
.valueOrNull;
|
||||||
|
|
@ -371,7 +368,7 @@ class _BookCover extends HookConsumerWidget {
|
||||||
return ThemeSwitcher(
|
return ThemeSwitcher(
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
// change theme after 2 seconds
|
// change theme after 2 seconds
|
||||||
if (useMaterialThemeOnItemPage) {
|
if (themeSettings.useMaterialThemeOnItemPage) {
|
||||||
Future.delayed(150.ms, () {
|
Future.delayed(150.ms, () {
|
||||||
try {
|
try {
|
||||||
ThemeSwitcher.of(context).changeTheme(
|
ThemeSwitcher.of(context).changeTheme(
|
||||||
|
|
@ -415,7 +412,6 @@ class _BookCover extends HookConsumerWidget {
|
||||||
|
|
||||||
class _BookTitle extends StatelessWidget {
|
class _BookTitle extends StatelessWidget {
|
||||||
const _BookTitle({
|
const _BookTitle({
|
||||||
super.key,
|
|
||||||
required this.extraMap,
|
required this.extraMap,
|
||||||
required this.itemBookMetadata,
|
required this.itemBookMetadata,
|
||||||
});
|
});
|
||||||
|
|
@ -449,7 +445,7 @@ class _BookTitle extends StatelessWidget {
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: Text(
|
: Text(
|
||||||
style: themeData.textTheme.titleSmall?.copyWith(
|
style: themeData.textTheme.titleSmall?.copyWith(
|
||||||
color: themeData.colorScheme.onSurface.withOpacity(0.8),
|
color: themeData.colorScheme.onSurface.withValues(alpha: 0.8),
|
||||||
),
|
),
|
||||||
itemBookMetadata?.subtitle ?? '',
|
itemBookMetadata?.subtitle ?? '',
|
||||||
),
|
),
|
||||||
|
|
@ -460,7 +456,6 @@ class _BookTitle extends StatelessWidget {
|
||||||
|
|
||||||
class _BookAuthors extends StatelessWidget {
|
class _BookAuthors extends StatelessWidget {
|
||||||
const _BookAuthors({
|
const _BookAuthors({
|
||||||
super.key,
|
|
||||||
required this.itemBookMetadata,
|
required this.itemBookMetadata,
|
||||||
required this.bookDetailsCached,
|
required this.bookDetailsCached,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,10 @@ class LibraryItemMetadata extends HookConsumerWidget {
|
||||||
return VerticalDivider(
|
return VerticalDivider(
|
||||||
indent: 6,
|
indent: 6,
|
||||||
endIndent: 6,
|
endIndent: 6,
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.6),
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withValues(alpha: 0.6),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
@ -109,7 +112,6 @@ class LibraryItemMetadata extends HookConsumerWidget {
|
||||||
/// key-value pair to display as column
|
/// key-value pair to display as column
|
||||||
class _MetadataItem extends StatelessWidget {
|
class _MetadataItem extends StatelessWidget {
|
||||||
const _MetadataItem({
|
const _MetadataItem({
|
||||||
super.key,
|
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.value,
|
required this.value,
|
||||||
});
|
});
|
||||||
|
|
@ -126,7 +128,7 @@ class _MetadataItem extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
style: themeData.textTheme.titleMedium?.copyWith(
|
style: themeData.textTheme.titleMedium?.copyWith(
|
||||||
color: themeData.colorScheme.onSurface.withOpacity(0.90),
|
color: themeData.colorScheme.onSurface.withValues(alpha: 0.90),
|
||||||
),
|
),
|
||||||
value,
|
value,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
|
@ -134,7 +136,7 @@ class _MetadataItem extends StatelessWidget {
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
style: themeData.textTheme.bodySmall?.copyWith(
|
style: themeData.textTheme.bodySmall?.copyWith(
|
||||||
color: themeData.colorScheme.onSurface.withOpacity(0.7),
|
color: themeData.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||||
),
|
),
|
||||||
title,
|
title,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,11 @@ import 'dart:math';
|
||||||
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
|
import 'package:animated_theme_switcher/animated_theme_switcher.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:vaani/api/library_item_provider.dart';
|
import 'package:vaani/api/library_item_provider.dart';
|
||||||
import 'package:vaani/features/item_viewer/view/library_item_sliver_app_bar.dart';
|
import 'package:vaani/features/item_viewer/view/library_item_sliver_app_bar.dart';
|
||||||
import 'package:vaani/features/player/providers/player_form.dart';
|
import 'package:vaani/features/player/view/mini_player_bottom_padding.dart';
|
||||||
import 'package:vaani/router/models/library_item_extras.dart';
|
import 'package:vaani/router/models/library_item_extras.dart';
|
||||||
import 'package:vaani/shared/widgets/expandable_description.dart';
|
import 'package:vaani/shared/widgets/expandable_description.dart';
|
||||||
|
|
||||||
|
|
@ -23,19 +24,89 @@ class LibraryItemPage extends HookConsumerWidget {
|
||||||
|
|
||||||
final String itemId;
|
final String itemId;
|
||||||
final Object? extra;
|
final Object? extra;
|
||||||
|
static const double _showFabThreshold = 300.0;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final additionalItemData =
|
final additionalItemData =
|
||||||
extra is LibraryItemExtras ? extra as LibraryItemExtras : null;
|
extra is LibraryItemExtras ? extra as LibraryItemExtras : null;
|
||||||
|
final scrollController = useScrollController();
|
||||||
|
final showFab = useState(false);
|
||||||
|
|
||||||
|
// Effect to listen to scroll changes and update FAB visibility
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
void listener() {
|
||||||
|
if (!scrollController.hasClients) {
|
||||||
|
return; // Ensure controller is attached
|
||||||
|
}
|
||||||
|
final shouldShow = scrollController.offset > _showFabThreshold;
|
||||||
|
// Update state only if it changes and widget is still mounted
|
||||||
|
if (showFab.value != shouldShow && context.mounted) {
|
||||||
|
showFab.value = shouldShow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollController.addListener(listener);
|
||||||
|
// Initial check in case the view starts scrolled (less likely but safe)
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (scrollController.hasClients && context.mounted) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup: remove the listener when the widget is disposed
|
||||||
|
return () => scrollController.removeListener(listener);
|
||||||
|
},
|
||||||
|
[scrollController],
|
||||||
|
); // Re-run effect if scrollController changes
|
||||||
|
|
||||||
|
// --- FAB Scroll-to-Top Logic ---
|
||||||
|
void scrollToTop() {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
scrollController.animateTo(
|
||||||
|
0.0, // Target offset (top)
|
||||||
|
duration: 300.ms,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ThemeProvider(
|
return ThemeProvider(
|
||||||
initTheme: Theme.of(context),
|
initTheme: Theme.of(context),
|
||||||
duration: 200.ms,
|
duration: 200.ms,
|
||||||
child: ThemeSwitchingArea(
|
child: ThemeSwitchingArea(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
|
floatingActionButton: AnimatedSwitcher(
|
||||||
|
duration: 250.ms,
|
||||||
|
// A common transition for FABs (fade + scale)
|
||||||
|
transitionBuilder: (Widget child, Animation<double> animation) {
|
||||||
|
return ScaleTransition(
|
||||||
|
scale: animation,
|
||||||
|
child: FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: showFab.value
|
||||||
|
? FloatingActionButton(
|
||||||
|
// Key is important for AnimatedSwitcher to differentiate
|
||||||
|
key: const ValueKey('fab-scroll-top'),
|
||||||
|
onPressed: scrollToTop,
|
||||||
|
tooltip: 'Scroll to top',
|
||||||
|
child: const Icon(Icons.arrow_upward),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink(
|
||||||
|
key: ValueKey('fab-empty'),
|
||||||
|
),
|
||||||
|
),
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
|
controller: scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
const LibraryItemSliverAppBar(),
|
LibraryItemSliverAppBar(
|
||||||
|
id: itemId,
|
||||||
|
scrollController: scrollController,
|
||||||
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
sliver: LibraryItemHeroSection(
|
sliver: LibraryItemHeroSection(
|
||||||
|
|
@ -56,9 +127,7 @@ class LibraryItemPage extends HookConsumerWidget {
|
||||||
child: LibraryItemDescription(id: itemId),
|
child: LibraryItemDescription(id: itemId),
|
||||||
),
|
),
|
||||||
// a padding at the bottom to make sure the last item is not hidden by mini player
|
// a padding at the bottom to make sure the last item is not hidden by mini player
|
||||||
const SliverToBoxAdapter(
|
const SliverToBoxAdapter(child: MiniPlayerBottomPadding()),
|
||||||
child: SizedBox(height: playerMinHeight),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,80 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider;
|
||||||
|
|
||||||
class LibraryItemSliverAppBar extends StatelessWidget {
|
class LibraryItemSliverAppBar extends HookConsumerWidget {
|
||||||
const LibraryItemSliverAppBar({
|
const LibraryItemSliverAppBar({
|
||||||
super.key,
|
super.key,
|
||||||
|
required this.id,
|
||||||
|
required this.scrollController,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final ScrollController scrollController;
|
||||||
|
|
||||||
|
static const double _showTitleThreshold = kToolbarHeight * 0.5;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final item = ref.watch(libraryItemProvider(id)).valueOrNull;
|
||||||
|
|
||||||
|
final showTitle = useState(false);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
void listener() {
|
||||||
|
final shouldShow = scrollController.hasClients &&
|
||||||
|
scrollController.offset > _showTitleThreshold;
|
||||||
|
if (showTitle.value != shouldShow) {
|
||||||
|
showTitle.value = shouldShow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollController.addListener(listener);
|
||||||
|
// Trigger listener once initially in case the view starts scrolled
|
||||||
|
// (though unlikely for this specific use case, it's good practice)
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => scrollController.removeListener(listener);
|
||||||
|
},
|
||||||
|
[scrollController],
|
||||||
|
);
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
floating: true,
|
floating: false,
|
||||||
|
pinned: true,
|
||||||
primary: true,
|
primary: true,
|
||||||
snap: true,
|
|
||||||
actions: [
|
actions: [
|
||||||
// cast button
|
// IconButton(
|
||||||
IconButton(onPressed: () {}, icon: const Icon(Icons.cast)),
|
// icon: const Icon(Icons.cast),
|
||||||
IconButton(onPressed: () {}, icon: const Icon(Icons.more_vert)),
|
// onPressed: () {
|
||||||
|
// // Handle search action
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
],
|
],
|
||||||
|
title: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 150),
|
||||||
|
child: showTitle.value
|
||||||
|
? Text(
|
||||||
|
// Use a Key to help AnimatedSwitcher differentiate widgets
|
||||||
|
key: const ValueKey('title-text'),
|
||||||
|
item?.media.metadata.title ?? '',
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
|
)
|
||||||
|
: const SizedBox(
|
||||||
|
// Also give it a key for differentiation
|
||||||
|
key: ValueKey('empty-title'),
|
||||||
|
width: 0, // Ensure it takes no space if possible
|
||||||
|
height: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,47 +1,83 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/api/library_provider.dart' show currentLibraryProvider;
|
||||||
|
import 'package:vaani/features/you/view/widgets/library_switch_chip.dart'
|
||||||
|
show showLibrarySwitcher;
|
||||||
|
import 'package:vaani/router/router.dart' show Routes;
|
||||||
|
import 'package:vaani/shared/icons/abs_icons.dart' show AbsIcons;
|
||||||
|
import 'package:vaani/shared/widgets/not_implemented.dart'
|
||||||
|
show showNotImplementedToast;
|
||||||
|
|
||||||
class LibraryBrowserPage extends HookConsumerWidget {
|
class LibraryBrowserPage extends HookConsumerWidget {
|
||||||
const LibraryBrowserPage({super.key});
|
const LibraryBrowserPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final currentLibrary = ref.watch(currentLibraryProvider).valueOrNull;
|
||||||
|
|
||||||
|
// Determine the icon to use, with a fallback
|
||||||
|
final IconData libraryIconData =
|
||||||
|
AbsIcons.getIconByName(currentLibrary?.icon) ?? Icons.library_books;
|
||||||
|
|
||||||
|
// Determine the title text
|
||||||
|
final String appBarTitle = '${currentLibrary?.name ?? 'Your'} Library';
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
// Use CustomScrollView to enable slivers
|
||||||
title: const Text('Library'),
|
body: CustomScrollView(
|
||||||
backgroundColor: Colors.transparent,
|
slivers: <Widget>[
|
||||||
),
|
SliverAppBar(
|
||||||
// a list redirecting to authors, genres, and series pages
|
pinned: true,
|
||||||
body: ListView(
|
// floating: true, // Optional: uncomment if you want floating behavior
|
||||||
children: [
|
// snap:
|
||||||
ListTile(
|
// true, // Optional: uncomment if you want snapping behavior (usually with floating: true)
|
||||||
title: const Text('Authors'),
|
leading: IconButton(
|
||||||
leading: const Icon(Icons.person),
|
icon: Icon(libraryIconData),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
tooltip: 'Switch Library', // Helpful tooltip for users
|
||||||
onTap: () {},
|
onPressed: () {
|
||||||
|
showLibrarySwitcher(context, ref);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: Text(appBarTitle),
|
||||||
),
|
),
|
||||||
ListTile(
|
SliverList(
|
||||||
title: const Text('Genres'),
|
delegate: SliverChildListDelegate(
|
||||||
leading: const Icon(Icons.category),
|
[
|
||||||
trailing: const Icon(Icons.chevron_right),
|
ListTile(
|
||||||
onTap: () {},
|
title: const Text('Authors'),
|
||||||
),
|
leading: const Icon(Icons.person),
|
||||||
ListTile(
|
trailing: const Icon(Icons.chevron_right),
|
||||||
title: const Text('Series'),
|
onTap: () {
|
||||||
leading: const Icon(Icons.list),
|
showNotImplementedToast(context);
|
||||||
trailing: const Icon(Icons.chevron_right),
|
},
|
||||||
onTap: () {},
|
),
|
||||||
),
|
ListTile(
|
||||||
// Downloads
|
title: const Text('Genres'),
|
||||||
ListTile(
|
leading: const Icon(Icons.category),
|
||||||
title: const Text('Downloads'),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
leading: const Icon(Icons.download),
|
onTap: () {
|
||||||
trailing: const Icon(Icons.chevron_right),
|
showNotImplementedToast(context);
|
||||||
onTap: () {
|
},
|
||||||
GoRouter.of(context).pushNamed(Routes.downloads.name);
|
),
|
||||||
},
|
ListTile(
|
||||||
|
title: const Text('Series'),
|
||||||
|
leading: const Icon(Icons.list),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
showNotImplementedToast(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
// Downloads
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Downloads'),
|
||||||
|
leading: const Icon(Icons.download),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () {
|
||||||
|
GoRouter.of(context).pushNamed(Routes.downloads.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:archive/archive_io.dart';
|
import 'package:archive/archive_io.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:vaani/features/logging/core/logger.dart';
|
import 'package:vaani/features/logging/core/logger.dart';
|
||||||
|
|
||||||
part 'logs_provider.g.dart';
|
part 'logs_provider.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
|
|
@ -29,11 +29,23 @@ class Logs extends _$Logs {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String> getZipFilePath() async {
|
Future<String> getZipFilePath() async {
|
||||||
|
final String targetZipPath = await generateZipFilePath();
|
||||||
var encoder = ZipFileEncoder();
|
var encoder = ZipFileEncoder();
|
||||||
encoder.create(await generateZipFilePath());
|
encoder.create(targetZipPath);
|
||||||
encoder.addFile(File(await getLoggingFilePath()));
|
final logFilePath = await getLoggingFilePath();
|
||||||
encoder.close();
|
final logFile = File(logFilePath);
|
||||||
return encoder.zipPath;
|
if (await logFile.exists()) {
|
||||||
|
// Check if log file exists before adding
|
||||||
|
await encoder.addFile(logFile);
|
||||||
|
} else {
|
||||||
|
// Handle case where log file doesn't exist? Maybe log a warning?
|
||||||
|
// Or create an empty file inside the zip? For now, just don't add.
|
||||||
|
debugPrint(
|
||||||
|
'Warning: Log file not found at $logFilePath, creating potentially empty zip.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await encoder.close();
|
||||||
|
return targetZipPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,7 +55,7 @@ Future<String> generateZipFilePath() async {
|
||||||
}
|
}
|
||||||
|
|
||||||
String generateZipFileName() {
|
String generateZipFileName() {
|
||||||
return 'vaani-${DateTime.now().toIso8601String()}.zip';
|
return 'vaani-${DateTime.now().microsecondsSinceEpoch}.zip';
|
||||||
}
|
}
|
||||||
|
|
||||||
Level parseLevel(String level) {
|
Level parseLevel(String level) {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'logs_provider.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$logsHash() => r'901376741d17ddbb889d1b7b96bc2882289720a0';
|
String _$logsHash() => r'aa9d3d56586cba6ddf69615320ea605d071ea5e2';
|
||||||
|
|
||||||
/// See also [Logs].
|
/// See also [Logs].
|
||||||
@ProviderFor(Logs)
|
@ProviderFor(Logs)
|
||||||
|
|
@ -22,4 +22,4 @@ final logsProvider =
|
||||||
|
|
||||||
typedef _$Logs = AutoDisposeAsyncNotifier<List<LogRecord>>;
|
typedef _$Logs = AutoDisposeAsyncNotifier<List<LogRecord>>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,3 @@
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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';
|
||||||
|
|
@ -76,32 +73,77 @@ class LogsPage extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
IconButton(
|
|
||||||
tooltip: 'Download logs',
|
|
||||||
icon: const Icon(Icons.download),
|
|
||||||
onPressed: () async {
|
|
||||||
appLogger.info('Preparing logs for download');
|
|
||||||
final zipLogFilePath =
|
|
||||||
await ref.read(logsProvider.notifier).getZipFilePath();
|
|
||||||
|
|
||||||
// save to folder
|
// downloads disabled since manage external storage permission was removed
|
||||||
String? outputFile = await FilePicker.platform.saveFile(
|
// see https://gitlab.com/IzzyOnDroid/repo/-/issues/623#note_2240386369
|
||||||
dialogTitle: 'Please select an output file:',
|
// IconButton(
|
||||||
fileName: zipLogFilePath.split('/').last,
|
// tooltip: 'Download logs',
|
||||||
);
|
// icon: const Icon(Icons.download),
|
||||||
if (outputFile != null) {
|
// onPressed: () async {
|
||||||
try {
|
// appLogger.info('Preparing logs for download');
|
||||||
final file = File(outputFile);
|
|
||||||
final zipFile = File(zipLogFilePath);
|
// if (Platform.isAndroid) {
|
||||||
await zipFile.copy(file.path);
|
// final androidVersion =
|
||||||
} catch (e) {
|
// await ref.watch(deviceSdkVersionProvider.future);
|
||||||
appLogger.severe('Error saving file: $e');
|
|
||||||
}
|
// if ((int.parse(androidVersion)) > 29) {
|
||||||
} else {
|
// final status = await Permission.storage.status;
|
||||||
appLogger.info('Download cancelled');
|
// 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(
|
IconButton(
|
||||||
tooltip: 'Refresh logs',
|
tooltip: 'Refresh logs',
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
|
|
@ -122,7 +164,7 @@ class LogsPage extends HookConsumerWidget {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
// a column with listview.builder and a scrollable list of logs
|
// a column with ListView.builder and a scrollable list of logs
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// a filter for log levels, loggers, and search
|
// a filter for log levels, loggers, and search
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart';
|
||||||
import 'package:vaani/models/error_response.dart';
|
import 'package:vaani/models/error_response.dart';
|
||||||
|
|
@ -61,7 +62,7 @@ class OauthFlows extends _$OauthFlows {
|
||||||
/// the code returned by the server in exchange for the verifier
|
/// the code returned by the server in exchange for the verifier
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<String?> loginInExchangeForCode(
|
Future<String?> loginInExchangeForCode(
|
||||||
LoginInExchangeForCodeRef ref, {
|
Ref ref, {
|
||||||
required State oauthState,
|
required State oauthState,
|
||||||
required Code code,
|
required Code code,
|
||||||
ErrorResponseHandler? responseHandler,
|
ErrorResponseHandler? responseHandler,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ part of 'oauth_provider.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$loginInExchangeForCodeHash() =>
|
String _$loginInExchangeForCodeHash() =>
|
||||||
r'e931254959d9eb8196439c6b0c884c26cbe17c2f';
|
r'bfc3945529048a0f536052fd5579b76457560fcd';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
@ -179,6 +179,8 @@ class LoginInExchangeForCodeProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin LoginInExchangeForCodeRef on AutoDisposeFutureProviderRef<String?> {
|
mixin LoginInExchangeForCodeRef on AutoDisposeFutureProviderRef<String?> {
|
||||||
/// The parameter `oauthState` of this provider.
|
/// The parameter `oauthState` of this provider.
|
||||||
String get oauthState;
|
String get oauthState;
|
||||||
|
|
@ -221,4 +223,4 @@ final oauthFlowsProvider =
|
||||||
|
|
||||||
typedef _$OauthFlows = Notifier<Map<State, Flow>>;
|
typedef _$OauthFlows = Notifier<Map<State, Flow>>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,6 @@ class BackToLoginButton extends StatelessWidget {
|
||||||
|
|
||||||
class _SomethingWentWrong extends StatelessWidget {
|
class _SomethingWentWrong extends StatelessWidget {
|
||||||
const _SomethingWentWrong({
|
const _SomethingWentWrong({
|
||||||
super.key,
|
|
||||||
this.message = 'Error with OAuth flow',
|
this.message = 'Error with OAuth flow',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,89 +13,122 @@ class OnboardingSinglePage extends HookConsumerWidget {
|
||||||
super.key,
|
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() ?? '',
|
text: apiSettings.activeServer?.serverUrl.toString() ?? 'https://',
|
||||||
);
|
);
|
||||||
var audiobookshelfUri = makeBaseUrl(serverUriController.text);
|
var audiobookshelfUri = makeBaseUrl(serverUriController.text);
|
||||||
|
|
||||||
final canUserLogin = useState(apiSettings.activeServer != null);
|
final canUserLogin = useState(apiSettings.activeServer != null);
|
||||||
|
|
||||||
fadeSlideTransitionBuilder(
|
return Column(
|
||||||
Widget child,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
Animation<double> animation,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
) {
|
children: [
|
||||||
return FadeTransition(
|
Padding(
|
||||||
opacity: animation,
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: SlideTransition(
|
child: Text(
|
||||||
position: Tween<Offset>(
|
'Welcome to Vaani',
|
||||||
begin: const Offset(0, 0.3),
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
end: const Offset(0, 0),
|
),
|
||||||
).animate(animation),
|
|
||||||
child: child,
|
|
||||||
),
|
),
|
||||||
);
|
const SizedBox.square(
|
||||||
}
|
dimension: 16.0,
|
||||||
|
),
|
||||||
return Scaffold(
|
Padding(
|
||||||
body: Column(
|
padding: const EdgeInsets.all(8.0),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: AnimatedSwitcher(
|
||||||
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
|
||||||
? UserLoginWidget(
|
? Text(
|
||||||
server: audiobookshelfUri,
|
'Server connected, please login',
|
||||||
|
key: const ValueKey('connected'),
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
)
|
)
|
||||||
// ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0)
|
: Text(
|
||||||
: const RedirectToABS().animate().fadeIn().slideY(
|
'Please enter the URL of your AudiobookShelf Server',
|
||||||
curve: Curves.easeInOut,
|
key: const ValueKey('not_connected'),
|
||||||
duration: 500.ms,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: AddNewServer(
|
||||||
|
controller: serverUriController,
|
||||||
|
allowEmpty: true,
|
||||||
|
onPressed: () {
|
||||||
|
canUserLogin.value = serverUriController.text.isNotEmpty;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox.square(
|
||||||
|
dimension: 16.0,
|
||||||
|
),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: 500.ms,
|
||||||
|
transitionBuilder: fadeSlideTransitionBuilder,
|
||||||
|
child: canUserLogin.value
|
||||||
|
? UserLoginWidget(
|
||||||
|
server: audiobookshelfUri,
|
||||||
|
)
|
||||||
|
// ).animate().fade(duration: 600.ms).slideY(begin: 0.3, end: 0)
|
||||||
|
: const RedirectToABS().animate().fadeIn().slideY(
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
duration: 500.ms,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,42 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart' show AuthMethod;
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart' show serverStatusProvider;
|
||||||
import 'package:vaani/api/server_provider.dart';
|
import 'package:vaani/api/server_provider.dart'
|
||||||
import 'package:vaani/features/onboarding/view/user_login_with_open_id.dart';
|
show ServerAlreadyExistsException, audiobookShelfServerProvider;
|
||||||
import 'package:vaani/features/onboarding/view/user_login_with_password.dart';
|
import 'package:vaani/features/onboarding/view/onboarding_single_page.dart'
|
||||||
import 'package:vaani/features/onboarding/view/user_login_with_token.dart';
|
show fadeSlideTransitionBuilder;
|
||||||
import 'package:vaani/hacks/fix_autofill_losing_focus.dart';
|
import 'package:vaani/features/onboarding/view/user_login_with_open_id.dart'
|
||||||
import 'package:vaani/models/error_response.dart';
|
show UserLoginWithOpenID;
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
import 'package:vaani/features/onboarding/view/user_login_with_password.dart'
|
||||||
|
show UserLoginWithPassword;
|
||||||
|
import 'package:vaani/features/onboarding/view/user_login_with_token.dart'
|
||||||
|
show UserLoginWithToken;
|
||||||
|
import 'package:vaani/hacks/fix_autofill_losing_focus.dart'
|
||||||
|
show InactiveFocusScopeObserver;
|
||||||
|
import 'package:vaani/models/error_response.dart' show ErrorResponseHandler;
|
||||||
|
import 'package:vaani/settings/api_settings_provider.dart'
|
||||||
|
show apiSettingsProvider;
|
||||||
import 'package:vaani/settings/models/models.dart' as model;
|
import 'package:vaani/settings/models/models.dart' as model;
|
||||||
|
|
||||||
class UserLoginWidget extends HookConsumerWidget {
|
class UserLoginWidget extends HookConsumerWidget {
|
||||||
UserLoginWidget({
|
const UserLoginWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.server,
|
required this.server,
|
||||||
|
this.onSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
final serverStatusError = ErrorResponseHandler();
|
final Function(model.AuthenticatedUser)? onSuccess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final serverStatusError = useMemoized(() => ErrorResponseHandler(), []);
|
||||||
final serverStatus =
|
final serverStatus =
|
||||||
ref.watch(serverStatusProvider(server, serverStatusError.storeError));
|
ref.watch(serverStatusProvider(server, serverStatusError.storeError));
|
||||||
|
|
||||||
final api = ref.watch(audiobookshelfApiProvider(server));
|
|
||||||
|
|
||||||
return serverStatus.when(
|
return serverStatus.when(
|
||||||
data: (value) {
|
data: (value) {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
|
|
@ -42,6 +51,7 @@ 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: () {
|
||||||
|
|
@ -88,6 +98,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
this.openIDAvailable = false,
|
this.openIDAvailable = false,
|
||||||
this.onPressed,
|
this.onPressed,
|
||||||
this.openIDButtonText,
|
this.openIDButtonText,
|
||||||
|
this.onSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
|
|
@ -95,6 +106,7 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
final bool openIDAvailable;
|
final bool openIDAvailable;
|
||||||
final void Function()? onPressed;
|
final void Function()? onPressed;
|
||||||
final String? openIDButtonText;
|
final String? openIDButtonText;
|
||||||
|
final Function(model.AuthenticatedUser)? onSuccess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -104,8 +116,6 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
localAvailable ? AuthMethodChoice.local : AuthMethodChoice.authToken,
|
localAvailable ? AuthMethodChoice.local : AuthMethodChoice.authToken,
|
||||||
);
|
);
|
||||||
|
|
||||||
final apiSettings = ref.watch(apiSettingsProvider);
|
|
||||||
|
|
||||||
model.AudiobookShelfServer addServer() {
|
model.AudiobookShelfServer addServer() {
|
||||||
var newServer = model.AudiobookShelfServer(
|
var newServer = model.AudiobookShelfServer(
|
||||||
serverUrl: server,
|
serverUrl: server,
|
||||||
|
|
@ -119,9 +129,9 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
newServer = e.server;
|
newServer = e.server;
|
||||||
} finally {
|
} finally {
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
apiSettings.copyWith(
|
ref.read(apiSettingsProvider).copyWith(
|
||||||
activeServer: newServer,
|
activeServer: newServer,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return newServer;
|
return newServer;
|
||||||
|
|
@ -130,11 +140,11 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
return Center(
|
return Center(
|
||||||
child: InactiveFocusScopeObserver(
|
child: InactiveFocusScopeObserver(
|
||||||
child: AutofillGroup(
|
child: AutofillGroup(
|
||||||
child: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(8.0),
|
children: [
|
||||||
child: Column(
|
Padding(
|
||||||
children: [
|
padding: const EdgeInsets.all(8.0),
|
||||||
Wrap(
|
child: Wrap(
|
||||||
// mainAxisAlignment: MainAxisAlignment.center,
|
// mainAxisAlignment: MainAxisAlignment.center,
|
||||||
spacing: 10,
|
spacing: 10,
|
||||||
runAlignment: WrapAlignment.center,
|
runAlignment: WrapAlignment.center,
|
||||||
|
|
@ -172,28 +182,38 @@ class UserLoginMultipleAuth extends HookConsumerWidget {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
].animate(interval: 100.ms).fadeIn(
|
||||||
|
duration: 150.ms,
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox.square(
|
),
|
||||||
dimension: 8,
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: 200.ms,
|
||||||
|
transitionBuilder: fadeSlideTransitionBuilder,
|
||||||
|
child: switch (methodChoice.value) {
|
||||||
|
AuthMethodChoice.authToken => UserLoginWithToken(
|
||||||
|
server: server,
|
||||||
|
addServer: addServer,
|
||||||
|
onSuccess: onSuccess,
|
||||||
|
),
|
||||||
|
AuthMethodChoice.local => UserLoginWithPassword(
|
||||||
|
server: server,
|
||||||
|
addServer: addServer,
|
||||||
|
onSuccess: onSuccess,
|
||||||
|
),
|
||||||
|
AuthMethodChoice.openid => UserLoginWithOpenID(
|
||||||
|
server: server,
|
||||||
|
addServer: addServer,
|
||||||
|
openIDButtonText: openIDButtonText,
|
||||||
|
onSuccess: onSuccess,
|
||||||
|
),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
switch (methodChoice.value) {
|
),
|
||||||
AuthMethodChoice.authToken => UserLoginWithToken(
|
],
|
||||||
server: server,
|
|
||||||
addServer: addServer,
|
|
||||||
),
|
|
||||||
AuthMethodChoice.local => UserLoginWithPassword(
|
|
||||||
server: server,
|
|
||||||
addServer: addServer,
|
|
||||||
),
|
|
||||||
AuthMethodChoice.openid => UserLoginWithOpenID(
|
|
||||||
server: server,
|
|
||||||
addServer: addServer,
|
|
||||||
openIDButtonText: openIDButtonText,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -20,12 +20,14 @@ class UserLoginWithOpenID extends HookConsumerWidget {
|
||||||
required this.server,
|
required this.server,
|
||||||
required this.addServer,
|
required this.addServer,
|
||||||
this.openIDButtonText,
|
this.openIDButtonText,
|
||||||
|
this.onSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
final model.AudiobookShelfServer Function() addServer;
|
final model.AudiobookShelfServer Function() addServer;
|
||||||
final String? openIDButtonText;
|
final String? openIDButtonText;
|
||||||
final responseErrorHandler = ErrorResponseHandler(name: 'OpenID');
|
final responseErrorHandler = ErrorResponseHandler(name: 'OpenID');
|
||||||
|
final Function(model.AuthenticatedUser)? onSuccess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lottie/lottie.dart';
|
import 'package:lottie/lottie.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart';
|
||||||
import 'package:vaani/api/authenticated_user_provider.dart';
|
import 'package:vaani/api/authenticated_users_provider.dart';
|
||||||
import 'package:vaani/hacks/fix_autofill_losing_focus.dart';
|
import 'package:vaani/hacks/fix_autofill_losing_focus.dart';
|
||||||
import 'package:vaani/models/error_response.dart';
|
import 'package:vaani/models/error_response.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
|
import 'package:vaani/settings/constants.dart';
|
||||||
import 'package:vaani/settings/models/models.dart' as model;
|
import 'package:vaani/settings/models/models.dart' as model;
|
||||||
import 'package:vaani/shared/utils.dart';
|
import 'package:vaani/shared/utils.dart';
|
||||||
|
|
||||||
|
|
@ -17,17 +18,20 @@ class UserLoginWithPassword extends HookConsumerWidget {
|
||||||
super.key,
|
super.key,
|
||||||
required this.server,
|
required this.server,
|
||||||
required this.addServer,
|
required this.addServer,
|
||||||
|
this.onSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
final model.AudiobookShelfServer Function() addServer;
|
final model.AudiobookShelfServer Function() addServer;
|
||||||
final serverErrorResponse = ErrorResponseHandler();
|
final serverErrorResponse = ErrorResponseHandler();
|
||||||
|
final Function(model.AuthenticatedUser)? onSuccess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final usernameController = useTextEditingController();
|
final usernameController = useTextEditingController();
|
||||||
final passwordController = useTextEditingController();
|
final passwordController = useTextEditingController();
|
||||||
final isPasswordVisibleAnimationController = useAnimationController(
|
final isPasswordVisibleAnimationController = useAnimationController(
|
||||||
|
initialValue: 1,
|
||||||
duration: const Duration(milliseconds: 500),
|
duration: const Duration(milliseconds: 500),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -76,92 +80,94 @@ 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);
|
|
||||||
|
|
||||||
// redirect to the library page
|
if (onSuccess != null) {
|
||||||
GoRouter.of(context).goNamed(Routes.home.name);
|
onSuccess!(authenticatedUser);
|
||||||
|
} else {
|
||||||
|
// add the user to the list of users
|
||||||
|
ref
|
||||||
|
.read(authenticatedUsersProvider.notifier)
|
||||||
|
.addUser(authenticatedUser, setActive: true);
|
||||||
|
context.goNamed(Routes.home.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: InactiveFocusScopeObserver(
|
child: InactiveFocusScopeObserver(
|
||||||
child: AutofillGroup(
|
child: AutofillGroup(
|
||||||
child: Padding(
|
child: Column(
|
||||||
padding: const EdgeInsets.all(8.0),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
child: Column(
|
children: [
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
TextFormField(
|
||||||
children: [
|
controller: usernameController,
|
||||||
TextFormField(
|
autofocus: true,
|
||||||
controller: usernameController,
|
autofillHints: const [AutofillHints.username],
|
||||||
autofocus: true,
|
textInputAction: TextInputAction.next,
|
||||||
autofillHints: const [AutofillHints.username],
|
decoration: InputDecoration(
|
||||||
textInputAction: TextInputAction.next,
|
labelText: 'Username',
|
||||||
decoration: InputDecoration(
|
labelStyle: TextStyle(
|
||||||
labelText: 'Username',
|
color: Theme.of(context)
|
||||||
labelStyle: TextStyle(
|
.colorScheme
|
||||||
color: Theme.of(context)
|
.onSurface
|
||||||
.colorScheme
|
.withValues(alpha: 0.8),
|
||||||
.onSurface
|
|
||||||
.withOpacity(0.8),
|
|
||||||
),
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
TextFormField(
|
const SizedBox(height: 10),
|
||||||
controller: passwordController,
|
TextFormField(
|
||||||
autofillHints: const [AutofillHints.password],
|
controller: passwordController,
|
||||||
textInputAction: TextInputAction.done,
|
autofillHints: const [AutofillHints.password],
|
||||||
obscureText: !isPasswordVisible.value,
|
textInputAction: TextInputAction.done,
|
||||||
onFieldSubmitted: (_) {
|
obscureText: !isPasswordVisible.value,
|
||||||
loginAndSave();
|
onFieldSubmitted: (_) {
|
||||||
},
|
loginAndSave();
|
||||||
decoration: InputDecoration(
|
},
|
||||||
labelText: 'Password',
|
decoration: InputDecoration(
|
||||||
labelStyle: TextStyle(
|
labelText: 'Password',
|
||||||
color: Theme.of(context)
|
labelStyle: TextStyle(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withValues(alpha: 0.8),
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: ColorFiltered(
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.onSurface
|
.primary
|
||||||
.withOpacity(0.8),
|
.withValues(alpha: 0.8),
|
||||||
|
BlendMode.srcIn,
|
||||||
),
|
),
|
||||||
border: const OutlineInputBorder(),
|
child: InkWell(
|
||||||
suffixIcon: ColorFiltered(
|
borderRadius: BorderRadius.circular(50),
|
||||||
colorFilter: ColorFilter.mode(
|
onTap: () {
|
||||||
Theme.of(context).colorScheme.primary.withOpacity(0.8),
|
isPasswordVisible.value = !isPasswordVisible.value;
|
||||||
BlendMode.srcIn,
|
},
|
||||||
),
|
child: Container(
|
||||||
child: InkWell(
|
margin: const EdgeInsets.only(left: 8, right: 8),
|
||||||
borderRadius: BorderRadius.circular(50),
|
child: Lottie.asset(
|
||||||
onTap: () {
|
'assets/animations/Animation - 1714930099660.json',
|
||||||
isPasswordVisible.value = !isPasswordVisible.value;
|
controller: isPasswordVisibleAnimationController,
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(left: 8, right: 8),
|
|
||||||
child: Lottie.asset(
|
|
||||||
'assets/animations/Animation - 1714930099660.json',
|
|
||||||
controller: isPasswordVisibleAnimationController,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
suffixIconConstraints: const BoxConstraints(
|
),
|
||||||
maxHeight: 45,
|
suffixIconConstraints: const BoxConstraints(
|
||||||
),
|
maxHeight: 45,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 30),
|
),
|
||||||
ElevatedButton(
|
const SizedBox(height: 30),
|
||||||
onPressed: loginAndSave,
|
ElevatedButton(
|
||||||
child: const Text('Login'),
|
onPressed: loginAndSave,
|
||||||
),
|
child: const Text('Login'),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -207,8 +213,10 @@ Future<void> handleServerError(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// open an issue on the github page
|
// open an issue on the github page
|
||||||
handleLaunchUrl(
|
handleLaunchUrl(
|
||||||
Uri.parse(
|
AppMetadata.githubRepo
|
||||||
'https://github.com/Dr-Blank/Vaani/issues',
|
// append the issue url
|
||||||
|
.replace(
|
||||||
|
path: '${AppMetadata.githubRepo.path}/issues/new',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
import 'package:vaani/api/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart';
|
||||||
import 'package:vaani/api/authenticated_user_provider.dart';
|
import 'package:vaani/api/authenticated_users_provider.dart';
|
||||||
import 'package:vaani/models/error_response.dart';
|
import 'package:vaani/models/error_response.dart';
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/router/router.dart';
|
||||||
import 'package:vaani/settings/models/models.dart' as model;
|
import 'package:vaani/settings/models/models.dart' as model;
|
||||||
|
|
@ -14,11 +14,13 @@ class UserLoginWithToken extends HookConsumerWidget {
|
||||||
super.key,
|
super.key,
|
||||||
required this.server,
|
required this.server,
|
||||||
required this.addServer,
|
required this.addServer,
|
||||||
|
this.onSuccess,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Uri server;
|
final Uri server;
|
||||||
final model.AudiobookShelfServer Function() addServer;
|
final model.AudiobookShelfServer Function() addServer;
|
||||||
final serverErrorResponse = ErrorResponseHandler();
|
final serverErrorResponse = ErrorResponseHandler();
|
||||||
|
final Function(model.AuthenticatedUser)? onSuccess;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
|
@ -65,11 +67,14 @@ class UserLoginWithToken extends HookConsumerWidget {
|
||||||
authToken: api.token!,
|
authToken: api.token!,
|
||||||
);
|
);
|
||||||
|
|
||||||
ref
|
if (onSuccess != null) {
|
||||||
.read(authenticatedUserProvider.notifier)
|
onSuccess!(authenticatedUser);
|
||||||
.addUser(authenticatedUser, setActive: true);
|
} else {
|
||||||
|
ref
|
||||||
context.goNamed(Routes.home.name);
|
.read(authenticatedUsersProvider.notifier)
|
||||||
|
.addUser(authenticatedUser, setActive: true);
|
||||||
|
context.goNamed(Routes.home.name);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Form(
|
return Form(
|
||||||
|
|
@ -84,7 +89,10 @@ class UserLoginWithToken extends HookConsumerWidget {
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'API Token',
|
labelText: 'API Token',
|
||||||
labelStyle: TextStyle(
|
labelStyle: TextStyle(
|
||||||
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.8),
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withValues(alpha: 0.8),
|
||||||
),
|
),
|
||||||
border: const OutlineInputBorder(),
|
border: const OutlineInputBorder(),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -157,6 +157,8 @@ class BookSettingsProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
mixin BookSettingsRef on AutoDisposeNotifierProviderRef<model.BookSettings> {
|
mixin BookSettingsRef on AutoDisposeNotifierProviderRef<model.BookSettings> {
|
||||||
/// The parameter `bookId` of this provider.
|
/// The parameter `bookId` of this provider.
|
||||||
String get bookId;
|
String get bookId;
|
||||||
|
|
@ -171,4 +173,4 @@ class _BookSettingsProviderElement
|
||||||
String get bookId => (origin as BookSettingsProvider).bookId;
|
String get bookId => (origin as BookSettingsProvider).bookId;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,4 @@ final playbackReporterProvider =
|
||||||
|
|
||||||
typedef _$PlaybackReporter = AsyncNotifier<core.PlaybackReporter>;
|
typedef _$PlaybackReporter = AsyncNotifier<core.PlaybackReporter>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,4 @@ final playlistProvider =
|
||||||
|
|
||||||
typedef _$Playlist = AutoDisposeNotifier<AudiobookPlaylist>;
|
typedef _$Playlist = AutoDisposeNotifier<AudiobookPlaylist>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -43,4 +43,4 @@ final audiobookPlayerProvider =
|
||||||
|
|
||||||
typedef _$AudiobookPlayer = Notifier<core.AudiobookPlayer>;
|
typedef _$AudiobookPlayer = Notifier<core.AudiobookPlayer>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shelfsdk/audiobookshelf_api.dart';
|
import 'package:shelfsdk/audiobookshelf_api.dart';
|
||||||
|
|
@ -9,7 +10,7 @@ part 'currently_playing_provider.g.dart';
|
||||||
final _logger = Logger('CurrentlyPlayingProvider');
|
final _logger = Logger('CurrentlyPlayingProvider');
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
BookExpanded? currentlyPlayingBook(CurrentlyPlayingBookRef ref) {
|
BookExpanded? currentlyPlayingBook(Ref ref) {
|
||||||
try {
|
try {
|
||||||
final player = ref.watch(audiobookPlayerProvider);
|
final player = ref.watch(audiobookPlayerProvider);
|
||||||
return player.book;
|
return player.book;
|
||||||
|
|
@ -21,7 +22,7 @@ BookExpanded? currentlyPlayingBook(CurrentlyPlayingBookRef ref) {
|
||||||
|
|
||||||
/// provided the current chapter of the book being played
|
/// provided the current chapter of the book being played
|
||||||
@riverpod
|
@riverpod
|
||||||
BookChapter? currentPlayingChapter(CurrentPlayingChapterRef ref) {
|
BookChapter? currentPlayingChapter(Ref ref) {
|
||||||
final player = ref.watch(audiobookPlayerProvider);
|
final player = ref.watch(audiobookPlayerProvider);
|
||||||
player.slowPositionStream.listen((_) {
|
player.slowPositionStream.listen((_) {
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
|
|
@ -32,7 +33,7 @@ BookChapter? currentPlayingChapter(CurrentPlayingChapterRef ref) {
|
||||||
|
|
||||||
/// provides the book metadata of the currently playing book
|
/// provides the book metadata of the currently playing book
|
||||||
@riverpod
|
@riverpod
|
||||||
BookMetadataExpanded? currentBookMetadata(CurrentBookMetadataRef ref) {
|
BookMetadataExpanded? currentBookMetadata(Ref ref) {
|
||||||
final player = ref.watch(audiobookPlayerProvider);
|
final player = ref.watch(audiobookPlayerProvider);
|
||||||
if (player.book == null) return null;
|
if (player.book == null) return null;
|
||||||
return player.book!.metadata.asBookMetadataExpanded;
|
return player.book!.metadata.asBookMetadataExpanded;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ part of 'currently_playing_provider.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$currentlyPlayingBookHash() =>
|
String _$currentlyPlayingBookHash() =>
|
||||||
r'7440b0d54cb364f66e704783652e8f1490ae90e0';
|
r'e4258694c8f0d1e89651b330fae0f672ca13a484';
|
||||||
|
|
||||||
/// See also [currentlyPlayingBook].
|
/// See also [currentlyPlayingBook].
|
||||||
@ProviderFor(currentlyPlayingBook)
|
@ProviderFor(currentlyPlayingBook)
|
||||||
|
|
@ -22,9 +22,11 @@ final currentlyPlayingBookProvider =
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef CurrentlyPlayingBookRef = AutoDisposeProviderRef<BookExpanded?>;
|
typedef CurrentlyPlayingBookRef = AutoDisposeProviderRef<BookExpanded?>;
|
||||||
String _$currentPlayingChapterHash() =>
|
String _$currentPlayingChapterHash() =>
|
||||||
r'a084da724e3d8bb1b1475e867ab3200d7d61d827';
|
r'73db8b8a9058573bb0c68ec5d5f8aba9306f3d24';
|
||||||
|
|
||||||
/// provided the current chapter of the book being played
|
/// provided the current chapter of the book being played
|
||||||
///
|
///
|
||||||
|
|
@ -41,9 +43,11 @@ final currentPlayingChapterProvider =
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef CurrentPlayingChapterRef = AutoDisposeProviderRef<BookChapter?>;
|
typedef CurrentPlayingChapterRef = AutoDisposeProviderRef<BookChapter?>;
|
||||||
String _$currentBookMetadataHash() =>
|
String _$currentBookMetadataHash() =>
|
||||||
r'9088debba151894b61f2dcba1bba12a89244b9b1';
|
r'f537ef4ef19280bc952de658ecf6520c535ae344';
|
||||||
|
|
||||||
/// provides the book metadata of the currently playing book
|
/// provides the book metadata of the currently playing book
|
||||||
///
|
///
|
||||||
|
|
@ -60,6 +64,8 @@ final currentBookMetadataProvider =
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef CurrentBookMetadataRef = AutoDisposeProviderRef<BookMetadataExpanded?>;
|
typedef CurrentBookMetadataRef = AutoDisposeProviderRef<BookMetadataExpanded?>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:miniplayer/miniplayer.dart';
|
import 'package:miniplayer/miniplayer.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
||||||
|
|
||||||
part 'player_form.g.dart';
|
part 'player_form.g.dart';
|
||||||
|
|
||||||
|
|
@ -26,7 +27,7 @@ extension on Ref {
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
Raw<ValueNotifier<double>> playerExpandProgressNotifier(
|
Raw<ValueNotifier<double>> playerExpandProgressNotifier(
|
||||||
PlayerExpandProgressNotifierRef ref,
|
Ref ref,
|
||||||
) {
|
) {
|
||||||
final ValueNotifier<double> playerExpandProgress =
|
final ValueNotifier<double> playerExpandProgress =
|
||||||
ValueNotifier(playerMinHeight);
|
ValueNotifier(playerMinHeight);
|
||||||
|
|
@ -46,7 +47,7 @@ Raw<ValueNotifier<double>> playerExpandProgressNotifier(
|
||||||
// a provider that will listen to the playerExpandProgressNotifier and return the percentage of the player expanded
|
// a provider that will listen to the playerExpandProgressNotifier and return the percentage of the player expanded
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
double playerHeight(
|
double playerHeight(
|
||||||
PlayerHeightRef ref,
|
Ref ref,
|
||||||
) {
|
) {
|
||||||
final playerExpandProgress = ref.watch(playerExpandProgressNotifierProvider);
|
final playerExpandProgress = ref.watch(playerExpandProgressNotifierProvider);
|
||||||
|
|
||||||
|
|
@ -60,3 +61,20 @@ double playerHeight(
|
||||||
}
|
}
|
||||||
|
|
||||||
final audioBookMiniplayerController = MiniplayerController();
|
final audioBookMiniplayerController = MiniplayerController();
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
bool isPlayerActive(
|
||||||
|
Ref ref,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
final player = ref.watch(audiobookPlayerProvider);
|
||||||
|
if (player.book != null) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
final playerHeight = ref.watch(playerHeightProvider);
|
||||||
|
return playerHeight < playerMinHeight;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ part of 'player_form.dart';
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$playerExpandProgressNotifierHash() =>
|
String _$playerExpandProgressNotifierHash() =>
|
||||||
r'e4817361b9a311b61ca23e51082ed11b0a1120ab';
|
r'1ac7172d90a070f96222286edd1a176be197f378';
|
||||||
|
|
||||||
/// See also [playerExpandProgressNotifier].
|
/// See also [playerExpandProgressNotifier].
|
||||||
@ProviderFor(playerExpandProgressNotifier)
|
@ProviderFor(playerExpandProgressNotifier)
|
||||||
|
|
@ -22,9 +22,11 @@ final playerExpandProgressNotifierProvider =
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef PlayerExpandProgressNotifierRef
|
typedef PlayerExpandProgressNotifierRef
|
||||||
= ProviderRef<Raw<ValueNotifier<double>>>;
|
= ProviderRef<Raw<ValueNotifier<double>>>;
|
||||||
String _$playerHeightHash() => r'26dbcb180d494575488d700bd5bdb58c02c224a9';
|
String _$playerHeightHash() => r'3f031eaffdffbb2c6ddf7eb1aba31bf1619260fc';
|
||||||
|
|
||||||
/// See also [playerHeight].
|
/// See also [playerHeight].
|
||||||
@ProviderFor(playerHeight)
|
@ProviderFor(playerHeight)
|
||||||
|
|
@ -37,6 +39,25 @@ final playerHeightProvider = Provider<double>.internal(
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
typedef PlayerHeightRef = ProviderRef<double>;
|
typedef PlayerHeightRef = ProviderRef<double>;
|
||||||
|
String _$isPlayerActiveHash() => r'2c7ca125423126fb5f0ef218d37bc8fe0ca9ec98';
|
||||||
|
|
||||||
|
/// See also [isPlayerActive].
|
||||||
|
@ProviderFor(isPlayerActive)
|
||||||
|
final isPlayerActiveProvider = Provider<bool>.internal(
|
||||||
|
isPlayerActive,
|
||||||
|
name: r'isPlayerActiveProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$isPlayerActiveHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||||
|
// ignore: unused_element
|
||||||
|
typedef IsPlayerActiveRef = ProviderRef<bool>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import 'package:vaani/features/player/providers/player_form.dart';
|
||||||
import 'package:vaani/settings/app_settings_provider.dart';
|
import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
import 'package:vaani/shared/extensions/inverse_lerp.dart';
|
import 'package:vaani/shared/extensions/inverse_lerp.dart';
|
||||||
import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
|
import 'package:vaani/shared/widgets/shelves/book_shelf.dart';
|
||||||
import 'package:vaani/theme/theme_from_cover_provider.dart';
|
import 'package:vaani/theme/providers/theme_from_cover_provider.dart';
|
||||||
|
|
||||||
import 'player_when_expanded.dart';
|
import 'player_when_expanded.dart';
|
||||||
import 'player_when_minimized.dart';
|
import 'player_when_minimized.dart';
|
||||||
|
|
@ -65,6 +65,8 @@ class AudiobookPlayer extends HookConsumerWidget {
|
||||||
themeOfLibraryItemProvider(
|
themeOfLibraryItemProvider(
|
||||||
itemBeingPlayed.valueOrNull?.id,
|
itemBeingPlayed.valueOrNull?.id,
|
||||||
brightness: Theme.of(context).brightness,
|
brightness: Theme.of(context).brightness,
|
||||||
|
highContrast: appSettings.themeSettings.highContrast ||
|
||||||
|
MediaQuery.of(context).highContrast,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
16
lib/features/player/view/mini_player_bottom_padding.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:vaani/features/player/providers/player_form.dart';
|
||||||
|
|
||||||
|
class MiniPlayerBottomPadding extends HookConsumerWidget {
|
||||||
|
const MiniPlayerBottomPadding({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: ref.watch(isPlayerActiveProvider)
|
||||||
|
? const SizedBox(height: playerMinHeight + 8)
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -104,8 +104,10 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color:
|
color: Theme.of(context)
|
||||||
Theme.of(context).colorScheme.primary.withOpacity(0.1),
|
.colorScheme
|
||||||
|
.primary
|
||||||
|
.withValues(alpha: 0.1),
|
||||||
blurRadius: 32 * earlyPercentage,
|
blurRadius: 32 * earlyPercentage,
|
||||||
spreadRadius: 8 * earlyPercentage,
|
spreadRadius: 8 * earlyPercentage,
|
||||||
// offset: Offset(0, 16 * earlyPercentage),
|
// offset: Offset(0, 16 * earlyPercentage),
|
||||||
|
|
@ -171,7 +173,7 @@ class PlayerWhenExpanded extends HookConsumerWidget {
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.onSurface
|
.onSurface
|
||||||
.withOpacity(0.7),
|
.withValues(alpha: 0.7),
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,7 @@ class PlayerWhenMinimized extends HookConsumerWidget {
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
.colorScheme
|
.colorScheme
|
||||||
.onSurface
|
.onSurface
|
||||||
.withOpacity(0.7),
|
.withValues(alpha: 0.7),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
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:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
import 'package:vaani/features/player/providers/audiobook_player.dart'
|
||||||
import 'package:vaani/features/player/providers/currently_playing_provider.dart';
|
show audiobookPlayerProvider;
|
||||||
import 'package:vaani/features/player/view/player_when_expanded.dart';
|
import 'package:vaani/features/player/providers/currently_playing_provider.dart'
|
||||||
import 'package:vaani/main.dart';
|
show currentPlayingChapterProvider, currentlyPlayingBookProvider;
|
||||||
import 'package:vaani/shared/extensions/chapter.dart';
|
import 'package:vaani/features/player/view/player_when_expanded.dart'
|
||||||
import 'package:vaani/shared/extensions/duration_format.dart';
|
show pendingPlayerModals;
|
||||||
import 'package:vaani/shared/hooks.dart';
|
import 'package:vaani/features/player/view/widgets/playing_indicator_icon.dart';
|
||||||
|
import 'package:vaani/main.dart' show appLogger;
|
||||||
|
import 'package:vaani/shared/extensions/chapter.dart' show ChapterDuration;
|
||||||
|
import 'package:vaani/shared/extensions/duration_format.dart'
|
||||||
|
show DurationFormat;
|
||||||
|
import 'package:vaani/shared/hooks.dart' show useTimer;
|
||||||
|
|
||||||
class ChapterSelectionButton extends HookConsumerWidget {
|
class ChapterSelectionButton extends HookConsumerWidget {
|
||||||
const ChapterSelectionButton({
|
const ChapterSelectionButton({
|
||||||
|
|
@ -67,6 +72,7 @@ class ChapterSelectionModal extends HookConsumerWidget {
|
||||||
|
|
||||||
useTimer(scrollToCurrentChapter, 500.ms);
|
useTimer(scrollToCurrentChapter, 500.ms);
|
||||||
// useInterval(scrollToCurrentChapter, 500.ms);
|
// useInterval(scrollToCurrentChapter, 500.ms);
|
||||||
|
final theme = Theme.of(context);
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|
@ -81,24 +87,41 @@ class ChapterSelectionModal extends HookConsumerWidget {
|
||||||
child: currentBook?.chapters == null
|
child: currentBook?.chapters == null
|
||||||
? const Text('No chapters found')
|
? const Text('No chapters found')
|
||||||
: Column(
|
: Column(
|
||||||
children: [
|
children: currentBook!.chapters.map(
|
||||||
for (final chapter in currentBook!.chapters)
|
(chapter) {
|
||||||
ListTile(
|
final isCurrent = currentChapterIndex == chapter.id;
|
||||||
title: Text(chapter.title),
|
final isPlayed = currentChapterIndex != null &&
|
||||||
trailing: Text(
|
chapter.id < currentChapterIndex;
|
||||||
'(${chapter.duration.smartBinaryFormat})',
|
return ListTile(
|
||||||
),
|
autofocus: isCurrent,
|
||||||
selected: currentChapterIndex == chapter.id,
|
iconColor: isPlayed && !isCurrent
|
||||||
key: currentChapterIndex == chapter.id
|
? theme.disabledColor
|
||||||
? chapterKey
|
|
||||||
: null,
|
: null,
|
||||||
|
title: Text(
|
||||||
|
chapter.title,
|
||||||
|
style: isPlayed && !isCurrent
|
||||||
|
? TextStyle(color: theme.disabledColor)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'(${chapter.duration.smartBinaryFormat})',
|
||||||
|
style: isPlayed && !isCurrent
|
||||||
|
? TextStyle(color: theme.disabledColor)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
trailing: isCurrent
|
||||||
|
? const PlayingIndicatorIcon()
|
||||||
|
: const Icon(Icons.play_arrow),
|
||||||
|
selected: isCurrent,
|
||||||
|
key: isCurrent ? chapterKey : null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
notifier.seek(chapter.start + 90.ms);
|
notifier.seek(chapter.start + 90.ms);
|
||||||
notifier.play();
|
notifier.play();
|
||||||
},
|
},
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
|
).toList(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
194
lib/features/player/view/widgets/playing_indicator_icon.dart
Normal file
|
|
@ -0,0 +1,194 @@
|
||||||
|
import 'dart:math';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
|
|
||||||
|
/// An icon that animates like audio equalizer bars to indicate playback.
|
||||||
|
///
|
||||||
|
/// Creates multiple vertical bars that independently animate their height
|
||||||
|
/// in a looping, visually dynamic pattern.
|
||||||
|
class PlayingIndicatorIcon extends StatefulWidget {
|
||||||
|
/// The number of vertical bars in the indicator.
|
||||||
|
final int barCount;
|
||||||
|
|
||||||
|
/// The total width and height of the icon area.
|
||||||
|
final double size;
|
||||||
|
|
||||||
|
/// The color of the bars. Defaults to the current [IconTheme] color.
|
||||||
|
final Color? color;
|
||||||
|
|
||||||
|
/// The minimum height factor for a bar (relative to [size]).
|
||||||
|
/// When [centerSymmetric] is true, this represents the minimum height
|
||||||
|
/// extending from the center line (so total minimum height is 2 * minHeightFactor * size).
|
||||||
|
/// When false, it's the minimum height from the bottom.
|
||||||
|
final double minHeightFactor;
|
||||||
|
|
||||||
|
/// The maximum height factor for a bar (relative to [size]).
|
||||||
|
/// When [centerSymmetric] is true, this represents the maximum height
|
||||||
|
/// extending from the center line (so total maximum height is 2 * maxHeightFactor * size).
|
||||||
|
/// When false, it's the maximum height from the bottom.
|
||||||
|
final double maxHeightFactor;
|
||||||
|
|
||||||
|
/// Base duration for a full up/down animation cycle for a single bar.
|
||||||
|
/// Actual duration will vary slightly per bar.
|
||||||
|
final Duration baseCycleDuration;
|
||||||
|
|
||||||
|
/// If true, the bars animate symmetrically expanding/collapsing from the
|
||||||
|
/// horizontal center line. If false (default), they expand/collapse from
|
||||||
|
/// the bottom edge.
|
||||||
|
final bool centerSymmetric;
|
||||||
|
|
||||||
|
const PlayingIndicatorIcon({
|
||||||
|
super.key,
|
||||||
|
this.barCount = 4,
|
||||||
|
this.size = 20.0,
|
||||||
|
this.color,
|
||||||
|
this.minHeightFactor = 0.2,
|
||||||
|
this.maxHeightFactor = 1.0,
|
||||||
|
this.baseCycleDuration = const Duration(milliseconds: 350),
|
||||||
|
this.centerSymmetric = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PlayingIndicatorIcon> createState() => _PlayingIndicatorIconState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PlayingIndicatorIconState extends State<PlayingIndicatorIcon> {
|
||||||
|
late List<_BarAnimationParams> _animationParams;
|
||||||
|
final _random = Random();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_animationParams =
|
||||||
|
List.generate(widget.barCount, _createRandomParams, growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to generate random parameters for one bar's animation cycle
|
||||||
|
_BarAnimationParams _createRandomParams(int index) {
|
||||||
|
final duration1 =
|
||||||
|
(widget.baseCycleDuration * (0.8 + _random.nextDouble() * 0.4));
|
||||||
|
final duration2 =
|
||||||
|
(widget.baseCycleDuration * (0.8 + _random.nextDouble() * 0.4));
|
||||||
|
|
||||||
|
// Note: These factors represent the scale relative to the *half-height*
|
||||||
|
// if centerSymmetric is true, controlled by the alignment in scaleY.
|
||||||
|
final targetHeightFactor1 = widget.minHeightFactor +
|
||||||
|
_random.nextDouble() *
|
||||||
|
(widget.maxHeightFactor - widget.minHeightFactor);
|
||||||
|
final targetHeightFactor2 = widget.minHeightFactor +
|
||||||
|
_random.nextDouble() *
|
||||||
|
(widget.maxHeightFactor - widget.minHeightFactor);
|
||||||
|
|
||||||
|
// --- Random initial delay ---
|
||||||
|
final initialDelay =
|
||||||
|
(_random.nextDouble() * (widget.baseCycleDuration.inMilliseconds / 4))
|
||||||
|
.ms;
|
||||||
|
|
||||||
|
return _BarAnimationParams(
|
||||||
|
duration1: duration1,
|
||||||
|
duration2: duration2,
|
||||||
|
targetHeightFactor1: targetHeightFactor1,
|
||||||
|
targetHeightFactor2: targetHeightFactor2,
|
||||||
|
initialDelay: initialDelay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = widget.color ??
|
||||||
|
IconTheme.of(context).color ??
|
||||||
|
Theme.of(context).colorScheme.primary;
|
||||||
|
|
||||||
|
// --- Bar geometry calculation ---
|
||||||
|
final double totalSpacing = widget.size * 0.2;
|
||||||
|
// Ensure at least 1px spacing if size is very small
|
||||||
|
final double barSpacing = max(1.0, totalSpacing / (widget.barCount + 1));
|
||||||
|
final double availableWidthForBars =
|
||||||
|
widget.size - (barSpacing * (widget.barCount + 1));
|
||||||
|
final double barWidth = max(1.0, availableWidthForBars / widget.barCount);
|
||||||
|
// Max height remains the full size potential for the container
|
||||||
|
final double maxHeight = widget.size;
|
||||||
|
|
||||||
|
// Determine the alignment for scaling based on the symmetric flag
|
||||||
|
final Alignment scaleAlignment =
|
||||||
|
widget.centerSymmetric ? Alignment.center : Alignment.bottomCenter;
|
||||||
|
|
||||||
|
// Determine the cross axis alignment for the Row
|
||||||
|
final CrossAxisAlignment rowAlignment = widget.centerSymmetric
|
||||||
|
? CrossAxisAlignment.center
|
||||||
|
: CrossAxisAlignment.end;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: widget.size,
|
||||||
|
height: widget.size,
|
||||||
|
// Clip ensures bars don't draw outside the SizedBox bounds
|
||||||
|
// especially important for center alignment if maxFactor > 0.5
|
||||||
|
child: ClipRect(
|
||||||
|
child: Row(
|
||||||
|
// Use calculated alignment
|
||||||
|
crossAxisAlignment: rowAlignment,
|
||||||
|
// Use spaceEvenly for better distribution, especially with center alignment
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: List.generate(
|
||||||
|
widget.barCount,
|
||||||
|
(index) {
|
||||||
|
final params = _animationParams[index];
|
||||||
|
// The actual bar widget that will be animated
|
||||||
|
return Container(
|
||||||
|
width: barWidth,
|
||||||
|
// Set initial height to the max potential height
|
||||||
|
// The scaleY animation will control the visible height
|
||||||
|
height: maxHeight,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadius.circular(barWidth / 2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.animate(
|
||||||
|
delay: params.initialDelay,
|
||||||
|
onPlay: (controller) => controller.repeat(
|
||||||
|
reverse: true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// 1. Scale to targetHeightFactor1
|
||||||
|
.scaleY(
|
||||||
|
begin:
|
||||||
|
widget.minHeightFactor, // Scale factor starts near min
|
||||||
|
end: params.targetHeightFactor1,
|
||||||
|
duration: params.duration1,
|
||||||
|
curve: Curves.easeInOutCirc,
|
||||||
|
alignment: scaleAlignment, // Apply chosen alignment
|
||||||
|
)
|
||||||
|
// 2. Then scale to targetHeightFactor2
|
||||||
|
.then()
|
||||||
|
.scaleY(
|
||||||
|
end: params.targetHeightFactor2,
|
||||||
|
duration: params.duration2,
|
||||||
|
curve: Curves.easeInOutCirc,
|
||||||
|
alignment: scaleAlignment, // Apply chosen alignment
|
||||||
|
);
|
||||||
|
},
|
||||||
|
growable: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper class: Renamed height fields for clarity
|
||||||
|
class _BarAnimationParams {
|
||||||
|
final Duration duration1;
|
||||||
|
final Duration duration2;
|
||||||
|
final double targetHeightFactor1; // Factor relative to total size
|
||||||
|
final double targetHeightFactor2; // Factor relative to total size
|
||||||
|
final Duration initialDelay;
|
||||||
|
|
||||||
|
_BarAnimationParams({
|
||||||
|
required this.duration1,
|
||||||
|
required this.duration2,
|
||||||
|
required this.targetHeightFactor1,
|
||||||
|
required this.targetHeightFactor2,
|
||||||
|
required this.initialDelay,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart';
|
import 'package:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart';
|
||||||
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
import 'package:vaani/features/player/providers/audiobook_player.dart';
|
||||||
import 'package:vaani/settings/app_settings_provider.dart';
|
import 'package:vaani/settings/app_settings_provider.dart';
|
||||||
import 'package:vaani/shared/hooks.dart';
|
|
||||||
|
|
||||||
const double itemExtent = 25;
|
const double itemExtent = 25;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,4 +23,4 @@ final shakeDetectorProvider =
|
||||||
|
|
||||||
typedef _$ShakeDetector = AutoDisposeNotifier<core.ShakeDetector?>;
|
typedef _$ShakeDetector = AutoDisposeNotifier<core.ShakeDetector?>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -22,4 +22,4 @@ final sleepTimerProvider =
|
||||||
|
|
||||||
typedef _$SleepTimer = Notifier<core.SleepTimer?>;
|
typedef _$SleepTimer = Notifier<core.SleepTimer?>;
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart'
|
||||||
import 'package:vaani/main.dart';
|
import 'package:vaani/main.dart';
|
||||||
import 'package:vaani/settings/app_settings_provider.dart';
|
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/hooks.dart';
|
|
||||||
|
|
||||||
class SleepTimerButton extends HookConsumerWidget {
|
class SleepTimerButton extends HookConsumerWidget {
|
||||||
const SleepTimerButton({
|
const SleepTimerButton({
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,22 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.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/api_provider.dart';
|
import 'package:vaani/api/api_provider.dart' show makeBaseUrl;
|
||||||
import 'package:vaani/api/authenticated_user_provider.dart';
|
import 'package:vaani/api/authenticated_users_provider.dart'
|
||||||
import 'package:vaani/api/server_provider.dart';
|
show authenticatedUsersProvider;
|
||||||
import 'package:vaani/main.dart';
|
import 'package:vaani/api/server_provider.dart'
|
||||||
import 'package:vaani/models/error_response.dart';
|
show ServerAlreadyExistsException, audiobookShelfServerProvider;
|
||||||
import 'package:vaani/router/router.dart';
|
import 'package:vaani/features/onboarding/view/user_login.dart'
|
||||||
import 'package:vaani/settings/api_settings_provider.dart';
|
show UserLoginWidget;
|
||||||
|
import 'package:vaani/features/player/view/mini_player_bottom_padding.dart'
|
||||||
|
show MiniPlayerBottomPadding;
|
||||||
|
import 'package:vaani/main.dart' show appLogger;
|
||||||
|
import 'package:vaani/router/router.dart' show Routes;
|
||||||
|
import 'package:vaani/settings/api_settings_provider.dart'
|
||||||
|
show apiSettingsProvider;
|
||||||
import 'package:vaani/settings/models/models.dart' as model;
|
import 'package:vaani/settings/models/models.dart' as model;
|
||||||
import 'package:vaani/shared/extensions/obfuscation.dart';
|
import 'package:vaani/shared/extensions/obfuscation.dart' show ObfuscateSet;
|
||||||
import 'package:vaani/shared/widgets/add_new_server.dart';
|
import 'package:vaani/shared/widgets/add_new_server.dart' show AddNewServer;
|
||||||
|
|
||||||
class ServerManagerPage extends HookConsumerWidget {
|
class ServerManagerPage extends HookConsumerWidget {
|
||||||
const ServerManagerPage({
|
const ServerManagerPage({
|
||||||
|
|
@ -20,15 +26,6 @@ class ServerManagerPage extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final apiSettings = ref.watch(apiSettingsProvider);
|
|
||||||
final registeredServers = ref.watch(audiobookShelfServerProvider);
|
|
||||||
final registeredServersAsList = registeredServers.toList();
|
|
||||||
final availableUsers = ref.watch(authenticatedUserProvider);
|
|
||||||
final serverURIController = useTextEditingController();
|
|
||||||
final formKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
appLogger.fine('registered servers: ${registeredServers.obfuscate()}');
|
|
||||||
appLogger.fine('available users: ${availableUsers.obfuscate()}');
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Manage Accounts'),
|
title: const Text('Manage Accounts'),
|
||||||
|
|
@ -36,240 +33,118 @@ class ServerManagerPage extends HookConsumerWidget {
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(8.0),
|
padding: const EdgeInsets.all(8.0),
|
||||||
child: Column(
|
child: ServerManagerBody(),
|
||||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
'Registered Servers',
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: ListView.builder(
|
|
||||||
itemCount: registeredServers.length,
|
|
||||||
reverse: true,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
var registeredServer = registeredServersAsList[index];
|
|
||||||
return ExpansionTile(
|
|
||||||
title: Text(registeredServer.serverUrl.toString()),
|
|
||||||
subtitle: Text(
|
|
||||||
'Users: ${availableUsers.where((element) => element.server == registeredServer).length}',
|
|
||||||
),
|
|
||||||
// trailing: _DeleteServerButton(
|
|
||||||
// registeredServer: registeredServer,
|
|
||||||
// ),
|
|
||||||
// children are list of users of this server
|
|
||||||
children: availableUsers
|
|
||||||
.where(
|
|
||||||
(element) => element.server == registeredServer,
|
|
||||||
)
|
|
||||||
.map(
|
|
||||||
(e) => ListTile(
|
|
||||||
selected: apiSettings.activeUser == e,
|
|
||||||
leading: apiSettings.activeUser == e
|
|
||||||
? const Icon(Icons.person)
|
|
||||||
: const Icon(Icons.person_off_outlined),
|
|
||||||
title: Text(e.username ?? 'Anonymous'),
|
|
||||||
onTap: apiSettings.activeUser == e
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
ref
|
|
||||||
.read(apiSettingsProvider.notifier)
|
|
||||||
.updateState(
|
|
||||||
apiSettings.copyWith(
|
|
||||||
activeUser: e,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
// pop all routes and go to the home page
|
|
||||||
// while (context.canPop()) {
|
|
||||||
// context.pop();
|
|
||||||
// }
|
|
||||||
context.goNamed(
|
|
||||||
Routes.home.name,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
trailing: IconButton(
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
onPressed: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Delete User'),
|
|
||||||
content: const Text(
|
|
||||||
'Are you sure you want to delete this user?',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
ref
|
|
||||||
.read(
|
|
||||||
authenticatedUserProvider
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.removeUser(e);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: const Text('Delete'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.nonNulls
|
|
||||||
.toList()
|
|
||||||
|
|
||||||
// add buttons of delete server and add user to server at the end
|
|
||||||
..addAll([
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.person_add),
|
|
||||||
title: const Text('Add User'),
|
|
||||||
onTap: () async {
|
|
||||||
// open a dialog to add a new user with username and password or another method using only auth token
|
|
||||||
final addedUser = await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return _AddUserDialog(
|
|
||||||
server: registeredServer,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// if (addedUser != null) {
|
|
||||||
// // show a snackbar that the user has been added and ask if change to this user
|
|
||||||
// ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
// SnackBar(
|
|
||||||
// content: const Text(
|
|
||||||
// 'User added successfully, do you want to switch to this user?',
|
|
||||||
// ),
|
|
||||||
// action: SnackBarAction(
|
|
||||||
// label: 'Switch',
|
|
||||||
// onPressed: () {
|
|
||||||
// // set the active user
|
|
||||||
// ref
|
|
||||||
// .read(apiSettingsProvider.notifier)
|
|
||||||
// .updateState(
|
|
||||||
// apiSettings.copyWith(
|
|
||||||
// activeUser: addedUser,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
|
|
||||||
// context.goNamed(Routes.home.name);
|
|
||||||
// },
|
|
||||||
// ),
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.delete),
|
|
||||||
title: const Text('Delete Server'),
|
|
||||||
onTap: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: const Text('Delete Server'),
|
|
||||||
content: const Text(
|
|
||||||
'Are you sure you want to delete this server and all its users?',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
ref
|
|
||||||
.read(
|
|
||||||
audiobookShelfServerProvider
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.removeServer(
|
|
||||||
registeredServer,
|
|
||||||
removeUsers: true,
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
child: const Text('Delete'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
const Padding(
|
|
||||||
padding: EdgeInsets.all(8.0),
|
|
||||||
child: Text('Add New Server'),
|
|
||||||
),
|
|
||||||
Form(
|
|
||||||
key: formKey,
|
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
|
||||||
child: AddNewServer(
|
|
||||||
controller: serverURIController,
|
|
||||||
onPressed: () {
|
|
||||||
if (formKey.currentState!.validate()) {
|
|
||||||
try {
|
|
||||||
final newServer = model.AudiobookShelfServer(
|
|
||||||
serverUrl: makeBaseUrl(serverURIController.text),
|
|
||||||
);
|
|
||||||
ref
|
|
||||||
.read(audiobookShelfServerProvider.notifier)
|
|
||||||
.addServer(
|
|
||||||
newServer,
|
|
||||||
);
|
|
||||||
ref.read(apiSettingsProvider.notifier).updateState(
|
|
||||||
apiSettings.copyWith(
|
|
||||||
activeServer: newServer,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
serverURIController.clear();
|
|
||||||
} on ServerAlreadyExistsException catch (e) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(e.toString()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Invalid URL'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AddUserDialog extends HookConsumerWidget {
|
class ServerManagerBody extends HookConsumerWidget {
|
||||||
const _AddUserDialog({
|
const ServerManagerBody({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final registeredServers = ref.watch(audiobookShelfServerProvider);
|
||||||
|
final registeredServersAsList = registeredServers.toList();
|
||||||
|
final availableUsers = ref.watch(authenticatedUsersProvider);
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
final serverURIController = useTextEditingController();
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
appLogger.fine('registered servers: ${registeredServers.obfuscate()}');
|
||||||
|
appLogger.fine('available users: ${availableUsers.obfuscate()}');
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Registered Servers',
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: registeredServers.length,
|
||||||
|
reverse: true,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
var registeredServer = registeredServersAsList[index];
|
||||||
|
return ExpansionTile(
|
||||||
|
title: Text(registeredServer.serverUrl.toString()),
|
||||||
|
subtitle: Text(
|
||||||
|
'Users: ${availableUsers.where((element) => element.server == registeredServer).length}',
|
||||||
|
),
|
||||||
|
// children are list of users of this server
|
||||||
|
children: availableUsers
|
||||||
|
.where(
|
||||||
|
(element) => element.server == registeredServer,
|
||||||
|
)
|
||||||
|
.map<Widget>(
|
||||||
|
(e) => AvailableUserTile(user: e),
|
||||||
|
)
|
||||||
|
.nonNulls
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
// add buttons of delete server and add user to server at the end
|
||||||
|
..addAll([
|
||||||
|
AddUserTile(server: registeredServer),
|
||||||
|
DeleteServerTile(server: registeredServer),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Text('Add New Server'),
|
||||||
|
),
|
||||||
|
Form(
|
||||||
|
key: formKey,
|
||||||
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
|
child: AddNewServer(
|
||||||
|
controller: serverURIController,
|
||||||
|
onPressed: () {
|
||||||
|
if (formKey.currentState!.validate()) {
|
||||||
|
try {
|
||||||
|
final newServer = model.AudiobookShelfServer(
|
||||||
|
serverUrl: makeBaseUrl(serverURIController.text),
|
||||||
|
);
|
||||||
|
ref.read(audiobookShelfServerProvider.notifier).addServer(
|
||||||
|
newServer,
|
||||||
|
);
|
||||||
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
apiSettings.copyWith(
|
||||||
|
activeServer: newServer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
serverURIController.clear();
|
||||||
|
} on ServerAlreadyExistsException catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(e.toString()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Invalid URL'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MiniPlayerBottomPadding(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeleteServerTile extends HookConsumerWidget {
|
||||||
|
const DeleteServerTile({
|
||||||
super.key,
|
super.key,
|
||||||
required this.server,
|
required this.server,
|
||||||
});
|
});
|
||||||
|
|
@ -278,178 +153,220 @@ class _AddUserDialog extends HookConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final usernameController = useTextEditingController();
|
return ListTile(
|
||||||
final passwordController = useTextEditingController();
|
leading: const Icon(Icons.delete),
|
||||||
final authTokensController = useTextEditingController();
|
title: const Text('Delete Server'),
|
||||||
final isPasswordVisible = useState(false);
|
onTap: () {
|
||||||
final apiSettings = ref.watch(apiSettingsProvider);
|
showDialog(
|
||||||
final isMethodAuth = useState(false);
|
context: context,
|
||||||
final api = ref.watch(audiobookshelfApiProvider(server.serverUrl));
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
final formKey = GlobalKey<FormState>();
|
title: const Text('Remove Server and Users'),
|
||||||
|
// Make content scrollable in case of smaller screens/keyboard
|
||||||
final serverErrorResponse = ErrorResponseHandler();
|
content: SingleChildScrollView(
|
||||||
|
child: Text.rich(
|
||||||
/// Login to the server and save the user
|
TextSpan(
|
||||||
Future<model.AuthenticatedUser?> loginAndSave() async {
|
children: [
|
||||||
model.AuthenticatedUser? authenticatedUser;
|
const TextSpan(
|
||||||
if (isMethodAuth.value) {
|
text: 'This will remove the server ',
|
||||||
api.token = authTokensController.text;
|
),
|
||||||
final success = await api.misc.authorize(
|
TextSpan(
|
||||||
responseErrorHandler: serverErrorResponse.storeError,
|
text: server.serverUrl.host,
|
||||||
);
|
style: TextStyle(
|
||||||
if (success != null) {
|
fontWeight: FontWeight.bold,
|
||||||
authenticatedUser = model.AuthenticatedUser(
|
color: Theme.of(context).colorScheme.primary,
|
||||||
server: server,
|
),
|
||||||
id: success.user.id,
|
),
|
||||||
username: success.user.username,
|
const TextSpan(
|
||||||
authToken: api.token!,
|
text: ' and all its users\' login info from this app.',
|
||||||
);
|
),
|
||||||
}
|
],
|
||||||
} else {
|
|
||||||
final username = usernameController.text;
|
|
||||||
final password = passwordController.text;
|
|
||||||
final success = await api.login(
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
responseErrorHandler: serverErrorResponse.storeError,
|
|
||||||
);
|
|
||||||
if (success != null) {
|
|
||||||
authenticatedUser = model.AuthenticatedUser(
|
|
||||||
server: server,
|
|
||||||
id: success.user.id,
|
|
||||||
username: username,
|
|
||||||
authToken: api.token!,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// add the user to the list of users
|
|
||||||
if (authenticatedUser != null) {
|
|
||||||
ref.read(authenticatedUserProvider.notifier).addUser(authenticatedUser);
|
|
||||||
} else {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
'Login failed. Got response: ${serverErrorResponse.response.body} (${serverErrorResponse.response.statusCode})',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return authenticatedUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AlertDialog(
|
|
||||||
// title: Text('Add User for ${server.serverUrl}'),
|
|
||||||
title: Text.rich(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: 'Add User for ',
|
|
||||||
style: Theme.of(context).textTheme.labelLarge,
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: server.serverUrl.toString(),
|
|
||||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
content: Form(
|
|
||||||
key: formKey,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Wrap(
|
|
||||||
alignment: WrapAlignment.center,
|
|
||||||
spacing: 8.0,
|
|
||||||
children: [
|
|
||||||
ChoiceChip(
|
|
||||||
label: const Text('Username/Password'),
|
|
||||||
selected: !isMethodAuth.value,
|
|
||||||
onSelected: (selected) {
|
|
||||||
isMethodAuth.value = !selected;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ChoiceChip(
|
),
|
||||||
label: const Text('Auth Token'),
|
actions: [
|
||||||
selected: isMethodAuth.value,
|
TextButton(
|
||||||
onSelected: (selected) {
|
onPressed: () {
|
||||||
isMethodAuth.value = selected;
|
Navigator.of(context).pop();
|
||||||
},
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
audiobookShelfServerProvider.notifier,
|
||||||
|
)
|
||||||
|
.removeServer(
|
||||||
|
server,
|
||||||
|
removeUsers: true,
|
||||||
|
);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Delete'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (isMethodAuth.value)
|
|
||||||
TextFormField(
|
|
||||||
controller: authTokensController,
|
|
||||||
decoration: const InputDecoration(labelText: 'Auth Token'),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Please enter an auth token';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
)
|
|
||||||
else ...[
|
|
||||||
TextFormField(
|
|
||||||
controller: usernameController,
|
|
||||||
decoration: const InputDecoration(labelText: 'Username'),
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Please enter a username';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: passwordController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'Password',
|
|
||||||
suffixIcon: IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
isPasswordVisible.value
|
|
||||||
? Icons.visibility
|
|
||||||
: Icons.visibility_off,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
isPasswordVisible.value = !isPasswordVisible.value;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
obscureText: !isPasswordVisible.value,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) {
|
|
||||||
return 'Please enter a password';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
},
|
||||||
child: const Text('Cancel'),
|
);
|
||||||
),
|
},
|
||||||
ElevatedButton(
|
);
|
||||||
onPressed: () async {
|
}
|
||||||
if (formKey.currentState!.validate()) {
|
}
|
||||||
final addedUser = await loginAndSave();
|
|
||||||
if (addedUser != null) {
|
class AddUserTile extends HookConsumerWidget {
|
||||||
Navigator.of(context).pop(addedUser);
|
const AddUserTile({
|
||||||
}
|
super.key,
|
||||||
}
|
required this.server,
|
||||||
},
|
});
|
||||||
child: const Text('Add User'),
|
|
||||||
),
|
final model.AudiobookShelfServer server;
|
||||||
],
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.person_add),
|
||||||
|
title: const Text('Add User'),
|
||||||
|
onTap: () async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
// barrierDismissible: false, // Optional: prevent closing by tapping outside
|
||||||
|
builder: (dialogContext) {
|
||||||
|
// Use a different context name to avoid conflicts
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text('Add User to ${server.serverUrl.host}'),
|
||||||
|
// Make content scrollable in case of smaller screens/keyboard
|
||||||
|
content: SingleChildScrollView(
|
||||||
|
child: UserLoginWidget(
|
||||||
|
server: server.serverUrl,
|
||||||
|
// Pass the callback to pop the dialog on success
|
||||||
|
onSuccess: (user) {
|
||||||
|
// Add the user to the server
|
||||||
|
ref.read(authenticatedUsersProvider.notifier).addUser(user);
|
||||||
|
Navigator.of(dialogContext).pop(); // Close the dialog
|
||||||
|
// Optional: Show a confirmation SnackBar
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('User added successfully! Switch?'),
|
||||||
|
action: SnackBarAction(
|
||||||
|
label: 'Switch',
|
||||||
|
onPressed: () {
|
||||||
|
// Switch to the new user
|
||||||
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
ref.read(apiSettingsProvider).copyWith(
|
||||||
|
activeUser: user,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
context.goNamed(Routes.home.name);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(dialogContext).pop(); // Close the dialog
|
||||||
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// No need for the SnackBar asking to switch user here anymore.
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AvailableUserTile extends HookConsumerWidget {
|
||||||
|
const AvailableUserTile({
|
||||||
|
super.key,
|
||||||
|
required this.user,
|
||||||
|
});
|
||||||
|
|
||||||
|
final model.AuthenticatedUser user;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
selected: apiSettings.activeUser == user,
|
||||||
|
leading: apiSettings.activeUser == user
|
||||||
|
? const Icon(Icons.person)
|
||||||
|
: const Icon(Icons.person_off_outlined),
|
||||||
|
title: Text(user.username ?? 'Anonymous'),
|
||||||
|
onTap: apiSettings.activeUser == user
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
apiSettings.copyWith(
|
||||||
|
activeUser: user,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
// pop all routes and go to the home page
|
||||||
|
// while (context.canPop()) {
|
||||||
|
// context.pop();
|
||||||
|
// }
|
||||||
|
context.goNamed(
|
||||||
|
Routes.home.name,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.delete),
|
||||||
|
onPressed: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Remove User Login'),
|
||||||
|
content: Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: [
|
||||||
|
const TextSpan(
|
||||||
|
text: 'This will remove login details of the user ',
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: user.username ?? 'Anonymous',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const TextSpan(
|
||||||
|
text: ' from this app.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
authenticatedUsersProvider.notifier,
|
||||||
|
)
|
||||||
|
.removeUser(user);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Delete'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
225
lib/features/you/view/widgets/library_switch_chip.dart
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shelfsdk/audiobookshelf_api.dart' show Library;
|
||||||
|
import 'package:vaani/api/library_provider.dart';
|
||||||
|
import 'package:vaani/settings/api_settings_provider.dart'
|
||||||
|
show apiSettingsProvider;
|
||||||
|
import 'package:vaani/shared/icons/abs_icons.dart';
|
||||||
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:vaani/main.dart' show appLogger;
|
||||||
|
|
||||||
|
class LibrarySwitchChip extends HookConsumerWidget {
|
||||||
|
const LibrarySwitchChip({
|
||||||
|
super.key,
|
||||||
|
required this.libraries,
|
||||||
|
});
|
||||||
|
final List<Library> libraries;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final apiSettings = ref.watch(apiSettingsProvider);
|
||||||
|
|
||||||
|
return ActionChip(
|
||||||
|
avatar: Icon(
|
||||||
|
AbsIcons.getIconByName(
|
||||||
|
apiSettings.activeLibraryId != null
|
||||||
|
? libraries
|
||||||
|
.firstWhere(
|
||||||
|
(lib) => lib.id == apiSettings.activeLibraryId,
|
||||||
|
)
|
||||||
|
.icon
|
||||||
|
: libraries.first.icon,
|
||||||
|
),
|
||||||
|
), // Replace with your icon
|
||||||
|
label: const Text('Change Library'),
|
||||||
|
// Enable only if libraries are loaded and not empty
|
||||||
|
onPressed: libraries.isNotEmpty
|
||||||
|
? () => showLibrarySwitcher(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
)
|
||||||
|
: null, // Disable if no libraries
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helper Function to Show the Switcher ---
|
||||||
|
void showLibrarySwitcher(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
) {
|
||||||
|
final content = _LibrarySelectionContent();
|
||||||
|
|
||||||
|
// --- Platform-Specific UI ---
|
||||||
|
bool isDesktop = false;
|
||||||
|
if (!kIsWeb) {
|
||||||
|
// dart:io Platform is not available on web
|
||||||
|
isDesktop = Platform.isLinux || Platform.isMacOS || Platform.isWindows;
|
||||||
|
} else {
|
||||||
|
// Basic web detection (might need refinement based on screen size)
|
||||||
|
// Consider using MediaQuery for a size-based check instead for web/tablet
|
||||||
|
final size = MediaQuery.of(context).size;
|
||||||
|
isDesktop = size.width > 600; // Example threshold for "desktop-like" layout
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDesktop) {
|
||||||
|
// --- Desktop: Use AlertDialog ---
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => AlertDialog(
|
||||||
|
title: const Text('Select Library'),
|
||||||
|
content: SizedBox(
|
||||||
|
// Constrain size for dialogs
|
||||||
|
width: 300, // Adjust as needed
|
||||||
|
// Make content scrollable if list is long
|
||||||
|
child: Scrollbar(child: content),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Invalidate the provider to trigger a refetch
|
||||||
|
ref.invalidate(librariesProvider);
|
||||||
|
Navigator.pop(dialogContext);
|
||||||
|
},
|
||||||
|
child: const Text('Refresh'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(dialogContext),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// --- Mobile/Tablet: Use BottomSheet ---
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
// Make it scrollable and control height
|
||||||
|
isScrollControlled: true,
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxHeight:
|
||||||
|
MediaQuery.of(context).size.height * 0.6, // Max 60% of screen
|
||||||
|
),
|
||||||
|
builder: (sheetContext) => Padding(
|
||||||
|
// Add padding within the bottom sheet
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min, // Take minimum necessary height
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Select Library',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
const Divider(),
|
||||||
|
Flexible(
|
||||||
|
// Allow the list to take remaining space and scroll
|
||||||
|
child: Scrollbar(child: content),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('Refresh'),
|
||||||
|
onPressed: () {
|
||||||
|
// Invalidate the provider to trigger a refetch
|
||||||
|
ref.invalidate(librariesProvider);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Widget for the Selection List Content (Reusable) ---
|
||||||
|
class _LibrarySelectionContent extends ConsumerWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final librariesAsyncValue = ref.watch(librariesProvider);
|
||||||
|
final currentLibraryId = ref.watch(
|
||||||
|
apiSettingsProvider.select((settings) => settings.activeLibraryId),
|
||||||
|
);
|
||||||
|
final errorColor = Theme.of(context).colorScheme.error;
|
||||||
|
return librariesAsyncValue.when(
|
||||||
|
// --- Loading State ---
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
|
||||||
|
// --- Error State ---
|
||||||
|
error: (error, stackTrace) => Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, color: errorColor),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
'Error loading libraries: $error',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: errorColor),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
label: const Text('Retry'),
|
||||||
|
onPressed: () {
|
||||||
|
// Invalidate the provider to trigger a refetch
|
||||||
|
ref.invalidate(librariesProvider);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- Data State ---
|
||||||
|
data: (libraries) {
|
||||||
|
// Handle case where data loaded successfully but is empty
|
||||||
|
if (libraries.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: Text('No libraries available.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the list if libraries are available
|
||||||
|
return Scrollbar(
|
||||||
|
// Add scrollbar for potentially long lists
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true, // Important for Dialog/BottomSheet sizing
|
||||||
|
itemCount: libraries.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final library = libraries[index];
|
||||||
|
final bool isSelected = library.id == currentLibraryId;
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
title: Text(library.name),
|
||||||
|
leading: Icon(AbsIcons.getIconByName(library.icon)),
|
||||||
|
selected: isSelected,
|
||||||
|
trailing: isSelected ? const Icon(Icons.check) : null,
|
||||||
|
onTap: () {
|
||||||
|
appLogger.info(
|
||||||
|
'Selected library: ${library.name} (ID: ${library.id})',
|
||||||
|
);
|
||||||
|
// Get current settings state
|
||||||
|
final currentSettings = ref.read(apiSettingsProvider);
|
||||||
|
// Update the active library ID
|
||||||
|
ref.read(apiSettingsProvider.notifier).updateState(
|
||||||
|
currentSettings.copyWith(activeLibraryId: library.id),
|
||||||
|
);
|
||||||
|
// Close the dialog/bottom sheet
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||