├── .fvmrc
├── .github
├── actions
│ └── flutter-setup
│ │ └── action.yaml
├── labeler.yml
├── release-drafter.yml
└── workflows
│ ├── flutter-ci.yaml
│ ├── issue-labeler.yml
│ ├── prepare-release.yaml
│ └── release-drafter.yml
├── .gitignore
├── .gitmodules
├── .metadata
├── .vscode
├── launch.json
├── settings.json
└── tasks.json
├── CONTRIBUTING.md
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── whispering_pages
│ │ │ │ └── MainActivity.kt
│ │ ├── play_store_512.png
│ │ └── res
│ │ │ ├── drawable-hdpi
│ │ │ └── ic_stat_logo.png
│ │ │ ├── drawable-mdpi
│ │ │ └── ic_stat_logo.png
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable-xhdpi
│ │ │ └── ic_stat_logo.png
│ │ │ ├── drawable-xxhdpi
│ │ │ └── ic_stat_logo.png
│ │ │ ├── drawable-xxxhdpi
│ │ │ └── ic_stat_logo.png
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ └── ic_launcher.xml
│ │ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ ├── ic_launcher_monochrome.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ ├── ic_launcher_monochrome.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ ├── ic_launcher_monochrome.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ ├── ic_launcher_monochrome.png
│ │ │ └── launcher_icon.png
│ │ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ ├── ic_launcher_background.png
│ │ │ ├── ic_launcher_foreground.png
│ │ │ ├── ic_launcher_monochrome.png
│ │ │ └── launcher_icon.png
│ │ │ ├── raw
│ │ │ └── keep.xml
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
├── animations
│ └── Animation - 1714930099660.json
├── fonts
│ └── AbsIcons.ttf
├── icon
│ └── logo.png
├── images
│ ├── undraw_set_preferences_kwia.svg
│ ├── undraw_time_management_re_tk5w.svg
│ └── vaani_logo_foreground.png
└── sounds
│ └── beep.mp3
├── distribute_options.yaml
├── docs
├── images_and_logos.md
├── linux_build_guide.md
└── linux_deeplink.md
├── fastlane
├── Appfile
├── Fastfile
├── README.md
├── metadata
│ └── android
│ │ └── en-US
│ │ ├── full_description.txt
│ │ ├── images
│ │ ├── featureGraphic.png
│ │ ├── icon.png
│ │ └── phoneScreenshots
│ │ │ ├── 1_en-US.jpeg
│ │ │ ├── 2_en-US.jpeg
│ │ │ ├── 3_en-US.jpeg
│ │ │ ├── 4_en-US.jpeg
│ │ │ ├── 5_en-US.jpeg
│ │ │ ├── 6_en-US.jpeg
│ │ │ ├── 7_en-US.jpeg
│ │ │ └── 8_en-US.jpeg
│ │ ├── short_description.txt
│ │ ├── title.txt
│ │ └── video.txt
└── report.xml
├── images
├── banner.png
├── screenshots
│ └── android
│ │ ├── bookview.jpg
│ │ ├── home.jpg
│ │ └── player.jpg
└── vaani_logo.svg
├── lib
├── api
│ ├── api_provider.dart
│ ├── api_provider.g.dart
│ ├── authenticated_users_provider.dart
│ ├── authenticated_users_provider.g.dart
│ ├── image_provider.dart
│ ├── image_provider.g.dart
│ ├── library_item_provider.dart
│ ├── library_item_provider.g.dart
│ ├── library_provider.dart
│ ├── library_provider.g.dart
│ ├── server_provider.dart
│ └── server_provider.g.dart
├── constants
│ ├── hero_tag_conventions.dart
│ └── sizes.dart
├── db
│ ├── available_boxes.dart
│ ├── cache
│ │ ├── cache_key.dart
│ │ └── schemas
│ │ │ ├── image.dart
│ │ │ └── image.g.dart
│ ├── cache_manager.dart
│ ├── init.dart
│ ├── player_prefs
│ │ ├── book_prefs.dart
│ │ └── book_prefs.g.dart
│ ├── register_models.dart
│ └── storage.dart
├── features
│ ├── downloads
│ │ ├── core
│ │ │ └── download_manager.dart
│ │ ├── providers
│ │ │ ├── download_manager.dart
│ │ │ └── download_manager.g.dart
│ │ └── view
│ │ │ └── downloads_page.dart
│ ├── explore
│ │ ├── providers
│ │ │ ├── search_controller.dart
│ │ │ ├── search_controller.g.dart
│ │ │ ├── search_result_provider.dart
│ │ │ └── search_result_provider.g.dart
│ │ └── view
│ │ │ ├── explore_page.dart
│ │ │ └── search_result_page.dart
│ ├── item_viewer
│ │ └── view
│ │ │ ├── library_item_actions.dart
│ │ │ ├── library_item_hero_section.dart
│ │ │ ├── library_item_metadata.dart
│ │ │ ├── library_item_page.dart
│ │ │ └── library_item_sliver_app_bar.dart
│ ├── library_browser
│ │ └── view
│ │ │ └── library_browser_page.dart
│ ├── logging
│ │ ├── core
│ │ │ └── logger.dart
│ │ ├── providers
│ │ │ ├── logs_provider.dart
│ │ │ └── logs_provider.g.dart
│ │ └── view
│ │ │ └── logs_page.dart
│ ├── onboarding
│ │ ├── models
│ │ │ ├── flow.dart
│ │ │ └── flow.freezed.dart
│ │ ├── providers
│ │ │ ├── oauth_provider.dart
│ │ │ └── oauth_provider.g.dart
│ │ └── view
│ │ │ ├── callback_page.dart
│ │ │ ├── onboarding_single_page.dart
│ │ │ ├── user_login.dart
│ │ │ ├── user_login_with_open_id.dart
│ │ │ ├── user_login_with_password.dart
│ │ │ └── user_login_with_token.dart
│ ├── per_book_settings
│ │ ├── models
│ │ │ ├── book_settings.dart
│ │ │ ├── book_settings.freezed.dart
│ │ │ ├── book_settings.g.dart
│ │ │ ├── nullable_player_settings.dart
│ │ │ ├── nullable_player_settings.freezed.dart
│ │ │ └── nullable_player_settings.g.dart
│ │ └── providers
│ │ │ ├── book_settings_provider.dart
│ │ │ └── book_settings_provider.g.dart
│ ├── playback_reporting
│ │ ├── core
│ │ │ └── playback_reporter.dart
│ │ └── providers
│ │ │ ├── playback_reporter_provider.dart
│ │ │ └── playback_reporter_provider.g.dart
│ ├── player
│ │ ├── core
│ │ │ ├── audiobook_player.dart
│ │ │ └── init.dart
│ │ ├── playlist.dart
│ │ ├── playlist_provider.dart
│ │ ├── playlist_provider.g.dart
│ │ ├── providers
│ │ │ ├── audiobook_player.dart
│ │ │ ├── audiobook_player.g.dart
│ │ │ ├── currently_playing_provider.dart
│ │ │ ├── currently_playing_provider.g.dart
│ │ │ ├── player_form.dart
│ │ │ └── player_form.g.dart
│ │ └── view
│ │ │ ├── audiobook_player.dart
│ │ │ ├── mini_player_bottom_padding.dart
│ │ │ ├── player_when_expanded.dart
│ │ │ ├── player_when_minimized.dart
│ │ │ └── widgets
│ │ │ ├── audiobook_player_seek_button.dart
│ │ │ ├── audiobook_player_seek_chapter_button.dart
│ │ │ ├── chapter_selection_button.dart
│ │ │ ├── player_speed_adjust_button.dart
│ │ │ ├── playing_indicator_icon.dart
│ │ │ └── speed_selector.dart
│ ├── shake_detection
│ │ ├── core
│ │ │ └── shake_detector.dart
│ │ └── providers
│ │ │ ├── shake_detector.dart
│ │ │ └── shake_detector.g.dart
│ ├── sleep_timer
│ │ ├── core
│ │ │ └── sleep_timer.dart
│ │ ├── providers
│ │ │ ├── sleep_timer_provider.dart
│ │ │ └── sleep_timer_provider.g.dart
│ │ └── view
│ │ │ └── sleep_timer_button.dart
│ └── you
│ │ └── view
│ │ ├── server_manager.dart
│ │ ├── widgets
│ │ └── library_switch_chip.dart
│ │ └── you_page.dart
├── hacks
│ └── fix_autofill_losing_focus.dart
├── main.dart
├── models
│ └── error_response.dart
├── pages
│ ├── home_page.dart
│ └── library_page.dart
├── router
│ ├── constants.dart
│ ├── models
│ │ ├── library_item_extras.dart
│ │ └── library_item_extras.freezed.dart
│ ├── router.dart
│ ├── scaffold_with_nav_bar.dart
│ └── transitions
│ │ └── slide.dart
├── settings
│ ├── api_settings_provider.dart
│ ├── api_settings_provider.g.dart
│ ├── app_settings_provider.dart
│ ├── app_settings_provider.g.dart
│ ├── constants.dart
│ ├── metadata
│ │ ├── metadata_provider.dart
│ │ └── metadata_provider.g.dart
│ ├── models
│ │ ├── api_settings.dart
│ │ ├── api_settings.freezed.dart
│ │ ├── api_settings.g.dart
│ │ ├── app_settings.dart
│ │ ├── app_settings.freezed.dart
│ │ ├── app_settings.g.dart
│ │ ├── audiobookshelf_server.dart
│ │ ├── audiobookshelf_server.freezed.dart
│ │ ├── audiobookshelf_server.g.dart
│ │ ├── authenticated_user.dart
│ │ ├── authenticated_user.freezed.dart
│ │ ├── authenticated_user.g.dart
│ │ └── models.dart
│ ├── settings.dart
│ └── view
│ │ ├── app_settings_page.dart
│ │ ├── auto_sleep_timer_settings_page.dart
│ │ ├── buttons.dart
│ │ ├── home_page_settings_page.dart
│ │ ├── notification_settings_page.dart
│ │ ├── player_settings_page.dart
│ │ ├── shake_detector_settings_page.dart
│ │ ├── simple_settings_page.dart
│ │ ├── theme_settings_page.dart
│ │ └── widgets
│ │ └── navigation_with_switch_tile.dart
├── shared
│ ├── extensions
│ │ ├── chapter.dart
│ │ ├── duration_format.dart
│ │ ├── enum.dart
│ │ ├── inverse_lerp.dart
│ │ ├── item_files.dart
│ │ ├── model_conversions.dart
│ │ ├── obfuscation.dart
│ │ └── time_of_day.dart
│ ├── hooks.dart
│ ├── icons
│ │ └── abs_icons.dart
│ ├── utils.dart
│ └── widgets
│ │ ├── add_new_server.dart
│ │ ├── drawer.dart
│ │ ├── expandable_description.dart
│ │ ├── not_implemented.dart
│ │ ├── shelves
│ │ ├── author_shelf.dart
│ │ ├── book_shelf.dart
│ │ └── home_shelf.dart
│ │ └── vaani_logo.dart
└── theme
│ ├── providers
│ ├── system_theme_provider.dart
│ ├── system_theme_provider.g.dart
│ ├── theme_from_cover_provider.dart
│ └── theme_from_cover_provider.g.dart
│ └── theme.dart
├── linux
├── .gitignore
├── CMakeLists.txt
├── flutter
│ ├── CMakeLists.txt
│ ├── generated_plugin_registrant.cc
│ ├── generated_plugin_registrant.h
│ └── generated_plugins.cmake
├── main.cc
├── my_application.cc
├── my_application.h
└── packaging
│ ├── appimage
│ └── make_config.yaml
│ └── deb
│ └── make_config.yaml
├── privacy-policy.md
├── pubspec.lock
├── pubspec.yaml
├── test
├── features
│ └── logging
│ │ └── providers
│ │ └── logs_provider_test.dart
└── shared
│ └── extensions
│ └── time_of_day_test.dart
├── web
├── favicon.png
├── icons
│ ├── Icon-192.png
│ ├── Icon-512.png
│ ├── Icon-maskable-192.png
│ └── Icon-maskable-512.png
├── index.html
└── manifest.json
└── windows
├── .gitignore
├── CMakeLists.txt
├── flutter
├── CMakeLists.txt
├── generated_plugin_registrant.cc
├── generated_plugin_registrant.h
└── generated_plugins.cmake
└── runner
├── CMakeLists.txt
├── Runner.rc
├── flutter_window.cpp
├── flutter_window.h
├── main.cpp
├── resource.h
├── resources
└── app_icon.ico
├── runner.exe.manifest
├── utils.cpp
├── utils.h
├── win32_window.cpp
└── win32_window.h
/.fvmrc:
--------------------------------------------------------------------------------
1 | {
2 | "flutter": "3.32.0"
3 | }
--------------------------------------------------------------------------------
/.github/actions/flutter-setup/action.yaml:
--------------------------------------------------------------------------------
1 | # .github/actions/flutter-setup/action.yml
2 | name: "Flutter Setup Composite Action"
3 | description: "Checks out code, sets up Java/Flutter, caches, and runs pub get"
4 |
5 | # Define inputs for customization (optional, but good practice)
6 | inputs:
7 | flutter-channel:
8 | description: "Flutter channel to use (stable, beta, dev, master)"
9 | required: false
10 | default: "stable"
11 | java-version:
12 | description: "Java version to set up"
13 | required: false
14 | default: "17"
15 |
16 | runs:
17 | using: "composite" # Specify this is a composite action
18 | steps:
19 | - name: Set up Java
20 | uses: actions/setup-java@v4
21 | with:
22 | distribution: "temurin"
23 | java-version: ${{ inputs.java-version }}
24 |
25 | - name: Set up Flutter SDK
26 | uses: subosito/flutter-action@v2
27 | with:
28 | channel: ${{ inputs.flutter-channel }}
29 | flutter-version-file: pubspec.yaml
30 | cache: true # Cache Flutter SDK itself
31 |
32 | - name: Cache Flutter dependencies
33 | id: cache-pub
34 | uses: actions/cache@v4
35 | with:
36 | path: ${{ env.FLUTTER_HOME }}/.pub-cache
37 | key: ${{ runner.os }}-flutter-pub-${{ hashFiles('**/pubspec.lock') }}
38 | restore-keys: |
39 | ${{ runner.os }}-flutter-pub-
40 |
41 | - name: Get Flutter dependencies
42 | run: flutter pub get
43 | # Use shell: bash for potential cross-platform compatibility in complex commands
44 | shell: bash
45 |
46 | # Add other common setup steps if needed
47 |
--------------------------------------------------------------------------------
/.github/labeler.yml:
--------------------------------------------------------------------------------
1 | needs triage:
2 | - "/.*/"
3 |
--------------------------------------------------------------------------------
/.github/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name-template: "v$RESOLVED_VERSION"
2 | tag-template: "v$RESOLVED_VERSION"
3 |
4 | categories:
5 | - title: "🚀 Features"
6 | labels:
7 | - "feature"
8 | - "enhancement"
9 | - title: "📱 UI Changes"
10 | labels:
11 | - "ui"
12 | - title: "🐛 Bug Fixes"
13 | labels:
14 | - "fix"
15 | - "bugfix"
16 | - "bug"
17 | - title: "🧰 Maintenance"
18 | label: "chore"
19 | - title: "💥 Breaking Changes"
20 | labels:
21 | - "breaking"
22 | change-template: "- $TITLE (#$NUMBER)"
23 | change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
24 | version-resolver:
25 | major:
26 | labels:
27 | - "major"
28 | minor:
29 | labels:
30 | - "minor"
31 | patch:
32 | labels:
33 | - "patch"
34 | default: patch
35 | template: |
36 | ## Changes
37 |
38 | $CHANGES
39 |
40 | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION
41 |
42 | exclude-labels:
43 | - "skip changelog"
44 |
45 | exclude-contributors:
46 | - "Dr-Blank"
47 |
48 | autolabeler:
49 | - label: "bug"
50 | branch:
51 | - '/fix\/.+/'
52 | title:
53 | - "/fix/i"
54 | - label: "enhancement"
55 | branch:
56 | - '/feature\/.+/'
57 | title:
58 | - "/^feat(ure)?/i"
59 | body:
60 | - "/JIRA-[0-9]{1,4}/"
61 | - label: "chore"
62 | title:
63 | - "/^chore\b/i"
64 | - label: "ui"
65 | title:
66 | - "/^ui\b/i"
67 | - label: "refactor"
68 | title:
69 | - "/^refactor/i"
70 |
--------------------------------------------------------------------------------
/.github/workflows/issue-labeler.yml:
--------------------------------------------------------------------------------
1 | name: New issue labeler
2 | on:
3 | # Runs on newly opened issues
4 | issues:
5 | types: [opened]
6 |
7 | # Sets permissions of the GITHUB_TOKEN
8 | permissions:
9 | issues: write
10 | contents: read
11 |
12 | jobs:
13 | triage:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: github/issue-labeler@v3.4
17 | with:
18 | configuration-path: .github/labeler.yml
19 | enable-versioned-regex: 0
20 | repo-token: "${{secrets.GITHUB_TOKEN}}"
21 |
--------------------------------------------------------------------------------
/.github/workflows/release-drafter.yml:
--------------------------------------------------------------------------------
1 | name: Release Drafter
2 |
3 | on:
4 | push:
5 | # branches to consider in the event; optional, defaults to all
6 | branches:
7 | - master
8 | # pull_request event is required only for autolabeler
9 | pull_request:
10 | # Only following types are handled by the action, but one can default to all as well
11 | types: [opened, reopened, synchronize]
12 | # pull_request_target event is required for autolabeler to support PRs from forks
13 | # pull_request_target:
14 | # types: [opened, reopened, synchronize]
15 |
16 | permissions:
17 | contents: read
18 |
19 | jobs:
20 | update_release_draft:
21 | permissions:
22 | # write permission is required to create a github release
23 | contents: write
24 | # write permission is required for autolabeler
25 | # otherwise, read permission is required at least
26 | pull-requests: write
27 | runs-on: ubuntu-latest
28 | steps:
29 | # (Optional) GitHub Enterprise requires GHE_HOST variable set
30 | #- name: Set GHE_HOST
31 | # run: |
32 | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV
33 |
34 | # Drafts your next Release notes as Pull Requests are merged into "master"
35 | - uses: release-drafter/release-drafter@v6
36 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
37 | # with:
38 | # config-name: my-config.yml
39 | # disable-autolabeler: true
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | migrate_working_dir/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .pub-cache/
31 | .pub/
32 | /build/
33 | dist/
34 |
35 | # Symbolication related
36 | app.*.symbols
37 |
38 | # Obfuscation related
39 | app.*.map.json
40 |
41 | # Android Studio will place build artifacts here
42 | /android/app/debug
43 | /android/app/profile
44 | /android/app/release
45 | /android/app/.cxx/
46 |
47 | # secret keys
48 | /secrets
49 |
50 | # FVM Version Cache
51 | .fvm/
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "shelfsdk"]
2 | path = shelfsdk
3 | url = https://github.com/Dr-Blank/shelfsdk
4 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d"
8 | channel: "stable"
9 |
10 | project_type: app
11 |
12 | # Tracks metadata for the flutter migrate command
13 | migration:
14 | platforms:
15 | - platform: root
16 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
17 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
18 | - platform: android
19 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
20 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
21 | - platform: ios
22 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
23 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
24 | - platform: linux
25 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
26 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
27 | - platform: macos
28 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
29 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
30 | - platform: web
31 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
32 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
33 | - platform: windows
34 | create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
35 | base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d
36 |
37 | # User provided section
38 |
39 | # List of Local paths (relative to this file) that should be
40 | # ignored by the migrate tool.
41 | #
42 | # Files that are not part of the templates will be ignored by default.
43 | unmanaged_files:
44 | - 'lib/main.dart'
45 | - 'ios/Runner.xcodeproj/project.pbxproj'
46 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": "vaani",
9 | "request": "launch",
10 | "program": "lib/main.dart",
11 | "type": "dart"
12 | },
13 | {
14 | "name": "vaani (profile mode)",
15 | "request": "launch",
16 | "type": "dart",
17 | "flutterMode": "profile"
18 | },
19 | {
20 | "name": "vaani (release mode)",
21 | "request": "launch",
22 | "type": "dart",
23 | "flutterMode": "release"
24 | },
25 | {
26 | "name": "debug debug.dart",
27 | "request": "launch",
28 | "type": "dart",
29 | "program": "${workspaceFolder}/shelfsdk/playground/debug.dart"
30 | }
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "cmake.configureOnOpen": false,
3 | "cSpell.words": [
4 | "audioplayers",
5 | "autolabeler",
6 | "Autovalidate",
7 | "Checkmark",
8 | "Debounceable",
9 | "deeplinking",
10 | "fullscreen",
11 | "Lerp",
12 | "miniplayer",
13 | "mocktail",
14 | "nodename",
15 | "numberpicker",
16 | "riverpod",
17 | "Schyler",
18 | "shelfsdk",
19 | "sysname",
20 | "tapable",
21 | "unfocus",
22 | "utsname",
23 | "Vaani"
24 | ],
25 | "dart.flutterSdkPath": ".fvm/versions/3.32.0",
26 | "files.exclude": {
27 | "**/*.freezed.dart": true,
28 | "**/*.g.dart": true
29 | },
30 | "workbench.colorCustomizations": {
31 | "activityBar.background": "#5A1021",
32 | "titleBar.activeBackground": "#7E162E",
33 | "titleBar.activeForeground": "#FEFBFC"
34 | }
35 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "icon": { "id": "eye-watch", "color": "terminal.ansiYellow" },
6 | "label": "build_runner watch",
7 | "type": "shell",
8 | "command": "dart run build_runner watch --delete-conflicting-outputs",
9 | "group": {
10 | "kind": "build",
11 | "isDefault": true
12 | },
13 | "detail": "Running build_runner watch for code generation",
14 | "presentation": {
15 | "revealProblems": "onProblem",
16 | "reveal": "silent",
17 | "panel": "dedicated"
18 | },
19 | "runOptions": {
20 | "instanceLimit": 1,
21 | "runOn": "folderOpen",
22 | "reevaluateOnRerun": true
23 | },
24 | "problemMatcher": {
25 | "owner": "dart",
26 | "fileLocation": ["relative", "${workspaceFolder}"],
27 | "pattern": {
28 | "regexp": "^(.*):(\\d+):(\\d+):\\s+(.*)$",
29 | "file": 1,
30 | "line": 2,
31 | "column": 3,
32 | "message": 4
33 | }
34 | }
35 | }
36 | ]
37 | }
38 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | gem "fastlane"
4 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | # Vaani
6 |
7 | Client for [Audiobookshelf](https://github.com/advplyr/audiobookshelf) server made with Flutter.
8 |
9 | ## Features
10 |
11 | * Functional Player: Speed Control, Sleep Timer, Shake to Control Player
12 | * Save data with Offline listening and caching
13 | * Material Design
14 | * Extensive Settings to customize the every tiny detail
15 |
16 | ## Download
17 |
18 | ### Android
19 |
20 |
21 | [
](http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/Dr-Blank/Vaani)
22 | [
](https://play.google.com/store/apps/details?id=dr.blank.vaani)
23 | [
](https://apt.izzysoft.de/fdroid/index/apk/dr.blank.vaani)
24 | [
](https://github.com/Dr-Blank/Vaani/releases/latest/download/app-universal-release.apk)
25 |
26 | *Play Store version is paid if you want to support the development.*
27 |
28 | ### Linux
29 |
30 | [
](https://github.com/Dr-Blank/Vaani/releases/latest/download/vaani-linux-amd64.deb)
31 | [
](https://github.com/Dr-Blank/Vaani/releases/latest/download/vaani-linux-amd64.AppImage)
32 |
33 | ## Screencaps
34 |
35 | https://github.com/user-attachments/assets/2ac9ace2-4a3c-40fc-adde-55914e4cf62d
36 |
37 | |
|
|
|
38 | | :-----------------------------------------------------------: | :---------------------------------------------------------------: | :-------------------------------------------------------------: |
39 | | Home | Book View | Player |
40 |
41 | Currently, the app is in development and is not ready for production use.
42 |
43 | Plan is to have support for android, and desktop.
44 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at https://dart.dev/lints.
17 | #
18 | # Instead of disabling a lint rule for the entire project in the
19 | # section below, it can also be suppressed for a single line of code
20 | # or a specific dart file by using the `// ignore: name_of_lint` and
21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
22 | # producing the lint.
23 | rules:
24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
26 | require_trailing_commas: true
27 | analyzer:
28 | exclude:
29 | - '**.freezed.dart'
30 | - '**.g.dart'
31 | - '**.gr.dart'
32 | errors:
33 | invalid_annotation_target: ignore
34 | plugins:
35 | - custom_lint
36 | # Additional information about this file can be found at
37 | # https://dart.dev/guides/language/analysis-options
38 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
9 | # Remember to never publicly share your keystore.
10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
11 | key.properties
12 | **/*.keystore
13 | **/*.jks
14 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/whispering_pages/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package dr.blank.vaani
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity()
6 |
--------------------------------------------------------------------------------
/android/app/src/main/play_store_512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/play_store_512.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_stat_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/drawable-hdpi/ic_stat_logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_stat_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/drawable-mdpi/ic_stat_logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_stat_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/drawable-xhdpi/ic_stat_logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_stat_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/drawable-xxhdpi/ic_stat_logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_stat_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/drawable-xxxhdpi/ic_stat_logo.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-hdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-mdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/raw/keep.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | }
6 | }
7 |
8 | rootProject.buildDir = '../build'
9 | subprojects {
10 | project.buildDir = "${rootProject.buildDir}/${project.name}"
11 | }
12 | // TODO: remove when https://github.com/livekit/client-sdk-flutter/issues/569#issuecomment-2275686786 is fixed
13 | subprojects {
14 | afterEvaluate { project ->
15 | if (project.plugins.hasPlugin("com.android.application") ||
16 | project.plugins.hasPlugin("com.android.library")) {
17 | project.android {
18 | compileSdkVersion 34
19 | buildToolsVersion "34.0.0"
20 | }
21 | }
22 | }
23 | }
24 | subprojects {
25 | project.evaluationDependsOn(':app')
26 | }
27 |
28 | tasks.register("clean", Delete) {
29 | delete rootProject.buildDir
30 | }
31 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4G
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | def flutterSdkPath = {
3 | def properties = new Properties()
4 | file("local.properties").withInputStream { properties.load(it) }
5 | def flutterSdkPath = properties.getProperty("flutter.sdk")
6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
7 | return flutterSdkPath
8 | }
9 | settings.ext.flutterSdkPath = flutterSdkPath()
10 |
11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
12 |
13 | repositories {
14 | google()
15 | mavenCentral()
16 | gradlePluginPortal()
17 | }
18 | }
19 |
20 | plugins {
21 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
22 | id "com.android.application" version '8.10.0' apply false
23 | id "org.jetbrains.kotlin.android" version "2.0.20" apply false
24 | }
25 |
26 | include ":app"
27 |
--------------------------------------------------------------------------------
/assets/fonts/AbsIcons.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/assets/fonts/AbsIcons.ttf
--------------------------------------------------------------------------------
/assets/icon/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/assets/icon/logo.png
--------------------------------------------------------------------------------
/assets/images/vaani_logo_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/assets/images/vaani_logo_foreground.png
--------------------------------------------------------------------------------
/assets/sounds/beep.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/assets/sounds/beep.mp3
--------------------------------------------------------------------------------
/distribute_options.yaml:
--------------------------------------------------------------------------------
1 | output: dist/
2 | releases:
3 | - name: dev
4 | jobs:
5 | - name: release-dev-linux-deb
6 | package:
7 | platform: linux
8 | target: deb
9 |
--------------------------------------------------------------------------------
/docs/images_and_logos.md:
--------------------------------------------------------------------------------
1 | this is how i converted my png to svg
2 |
3 | `convert -background White vaani_logo.png vaani_logo.pbm`
4 |
5 |
6 | `potrace -b svg -i vaani_logo.pbm -o vaani_logo.svg`
7 |
8 | `-i` flag was needed so that it took white as the svgs and black as background
--------------------------------------------------------------------------------
/docs/linux_build_guide.md:
--------------------------------------------------------------------------------
1 | # Linux Build Guide
2 |
3 | ## Determining Package Size
4 | To determine the installed size for your Linux package configuration, you can use the following script:
5 |
6 | ```bash
7 | #!/bin/bash
8 |
9 | # Build the Linux app
10 | flutter build linux
11 |
12 | # Get size in KB and add 17% buffer for runtime dependencies
13 | SIZE_KB=$(du -sk build/linux/x64/release/bundle | cut -f1)
14 | BUFFER_SIZE_KB=$(($SIZE_KB + ($SIZE_KB * 17 / 100)))
15 |
16 | echo "Actual bundle size: $SIZE_KB KB"
17 | echo "Recommended installed_size (with 17% buffer): $BUFFER_SIZE_KB KB"
18 | ```
19 |
20 | Save this as `get_package_size.sh` in your project root and make it executable:
21 | ```bash
22 | chmod +x get_package_size.sh
23 | ```
24 |
25 | ### Usage
26 | 1. Run the script:
27 | ```bash
28 | ./get_package_size.sh
29 | ```
30 | 2. Use the output value for `installed_size` in your `linux/packaging/deb/make_config.yaml` file:
31 | ```yaml
32 | installed_size: 75700 # Replace with the value from the script
33 | ```
34 |
35 | ### Why add a buffer?
36 | The 17% buffer is added to account for:
37 | - Runtime dependencies
38 | - Future updates
39 | - Potential additional assets
40 | - Prevent installation issues on systems with limited space
41 |
42 | ### Notes
43 | - The installed size should be specified in kilobytes (KB)
44 | - Always round up the buffer size to be safe
45 | - Re-run this script after significant changes to your app (new assets, dependencies, etc.)
--------------------------------------------------------------------------------
/docs/linux_deeplink.md:
--------------------------------------------------------------------------------
1 | to test deeplink
2 | `xdg-open vaani://test?code=123&state=abc`
--------------------------------------------------------------------------------
/fastlane/Appfile:
--------------------------------------------------------------------------------
1 | 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
2 | package_name("dr.blank.vaani") # e.g. com.krausefx.app
3 |
--------------------------------------------------------------------------------
/fastlane/Fastfile:
--------------------------------------------------------------------------------
1 | # This file contains the fastlane.tools configuration
2 | # You can find the documentation at https://docs.fastlane.tools
3 | #
4 | # For a list of all available actions, check out
5 | #
6 | # https://docs.fastlane.tools/actions
7 | #
8 | # For a list of all available plugins, check out
9 | #
10 | # https://docs.fastlane.tools/plugins/available-plugins
11 | #
12 |
13 | # Uncomment the line if you want fastlane to automatically update itself
14 | # update_fastlane
15 |
16 | default_platform(:android)
17 |
18 | platform :android do
19 | desc "Runs all the tests"
20 | lane :test do
21 | gradle(task: "test", project_dir: 'android/')
22 | end
23 |
24 | desc "Submit a new Beta Build to Crashlytics Beta"
25 | lane :beta do
26 | gradle(task: "clean assembleRelease", project_dir: 'android/')
27 | crashlytics
28 |
29 | # sh "your_script.sh"
30 | # You can also use other beta testing services here
31 | end
32 |
33 | desc "Deploy a new version to the Google Play"
34 | lane :deploy do
35 | gradle(task: "clean assembleRelease", project_dir: 'android/')
36 | upload_to_play_store
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/fastlane/README.md:
--------------------------------------------------------------------------------
1 | fastlane documentation
2 | ----
3 |
4 | # Installation
5 |
6 | Make sure you have the latest version of the Xcode command line tools installed:
7 |
8 | ```sh
9 | xcode-select --install
10 | ```
11 |
12 | For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
13 |
14 | # Available Actions
15 |
16 | ## Android
17 |
18 | ### android test
19 |
20 | ```sh
21 | [bundle exec] fastlane android test
22 | ```
23 |
24 | Runs all the tests
25 |
26 | ### android beta
27 |
28 | ```sh
29 | [bundle exec] fastlane android beta
30 | ```
31 |
32 | Submit a new Beta Build to Crashlytics Beta
33 |
34 | ### android deploy
35 |
36 | ```sh
37 | [bundle exec] fastlane android deploy
38 | ```
39 |
40 | Deploy a new version to the Google Play
41 |
42 | ----
43 |
44 | This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
45 |
46 | More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
47 |
48 | The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
49 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Vaani is a client for your (self-hosted) Audiobookshelf server.
2 |
3 | Features:
4 |
5 | - Functional Player: Speed Control, Sleep Timer, Shake to Control Player
6 | - Save data with Offline listening and caching
7 | - Material Design
8 | - Extensive Settings to customize the every tiny detail
9 |
10 | 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.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/fastlane/metadata/android/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/1_en-US.jpeg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/2_en-US.jpeg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/3_en-US.jpeg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/4_en-US.jpeg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/5_en-US.jpeg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/6_en-US.jpeg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/7_en-US.jpeg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/fastlane/metadata/android/en-US/images/phoneScreenshots/8_en-US.jpeg
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Beautiful, Fast and Functional Audiobook Player for your Audiobookshelf server.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Vaani
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/video.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/fastlane/metadata/android/en-US/video.txt
--------------------------------------------------------------------------------
/fastlane/report.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/images/banner.png
--------------------------------------------------------------------------------
/images/screenshots/android/bookview.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/images/screenshots/android/bookview.jpg
--------------------------------------------------------------------------------
/images/screenshots/android/home.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/images/screenshots/android/home.jpg
--------------------------------------------------------------------------------
/images/screenshots/android/player.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/images/screenshots/android/player.jpg
--------------------------------------------------------------------------------
/images/vaani_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/lib/api/authenticated_users_provider.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'authenticated_users_provider.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$authenticatedUsersHash() =>
10 | r'5fdd472f62fc3b73ff8417cdce9f02e86c33d00f';
11 |
12 | /// provides with a set of authenticated users
13 | ///
14 | /// Copied from [AuthenticatedUsers].
15 | @ProviderFor(AuthenticatedUsers)
16 | final authenticatedUsersProvider = AutoDisposeNotifierProvider<
17 | AuthenticatedUsers, Set>.internal(
18 | AuthenticatedUsers.new,
19 | name: r'authenticatedUsersProvider',
20 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
21 | ? null
22 | : _$authenticatedUsersHash,
23 | dependencies: null,
24 | allTransitiveDependencies: null,
25 | );
26 |
27 | typedef _$AuthenticatedUsers
28 | = AutoDisposeNotifier>;
29 | // ignore_for_file: type=lint
30 | // 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
31 |
--------------------------------------------------------------------------------
/lib/api/image_provider.dart:
--------------------------------------------------------------------------------
1 | import 'dart:typed_data';
2 |
3 | import 'package:logging/logging.dart';
4 | import 'package:riverpod_annotation/riverpod_annotation.dart';
5 | import 'package:shelfsdk/audiobookshelf_api.dart';
6 | import 'package:vaani/api/api_provider.dart';
7 | import 'package:vaani/api/library_item_provider.dart';
8 | import 'package:vaani/db/cache_manager.dart';
9 |
10 | /// provides cover images for the audiobooks
11 | ///
12 | /// is a stream provider that provides cover images first from the cache then from the server
13 | /// if the image is not found in the cache, it will be fetched from the server and saved to the cache
14 | /// if the image is not found in the server it will throw an error
15 |
16 | part 'image_provider.g.dart';
17 |
18 | final _logger = Logger('cover_image_provider');
19 |
20 | @Riverpod(keepAlive: true)
21 | class CoverImage extends _$CoverImage {
22 | @override
23 | Stream build(String itemId) async* {
24 | final api = ref.watch(authenticatedApiProvider);
25 |
26 | // ! artifical delay for testing
27 | // await Future.delayed(const Duration(seconds: 2));
28 |
29 | // try to get the image from the cache
30 | final file = await imageCacheManager.getFileFromMemory(itemId) ??
31 | await imageCacheManager.getFileFromCache(itemId);
32 |
33 | if (file != null) {
34 | // if the image is in the cache, yield it
35 | _logger.fine(
36 | 'cover image found in cache for $itemId at ${file.file.path}',
37 | );
38 | yield await file.file.readAsBytes();
39 | final libraryItem = await ref.watch(libraryItemProvider(itemId).future);
40 | // return if no need to fetch from the server
41 | if (libraryItem.updatedAt.isBefore(await file.file.lastModified())) {
42 | _logger.fine(
43 | 'cover image is up to date for $itemId, no need to fetch from the server',
44 | );
45 | return;
46 | } else {
47 | _logger.fine(
48 | 'cover image stale for $itemId, fetching from the server',
49 | );
50 | }
51 | } else {
52 | _logger.fine('cover image not found in cache for $itemId');
53 | }
54 |
55 | // check if the image is in the cache
56 | final coverImage = await api.items.getCover(
57 | libraryItemId: itemId,
58 | parameters: const GetImageReqParams(width: 1200),
59 | );
60 | // save the image to the cache
61 | if (coverImage != null) {
62 | final newFile = await imageCacheManager.putFile(
63 | itemId,
64 | coverImage,
65 | key: itemId,
66 | fileExtension: 'jpg',
67 | );
68 | _logger.fine(
69 | 'cover image fetched for for $itemId, file time: ${await newFile.lastModified()}',
70 | );
71 | }
72 |
73 | yield coverImage ?? Uint8List(0);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/api/library_item_provider.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:logging/logging.dart';
4 | import 'package:riverpod_annotation/riverpod_annotation.dart';
5 | import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk;
6 | import 'package:vaani/api/api_provider.dart';
7 | import 'package:vaani/db/cache/cache_key.dart';
8 | import 'package:vaani/db/cache_manager.dart';
9 | import 'package:vaani/shared/extensions/model_conversions.dart';
10 |
11 | part 'library_item_provider.g.dart';
12 |
13 | final _logger = Logger('LibraryItemProvider');
14 |
15 | /// provides the library item for the given id
16 | @Riverpod(keepAlive: true)
17 | class LibraryItem extends _$LibraryItem {
18 | @override
19 | Stream build(String id) async* {
20 | final api = ref.watch(authenticatedApiProvider);
21 |
22 | _logger.fine('LibraryItemProvider fetching library item: $id');
23 |
24 | // ! this is a mock delay
25 | // await Future.delayed(const Duration(seconds: 150));
26 |
27 | // look for the item in the cache
28 | final key = CacheKey.libraryItem(id);
29 | final cachedFile = await apiResponseCacheManager.getFileFromMemory(key) ??
30 | await apiResponseCacheManager.getFileFromCache(key);
31 | if (cachedFile != null) {
32 | _logger.fine(
33 | 'LibraryItemProvider reading from cache for $id from ${cachedFile.file}',
34 | );
35 | // read file as json
36 | final cachedItem = shelfsdk.LibraryItemExpanded.fromJson(
37 | jsonDecode(await cachedFile.file.readAsString()),
38 | );
39 | yield cachedItem;
40 | } else {
41 | _logger.fine('LibraryItemProvider cache miss for $id');
42 | }
43 |
44 | // ! this is a mock delay
45 | // await Future.delayed(const Duration(seconds: 3));
46 |
47 | final item = await api.items.get(
48 | libraryItemId: id,
49 | parameters: const shelfsdk.GetItemReqParams(
50 | expanded: true,
51 | include: [
52 | shelfsdk.GetItemIncludeOption.progress,
53 | shelfsdk.GetItemIncludeOption.rssFeed,
54 | shelfsdk.GetItemIncludeOption.authors,
55 | shelfsdk.GetItemIncludeOption.downloads,
56 | ],
57 | ),
58 | );
59 | if (item != null) {
60 | // save to cache
61 | final newFile = await apiResponseCacheManager.putFile(
62 | key,
63 | utf8.encode(jsonEncode(item.asExpanded.toJson())),
64 | fileExtension: 'json',
65 | key: key,
66 | );
67 | _logger.fine('writing to cache: $newFile');
68 | yield item.asExpanded;
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/lib/api/library_provider.dart:
--------------------------------------------------------------------------------
1 | import 'package:hooks_riverpod/hooks_riverpod.dart' show Ref;
2 | import 'package:logging/logging.dart' show Logger;
3 | import 'package:riverpod_annotation/riverpod_annotation.dart';
4 |
5 | import 'package:shelfsdk/audiobookshelf_api.dart' show Library;
6 | import 'package:vaani/api/api_provider.dart' show authenticatedApiProvider;
7 | import 'package:vaani/settings/api_settings_provider.dart'
8 | show apiSettingsProvider;
9 | part 'library_provider.g.dart';
10 |
11 | final _logger = Logger('LibraryProvider');
12 |
13 | @riverpod
14 | Future library(Ref ref, String id) async {
15 | final api = ref.watch(authenticatedApiProvider);
16 | final library = await api.libraries.get(libraryId: id);
17 | if (library == null) {
18 | _logger.warning('No library found through id: $id');
19 | // try to get the library from the list of libraries
20 | final libraries = await ref.watch(librariesProvider.future);
21 | for (final lib in libraries) {
22 | if (lib.id == id) {
23 | return lib;
24 | }
25 | }
26 | _logger.warning('No library found in the list of libraries');
27 | return null;
28 | }
29 | _logger.fine('Fetched library: $library');
30 | return library.library;
31 | }
32 |
33 | @riverpod
34 | Future currentLibrary(Ref ref) async {
35 | final libraryId =
36 | ref.watch(apiSettingsProvider.select((s) => s.activeLibraryId));
37 | if (libraryId == null) {
38 | _logger.warning('No active library id found');
39 | return null;
40 | }
41 | return await ref.watch(libraryProvider(libraryId).future);
42 | }
43 |
44 | @riverpod
45 | class Libraries extends _$Libraries {
46 | @override
47 | FutureOr> build() async {
48 | final api = ref.watch(authenticatedApiProvider);
49 | final libraries = await api.libraries.getAll();
50 | if (libraries == null) {
51 | _logger.warning('Failed to fetch libraries');
52 | return [];
53 | }
54 | _logger.fine('Fetched ${libraries.length} libraries');
55 | ref.keepAlive();
56 | return libraries;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/api/server_provider.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'server_provider.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$audiobookShelfServerHash() =>
10 | r'31a96b431221965cd586aad670a32ca901539e41';
11 |
12 | /// provides with a set of servers added by the user
13 | ///
14 | /// Copied from [AudiobookShelfServer].
15 | @ProviderFor(AudiobookShelfServer)
16 | final audiobookShelfServerProvider = AutoDisposeNotifierProvider<
17 | AudiobookShelfServer, Set>.internal(
18 | AudiobookShelfServer.new,
19 | name: r'audiobookShelfServerProvider',
20 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
21 | ? null
22 | : _$audiobookShelfServerHash,
23 | dependencies: null,
24 | allTransitiveDependencies: null,
25 | );
26 |
27 | typedef _$AudiobookShelfServer
28 | = AutoDisposeNotifier>;
29 | // ignore_for_file: type=lint
30 | // 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
31 |
--------------------------------------------------------------------------------
/lib/constants/hero_tag_conventions.dart:
--------------------------------------------------------------------------------
1 | class HeroTagPrefixes {
2 | static const String heroTagPrefix = 'hero_tag_';
3 |
4 | /// The hero tag for the book cover
5 | static const String bookCover = 'book_cover_';
6 | static const String bookCoverSkeleton = 'book_cover_skeleton_';
7 | static const String authorAvatar = 'author_avatar_';
8 | static const String authorAvatarSkeleton = 'author_avatar_skeleton_';
9 | static const String authorName = 'author_name_';
10 | static const String bookTitle = 'book_title_';
11 | static const String narratorName = 'narrator_name_';
12 | static const String libraryItemPlayButton = 'library_item_play_button_';
13 | }
14 |
--------------------------------------------------------------------------------
/lib/constants/sizes.dart:
--------------------------------------------------------------------------------
1 | class AppElementSizes {
2 | // paddings
3 | static const double paddingRegular = 8.0;
4 | static const double paddingSmall = paddingRegular / 2;
5 | static const double paddingLarge = paddingRegular * 2;
6 |
7 | // border radius
8 | static const double borderRadiusRegular = 12.0;
9 | static const double borderRadiusSmall = borderRadiusRegular / 2;
10 |
11 | // icon sizes
12 | static const double iconSizeRegular = 48.0;
13 | static const double iconSizeSmall = 36.0;
14 | static const double iconSizeLarge = 64.0;
15 | }
16 |
--------------------------------------------------------------------------------
/lib/db/available_boxes.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart' show immutable;
2 | import 'package:hive/hive.dart';
3 | import 'package:vaani/features/per_book_settings/models/book_settings.dart';
4 | import 'package:vaani/settings/models/models.dart';
5 |
6 | @immutable
7 | class AvailableHiveBoxes {
8 | const AvailableHiveBoxes._();
9 |
10 | /// Box for storing user preferences as [AppSettings]
11 | static final userPrefsBox = Hive.box(name: 'userPrefs');
12 |
13 | /// Box for storing [ApiSettings]
14 | static final apiSettingsBox = Hive.box(name: 'apiSettings');
15 |
16 | /// stores the a list of [AudiobookShelfServer]
17 | static final serverBox =
18 | Hive.box(name: 'audiobookShelfServer');
19 |
20 | /// stores the a list of [AuthenticatedUser]
21 | static final authenticatedUserBox =
22 | Hive.box(name: 'authenticatedUser');
23 |
24 | /// stores the a list of [BookSettings]
25 | static final individualBookSettingsBox =
26 | Hive.box(name: 'bookSettings');
27 | }
28 |
--------------------------------------------------------------------------------
/lib/db/cache/cache_key.dart:
--------------------------------------------------------------------------------
1 | class CacheKey {
2 | static String libraryItem(String id) {
3 | return 'library_item_$id';
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/lib/db/cache/schemas/image.dart:
--------------------------------------------------------------------------------
1 | import 'package:isar/isar.dart';
2 |
3 | part 'image.g.dart';
4 |
5 | /// Represents a cover image for a library item
6 | ///
7 | /// stores 2 paths, one is thumbnail and the other is the full size image
8 | /// both are optional
9 | /// also stores last fetched date for the image
10 | /// Id is passed as a parameter to the collection annotation (the lib_item_id)
11 | /// also index the id
12 | /// This is because the image is a part of the library item and the library item
13 | /// is the parent of the image
14 | @Collection(ignore: {'path'})
15 | @Name('CacheImage')
16 | class Image {
17 | @Id()
18 | int id;
19 |
20 | String? thumbnailPath;
21 | String? imagePath;
22 | DateTime lastSaved;
23 |
24 | Image({
25 | required this.id,
26 | this.thumbnailPath,
27 | this.imagePath,
28 | }) : lastSaved = DateTime.now();
29 |
30 | /// returns the path to the image
31 | String? get path => thumbnailPath ?? imagePath;
32 |
33 | /// automatically updates the last fetched date when saving a new path
34 | void updatePath(String? thumbnailPath, String? imagePath) async {
35 | this.thumbnailPath = thumbnailPath;
36 | this.imagePath = imagePath;
37 | lastSaved = DateTime.now();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/db/cache_manager.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_cache_manager/flutter_cache_manager.dart';
2 | import 'package:vaani/settings/constants.dart';
3 |
4 | final imageCacheManager = CacheManager(
5 | Config(
6 | '${AppMetadata.appNameLowerCase}_image_cache',
7 | stalePeriod: const Duration(days: 365 * 10),
8 | repo: JsonCacheInfoRepository(),
9 | maxNrOfCacheObjects: 1000,
10 | ),
11 | );
12 |
13 | final apiResponseCacheManager = CacheManager(
14 | Config(
15 | '${AppMetadata.appNameLowerCase}_api_response_cache',
16 | stalePeriod: const Duration(days: 7),
17 | repo: JsonCacheInfoRepository(),
18 | maxNrOfCacheObjects: 1000,
19 | ),
20 | );
21 |
--------------------------------------------------------------------------------
/lib/db/init.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:hive/hive.dart';
4 | import 'package:path/path.dart' as p;
5 | import 'package:path_provider/path_provider.dart';
6 | import 'package:vaani/main.dart';
7 | import 'package:vaani/settings/constants.dart';
8 |
9 | import 'register_models.dart';
10 |
11 | // does the initial setup of the storage
12 | Future initStorage() async {
13 | final dir = await getApplicationDocumentsDirectory();
14 |
15 | // use vaani as the directory for hive
16 | final storageDir = Directory(
17 | p.join(
18 | dir.path,
19 | AppMetadata.appNameLowerCase,
20 | ),
21 | );
22 | await storageDir.create(recursive: true);
23 |
24 | Hive.defaultDirectory = storageDir.path;
25 | appLogger.config('Hive storage directory init: ${Hive.defaultDirectory}');
26 |
27 | await registerModels();
28 | }
29 |
--------------------------------------------------------------------------------
/lib/db/player_prefs/book_prefs.dart:
--------------------------------------------------------------------------------
1 | // a table to track preferences of player for each book
2 | import 'package:isar/isar.dart';
3 |
4 | part 'book_prefs.g.dart';
5 |
6 | /// stores the preferences of the player for a book
7 | @Collection()
8 | @Name('BookPrefs')
9 | class BookPrefs {
10 | @Id()
11 | int libItemId;
12 |
13 | double? speed;
14 | // double? volume;
15 | // Duration? sleepTimer;
16 | // bool? showTotalProgress;
17 | // bool? showChapterProgress;
18 | // bool? useChapterInfo;
19 |
20 | BookPrefs({
21 | required this.libItemId,
22 | this.speed,
23 | // this.volume,
24 | // this.sleepTimer,
25 | // this.showTotalProgress,
26 | // this.showChapterProgress,
27 | // this.useChapterInfo,
28 | });
29 | }
30 |
--------------------------------------------------------------------------------
/lib/db/register_models.dart:
--------------------------------------------------------------------------------
1 | import 'package:hive/hive.dart';
2 | import 'package:vaani/features/per_book_settings/models/book_settings.dart';
3 | import 'package:vaani/settings/models/models.dart';
4 |
5 | // register all models to Hive for serialization
6 | Future registerModels() async {
7 | Hive.registerAdapter(
8 | 'AppSettings',
9 | ((json) => AppSettings.fromJson(json)),
10 | );
11 | Hive.registerAdapter(
12 | 'ApiSettings',
13 | ((json) => ApiSettings.fromJson(json)),
14 | );
15 | Hive.registerAdapter(
16 | 'AudiobookShelfServer',
17 | ((json) => AudiobookShelfServer.fromJson(json)),
18 | );
19 | Hive.registerAdapter(
20 | 'AuthenticatedUser',
21 | ((json) => AuthenticatedUser.fromJson(json)),
22 | );
23 | Hive.registerAdapter(
24 | 'BookSettings',
25 | ((json) => BookSettings.fromJson(json)),
26 | );
27 | Hive.registerAdapter(
28 | '_\$BookSettingsImpl', // hack because of freezed
29 | ((json) => BookSettings.fromJson(json)),
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/lib/db/storage.dart:
--------------------------------------------------------------------------------
1 | export 'available_boxes.dart';
2 | export 'init.dart';
3 | export 'register_models.dart';
4 |
--------------------------------------------------------------------------------
/lib/features/downloads/view/downloads_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:vaani/features/downloads/providers/download_manager.dart';
4 |
5 | class DownloadsPage extends HookConsumerWidget {
6 | const DownloadsPage({super.key});
7 |
8 | @override
9 | Widget build(BuildContext context, WidgetRef ref) {
10 | final manager = ref.read(simpleDownloadManagerProvider);
11 | final downloadHistory = ref.watch(downloadHistoryProvider());
12 |
13 | return Scaffold(
14 | appBar: AppBar(
15 | title: const Text('Downloads'),
16 | ),
17 | body: Center(
18 | // history of downloads
19 | child: downloadHistory.when(
20 | data: (records) {
21 | // each group is one list tile, which contains the files downloaded
22 | final uniqueGroups = records.map((e) => e.group).toSet();
23 | return ListView.builder(
24 | itemCount: uniqueGroups.length,
25 | itemBuilder: (context, index) {
26 | final group = uniqueGroups.elementAt(index);
27 | final groupRecords = records.where((e) => e.group == group);
28 | return ExpansionTile(
29 | title: Text(group ?? 'No Group'),
30 | children: groupRecords
31 | .map(
32 | (e) => ListTile(
33 | title: Text('${e.task.directory}/${e.task.filename}'),
34 | subtitle: Text(e.task.creationTime.toString()),
35 | ),
36 | )
37 | .toList(),
38 | );
39 | },
40 | );
41 | },
42 | loading: () => const CircularProgressIndicator(),
43 | error: (error, stackTrace) {
44 | return Text('Error: $error');
45 | },
46 | ),
47 | ),
48 | );
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/features/explore/providers/search_controller.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:riverpod_annotation/riverpod_annotation.dart';
3 |
4 | part 'search_controller.g.dart';
5 |
6 | /// The controller for the search bar.
7 | @Riverpod(keepAlive: true)
8 | class GlobalSearchController extends _$GlobalSearchController {
9 | @override
10 | Raw build() {
11 | final controller = SearchController();
12 | // dispose the controller when the provider is disposed
13 | ref.onDispose(controller.dispose);
14 | return controller;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/features/explore/providers/search_controller.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'search_controller.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$globalSearchControllerHash() =>
10 | r'd854ace6f2e00a10fc33aba63051375f82ad1b10';
11 |
12 | /// The controller for the search bar.
13 | ///
14 | /// Copied from [GlobalSearchController].
15 | @ProviderFor(GlobalSearchController)
16 | final globalSearchControllerProvider =
17 | NotifierProvider>.internal(
18 | GlobalSearchController.new,
19 | name: r'globalSearchControllerProvider',
20 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
21 | ? null
22 | : _$globalSearchControllerHash,
23 | dependencies: null,
24 | allTransitiveDependencies: null,
25 | );
26 |
27 | typedef _$GlobalSearchController = Notifier>;
28 | // ignore_for_file: type=lint
29 | // 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 |
--------------------------------------------------------------------------------
/lib/features/explore/providers/search_result_provider.dart:
--------------------------------------------------------------------------------
1 | import 'package:hooks_riverpod/hooks_riverpod.dart';
2 | import 'package:riverpod_annotation/riverpod_annotation.dart';
3 | import 'package:shelfsdk/audiobookshelf_api.dart';
4 | import 'package:vaani/api/api_provider.dart';
5 | import 'package:vaani/settings/api_settings_provider.dart';
6 |
7 | part 'search_result_provider.g.dart';
8 |
9 | /// The provider for the search result.
10 | @riverpod
11 | FutureOr searchResult(
12 | Ref ref,
13 | String query, {
14 | int limit = 25,
15 | }) async {
16 | final api = ref.watch(authenticatedApiProvider);
17 | final apiSettings = ref.watch(apiSettingsProvider);
18 |
19 | return await api.libraries.search(
20 | libraryId: apiSettings.activeLibraryId!,
21 | query: query,
22 | limit: limit,
23 | );
24 | }
25 |
--------------------------------------------------------------------------------
/lib/features/explore/view/search_result_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:shelfsdk/audiobookshelf_api.dart';
4 | import 'package:vaani/features/explore/providers/search_result_provider.dart';
5 | import 'package:vaani/features/explore/view/explore_page.dart';
6 | import 'package:vaani/shared/extensions/model_conversions.dart';
7 |
8 | enum SearchResultCategory {
9 | books,
10 | authors,
11 | series,
12 | tags,
13 | narrators,
14 | }
15 |
16 | class SearchResultPage extends HookConsumerWidget {
17 | const SearchResultPage({
18 | super.key,
19 | required this.query,
20 | this.category,
21 | Object? extra,
22 | });
23 |
24 | /// The search query.
25 | final String query;
26 |
27 | /// The category of the search result, if not provided, the search result will be displayed in all categories.
28 | final SearchResultCategory? category;
29 |
30 | @override
31 | Widget build(BuildContext context, WidgetRef ref) {
32 | final results = ref.watch(searchResultProvider(query));
33 | return Scaffold(
34 | appBar: AppBar(
35 | title: Text(
36 | category != null
37 | ? '${category.toString().split('.').last} in "$query"'
38 | : 'Search result for $query',
39 | ),
40 | ),
41 | body: results.when(
42 | data: (options) {
43 | if (options == null) {
44 | return Container(
45 | child: const Text('No data found'),
46 | );
47 | }
48 | if (options is BookLibrarySearchResponse) {
49 | if (category == null) {
50 | return Container();
51 | }
52 | return switch (category!) {
53 | SearchResultCategory.books => ListView.builder(
54 | itemCount: options.book.length,
55 | itemBuilder: (context, index) {
56 | final book =
57 | options.book[index].libraryItem.media.asBookExpanded;
58 | final metadata = book.metadata.asBookMetadataExpanded;
59 |
60 | return BookSearchResultMini(
61 | book: book,
62 | metadata: metadata,
63 | );
64 | },
65 | ),
66 | SearchResultCategory.authors => Container(),
67 | SearchResultCategory.series => Container(),
68 | SearchResultCategory.tags => Container(),
69 | SearchResultCategory.narrators => Container(),
70 | };
71 | }
72 | return null;
73 | },
74 | loading: () => const Center(
75 | child: CircularProgressIndicator(),
76 | ),
77 | error: (error, stackTrace) => Center(
78 | child: Text('Error: $error'),
79 | ),
80 | ),
81 | );
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/lib/features/item_viewer/view/library_item_sliver_app_bar.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_hooks/flutter_hooks.dart';
3 | import 'package:hooks_riverpod/hooks_riverpod.dart';
4 | import 'package:vaani/api/library_item_provider.dart' show libraryItemProvider;
5 |
6 | class LibraryItemSliverAppBar extends HookConsumerWidget {
7 | const LibraryItemSliverAppBar({
8 | super.key,
9 | required this.id,
10 | required this.scrollController,
11 | });
12 |
13 | final String id;
14 | final ScrollController scrollController;
15 |
16 | static const double _showTitleThreshold = kToolbarHeight * 0.5;
17 |
18 | @override
19 | Widget build(BuildContext context, WidgetRef ref) {
20 | final item = ref.watch(libraryItemProvider(id)).valueOrNull;
21 |
22 | final showTitle = useState(false);
23 |
24 | useEffect(
25 | () {
26 | void listener() {
27 | final shouldShow = scrollController.hasClients &&
28 | scrollController.offset > _showTitleThreshold;
29 | if (showTitle.value != shouldShow) {
30 | showTitle.value = shouldShow;
31 | }
32 | }
33 |
34 | scrollController.addListener(listener);
35 | // Trigger listener once initially in case the view starts scrolled
36 | // (though unlikely for this specific use case, it's good practice)
37 | WidgetsBinding.instance.addPostFrameCallback((_) {
38 | if (scrollController.hasClients) {
39 | listener();
40 | }
41 | });
42 | return () => scrollController.removeListener(listener);
43 | },
44 | [scrollController],
45 | );
46 |
47 | return SliverAppBar(
48 | elevation: 0,
49 | floating: false,
50 | pinned: true,
51 | primary: true,
52 | actions: [
53 | // IconButton(
54 | // icon: const Icon(Icons.cast),
55 | // onPressed: () {
56 | // // Handle search action
57 | // },
58 | // ),
59 | ],
60 | title: AnimatedSwitcher(
61 | duration: const Duration(milliseconds: 150),
62 | child: showTitle.value
63 | ? Text(
64 | // Use a Key to help AnimatedSwitcher differentiate widgets
65 | key: const ValueKey('title-text'),
66 | item?.media.metadata.title ?? '',
67 | overflow: TextOverflow.ellipsis,
68 | style: Theme.of(context).textTheme.bodyMedium,
69 | )
70 | : const SizedBox(
71 | // Also give it a key for differentiation
72 | key: ValueKey('empty-title'),
73 | width: 0, // Ensure it takes no space if possible
74 | height: 0,
75 | ),
76 | ),
77 | centerTitle: false,
78 | );
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/lib/features/logging/core/logger.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/foundation.dart';
4 | import 'package:logging/logging.dart';
5 | import 'package:logging_appenders/logging_appenders.dart';
6 | import 'package:path_provider/path_provider.dart';
7 | import 'package:vaani/shared/extensions/duration_format.dart';
8 |
9 | Future getLoggingFilePath() async {
10 | final Directory directory = await getApplicationDocumentsDirectory();
11 | return '${directory.path}/vaani.log';
12 | }
13 |
14 | Future initLogging() async {
15 | final formatter = const DefaultLogRecordFormatter();
16 | if (kReleaseMode) {
17 | Logger.root.level = Level.INFO; // is also the default
18 | // Write to a file
19 | RotatingFileAppender(
20 | baseFilePath: await getLoggingFilePath(),
21 | formatter: formatter,
22 | ).attachToLogger(Logger.root);
23 | } else {
24 | Logger.root.level = Level.FINE; // Capture all logs
25 | RotatingFileAppender(
26 | baseFilePath: await getLoggingFilePath(),
27 | formatter: formatter,
28 | ).attachToLogger(Logger.root);
29 | Logger.root.onRecord.listen((record) {
30 | // Print log records to the console
31 | debugPrint(
32 | '${record.loggerName}: ${record.level.name}: ${record.time.time}: ${record.message}',
33 | );
34 | });
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/features/logging/providers/logs_provider.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:archive/archive_io.dart';
4 | import 'package:flutter/foundation.dart';
5 | import 'package:logging/logging.dart';
6 | import 'package:path_provider/path_provider.dart';
7 | import 'package:riverpod_annotation/riverpod_annotation.dart';
8 | import 'package:vaani/features/logging/core/logger.dart';
9 | part 'logs_provider.g.dart';
10 |
11 | @riverpod
12 | class Logs extends _$Logs {
13 | @override
14 | Future> build() async {
15 | final path = await getLoggingFilePath();
16 | final file = File(path);
17 | if (!file.existsSync()) {
18 | return [];
19 | }
20 | final lines = await file.readAsLines();
21 | return lines.map(parseLogLine).toList();
22 | }
23 |
24 | Future clear() async {
25 | final path = await getLoggingFilePath();
26 | final file = File(path);
27 | await file.writeAsString('');
28 | state = AsyncData([]);
29 | }
30 |
31 | Future getZipFilePath() async {
32 | final String targetZipPath = await generateZipFilePath();
33 | var encoder = ZipFileEncoder();
34 | encoder.create(targetZipPath);
35 | final logFilePath = await getLoggingFilePath();
36 | final logFile = File(logFilePath);
37 | if (await logFile.exists()) {
38 | // Check if log file exists before adding
39 | await encoder.addFile(logFile);
40 | } else {
41 | // Handle case where log file doesn't exist? Maybe log a warning?
42 | // Or create an empty file inside the zip? For now, just don't add.
43 | debugPrint(
44 | 'Warning: Log file not found at $logFilePath, creating potentially empty zip.',
45 | );
46 | }
47 | await encoder.close();
48 | return targetZipPath;
49 | }
50 | }
51 |
52 | Future generateZipFilePath() async {
53 | Directory appDocDirectory = await getTemporaryDirectory();
54 | return '${appDocDirectory.path}/${generateZipFileName()}';
55 | }
56 |
57 | String generateZipFileName() {
58 | return 'vaani-${DateTime.now().microsecondsSinceEpoch}.zip';
59 | }
60 |
61 | Level parseLevel(String level) {
62 | return Level.LEVELS
63 | .firstWhere((l) => l.name == level, orElse: () => Level.ALL);
64 | }
65 |
66 | LogRecord parseLogLine(String line) {
67 | // 2024-10-03 00:48:58.012400 INFO GoRouter - getting location for name: "logs"
68 |
69 | final RegExp logLineRegExp = RegExp(
70 | r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{6}) (\w+) (\w+) - (.+)',
71 | );
72 |
73 | final match = logLineRegExp.firstMatch(line);
74 | if (match == null) {
75 | // return as is
76 | return LogRecord(Level.ALL, line, 'Unknown');
77 | }
78 |
79 | final timeString = match.group(1)!;
80 | final levelString = match.group(2)!;
81 | final loggerName = match.group(3)!;
82 | final message = match.group(4)!;
83 |
84 | final time = DateTime.parse(timeString);
85 | final level = parseLevel(levelString);
86 |
87 | return LogRecord(level, message, loggerName, time);
88 | }
89 |
--------------------------------------------------------------------------------
/lib/features/logging/providers/logs_provider.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'logs_provider.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$logsHash() => r'aa9d3d56586cba6ddf69615320ea605d071ea5e2';
10 |
11 | /// See also [Logs].
12 | @ProviderFor(Logs)
13 | final logsProvider =
14 | AutoDisposeAsyncNotifierProvider>.internal(
15 | Logs.new,
16 | name: r'logsProvider',
17 | debugGetCreateSourceHash:
18 | const bool.fromEnvironment('dart.vm.product') ? null : _$logsHash,
19 | dependencies: null,
20 | allTransitiveDependencies: null,
21 | );
22 |
23 | typedef _$Logs = AutoDisposeAsyncNotifier>;
24 | // ignore_for_file: type=lint
25 | // 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
26 |
--------------------------------------------------------------------------------
/lib/features/onboarding/models/flow.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:freezed_annotation/freezed_annotation.dart';
4 |
5 | part 'flow.freezed.dart';
6 |
7 | @freezed
8 | class Flow with _$Flow {
9 | const factory Flow({
10 | required Uri serverUri,
11 | required String state,
12 | required String verifier,
13 | required Cookie cookie,
14 | @Default(false) bool isFlowComplete,
15 | String? authToken,
16 | }) = _Flow;
17 | }
18 |
--------------------------------------------------------------------------------
/lib/features/onboarding/providers/oauth_provider.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:hooks_riverpod/hooks_riverpod.dart';
4 | import 'package:riverpod_annotation/riverpod_annotation.dart';
5 | import 'package:vaani/api/api_provider.dart';
6 | import 'package:vaani/models/error_response.dart';
7 |
8 | import '../models/flow.dart';
9 |
10 | part 'oauth_provider.g.dart';
11 |
12 | /// the string state of a flow started by user
13 | typedef State = String;
14 |
15 | /// the verifier string of a flow started by user
16 | typedef Verifier = String;
17 |
18 | /// the code returned by the oauth provider
19 | typedef Code = String;
20 |
21 | @Riverpod(keepAlive: true)
22 | class OauthFlows extends _$OauthFlows {
23 | @override
24 | Map build() {
25 | return {};
26 | }
27 |
28 | void addFlow(
29 | State oauthState, {
30 | required Verifier verifier,
31 | required Uri serverUri,
32 | required Cookie cookie,
33 | bool replaceExisting = false,
34 | }) {
35 | if (state.containsKey(oauthState) && !replaceExisting) {
36 | return;
37 | }
38 | state = {
39 | ...state,
40 | oauthState: Flow(
41 | state: oauthState,
42 | verifier: verifier,
43 | serverUri: serverUri,
44 | cookie: cookie,
45 | isFlowComplete: false,
46 | ),
47 | };
48 | }
49 |
50 | void markComplete(State oauthState, String? authToken) {
51 | if (!state.containsKey(oauthState)) {
52 | return;
53 | }
54 | state = {
55 | ...state,
56 | oauthState: state[oauthState]!
57 | .copyWith(isFlowComplete: true, authToken: authToken),
58 | };
59 | }
60 | }
61 |
62 | /// the code returned by the server in exchange for the verifier
63 | @riverpod
64 | Future loginInExchangeForCode(
65 | Ref ref, {
66 | required State oauthState,
67 | required Code code,
68 | ErrorResponseHandler? responseHandler,
69 | }) async {
70 | final flows = ref.watch(oauthFlowsProvider);
71 | final flow = flows[oauthState];
72 | if (flow == null) {
73 | throw StateError('No flow active for state: $oauthState');
74 | }
75 |
76 | if (flow.authToken != null) {
77 | return flow.authToken;
78 | }
79 |
80 | final api = ref.read(audiobookshelfApiProvider(flow.serverUri));
81 | final response = await api.server.oauth2Callback(
82 | code: code,
83 | codeVerifier: flow.verifier,
84 | state: oauthState,
85 | cookie: flow.cookie,
86 | responseErrorHandler: responseHandler?.storeError,
87 | );
88 |
89 | if (response == null) {
90 | return null;
91 | }
92 |
93 | ref.read(oauthFlowsProvider.notifier).markComplete(oauthState, api.token);
94 | return api.token;
95 | }
96 |
--------------------------------------------------------------------------------
/lib/features/per_book_settings/models/book_settings.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 | import 'package:vaani/features/per_book_settings/models/nullable_player_settings.dart';
3 |
4 | part 'book_settings.freezed.dart';
5 | part 'book_settings.g.dart';
6 |
7 | /// per book settings
8 | @freezed
9 | class BookSettings with _$BookSettings {
10 | const factory BookSettings({
11 | required String bookId,
12 | @Default(NullablePlayerSettings()) NullablePlayerSettings playerSettings,
13 | }) = _BookSettings;
14 |
15 | factory BookSettings.fromJson(Map json) =>
16 | _$BookSettingsFromJson(json);
17 | }
18 |
--------------------------------------------------------------------------------
/lib/features/per_book_settings/models/book_settings.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'book_settings.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$BookSettingsImpl _$$BookSettingsImplFromJson(Map json) =>
10 | _$BookSettingsImpl(
11 | bookId: json['bookId'] as String,
12 | playerSettings: json['playerSettings'] == null
13 | ? const NullablePlayerSettings()
14 | : NullablePlayerSettings.fromJson(
15 | json['playerSettings'] as Map),
16 | );
17 |
18 | Map _$$BookSettingsImplToJson(_$BookSettingsImpl instance) =>
19 | {
20 | 'bookId': instance.bookId,
21 | 'playerSettings': instance.playerSettings,
22 | };
23 |
--------------------------------------------------------------------------------
/lib/features/per_book_settings/models/nullable_player_settings.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 | import 'package:vaani/settings/models/app_settings.dart';
3 |
4 | part 'nullable_player_settings.freezed.dart';
5 | part 'nullable_player_settings.g.dart';
6 |
7 | @freezed
8 | class NullablePlayerSettings with _$NullablePlayerSettings {
9 | const factory NullablePlayerSettings({
10 | MinimizedPlayerSettings? miniPlayerSettings,
11 | ExpandedPlayerSettings? expandedPlayerSettings,
12 | double? preferredDefaultVolume,
13 | double? preferredDefaultSpeed,
14 | List? speedOptions,
15 | SleepTimerSettings? sleepTimerSettings,
16 | Duration? playbackReportInterval,
17 | }) = _NullablePlayerSettings;
18 |
19 | factory NullablePlayerSettings.fromJson(Map json) =>
20 | _$NullablePlayerSettingsFromJson(json);
21 | }
22 |
--------------------------------------------------------------------------------
/lib/features/per_book_settings/models/nullable_player_settings.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'nullable_player_settings.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$NullablePlayerSettingsImpl _$$NullablePlayerSettingsImplFromJson(
10 | Map json) =>
11 | _$NullablePlayerSettingsImpl(
12 | miniPlayerSettings: json['miniPlayerSettings'] == null
13 | ? null
14 | : MinimizedPlayerSettings.fromJson(
15 | json['miniPlayerSettings'] as Map),
16 | expandedPlayerSettings: json['expandedPlayerSettings'] == null
17 | ? null
18 | : ExpandedPlayerSettings.fromJson(
19 | json['expandedPlayerSettings'] as Map),
20 | preferredDefaultVolume:
21 | (json['preferredDefaultVolume'] as num?)?.toDouble(),
22 | preferredDefaultSpeed:
23 | (json['preferredDefaultSpeed'] as num?)?.toDouble(),
24 | speedOptions: (json['speedOptions'] as List?)
25 | ?.map((e) => (e as num).toDouble())
26 | .toList(),
27 | sleepTimerSettings: json['sleepTimerSettings'] == null
28 | ? null
29 | : SleepTimerSettings.fromJson(
30 | json['sleepTimerSettings'] as Map),
31 | playbackReportInterval: json['playbackReportInterval'] == null
32 | ? null
33 | : Duration(
34 | microseconds: (json['playbackReportInterval'] as num).toInt()),
35 | );
36 |
37 | Map _$$NullablePlayerSettingsImplToJson(
38 | _$NullablePlayerSettingsImpl instance) =>
39 | {
40 | 'miniPlayerSettings': instance.miniPlayerSettings,
41 | 'expandedPlayerSettings': instance.expandedPlayerSettings,
42 | 'preferredDefaultVolume': instance.preferredDefaultVolume,
43 | 'preferredDefaultSpeed': instance.preferredDefaultSpeed,
44 | 'speedOptions': instance.speedOptions,
45 | 'sleepTimerSettings': instance.sleepTimerSettings,
46 | 'playbackReportInterval': instance.playbackReportInterval?.inMicroseconds,
47 | };
48 |
--------------------------------------------------------------------------------
/lib/features/per_book_settings/providers/book_settings_provider.dart:
--------------------------------------------------------------------------------
1 | import 'package:logging/logging.dart';
2 | import 'package:riverpod_annotation/riverpod_annotation.dart';
3 | import 'package:vaani/db/available_boxes.dart';
4 | import 'package:vaani/features/per_book_settings/models/book_settings.dart'
5 | as model;
6 | import 'package:vaani/features/per_book_settings/models/nullable_player_settings.dart';
7 |
8 | part 'book_settings_provider.g.dart';
9 |
10 | final _box = AvailableHiveBoxes.individualBookSettingsBox;
11 |
12 | final _logger = Logger('BookSettingsProvider');
13 |
14 | model.BookSettings readFromBoxOrCreate(String bookId) {
15 | final foundSettings = _box.get(bookId);
16 | if (foundSettings != null) {
17 | _logger.fine('found book settings for $bookId in box: $foundSettings');
18 | return foundSettings;
19 | } else {
20 | // create a new settings object
21 | final settings = model.BookSettings(
22 | bookId: bookId,
23 | playerSettings: const NullablePlayerSettings(),
24 | );
25 | _logger.fine('created new book settings for $bookId: $settings');
26 | writeToBox(settings);
27 | return settings;
28 | }
29 | }
30 |
31 | void writeToBox(model.BookSettings newSettings) {
32 | _box.put(newSettings.bookId, newSettings);
33 | _logger.fine(
34 | 'wrote book settings for ${newSettings.bookId} to box: $newSettings',
35 | );
36 | }
37 |
38 | void updateState(model.BookSettings newSettings, {bool force = false}) {
39 | // check if the settings are different
40 | final foundSettings = _box.get(newSettings.bookId);
41 | if (foundSettings == newSettings && !force) {
42 | return;
43 | }
44 | writeToBox(newSettings);
45 | }
46 |
47 | @riverpod
48 | class BookSettings extends _$BookSettings {
49 | @override
50 | model.BookSettings build(String bookId) {
51 | return readFromBoxOrCreate(bookId);
52 | }
53 |
54 | void update(model.BookSettings newSettings, {bool force = false}) {
55 | updateState(newSettings, force: force);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/features/playback_reporting/providers/playback_reporter_provider.dart:
--------------------------------------------------------------------------------
1 | import 'package:package_info_plus/package_info_plus.dart';
2 | import 'package:riverpod_annotation/riverpod_annotation.dart';
3 | import 'package:vaani/api/api_provider.dart';
4 | import 'package:vaani/features/playback_reporting/core/playback_reporter.dart'
5 | as core;
6 | import 'package:vaani/features/player/providers/audiobook_player.dart';
7 | import 'package:vaani/settings/app_settings_provider.dart';
8 | import 'package:vaani/settings/metadata/metadata_provider.dart';
9 |
10 | part 'playback_reporter_provider.g.dart';
11 |
12 | @Riverpod(keepAlive: true)
13 | class PlaybackReporter extends _$PlaybackReporter {
14 | @override
15 | Future build() async {
16 | final playerSettings = ref.watch(appSettingsProvider).playerSettings;
17 | final player = ref.watch(simpleAudiobookPlayerProvider);
18 | final packageInfo = await PackageInfo.fromPlatform();
19 | final api = ref.watch(authenticatedApiProvider);
20 | final deviceName = await ref.watch(deviceNameProvider.future);
21 | final deviceModel = await ref.watch(deviceModelProvider.future);
22 | final deviceSdkVersion = await ref.watch(deviceSdkVersionProvider.future);
23 | final deviceManufacturer =
24 | await ref.watch(deviceManufacturerProvider.future);
25 |
26 | final reporter = core.PlaybackReporter(
27 | player,
28 | api,
29 | reportingInterval: playerSettings.playbackReportInterval,
30 | markCompleteWhenTimeLeft: playerSettings.markCompleteWhenTimeLeft,
31 | minimumPositionForReporting: playerSettings.minimumPositionForReporting,
32 | deviceName: deviceName,
33 | deviceModel: deviceModel,
34 | deviceSdkVersion: deviceSdkVersion,
35 | deviceClientName: packageInfo.appName,
36 | deviceClientVersion: packageInfo.version,
37 | deviceManufacturer: deviceManufacturer,
38 | );
39 | ref.onDispose(reporter.dispose);
40 | return reporter;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/lib/features/playback_reporting/providers/playback_reporter_provider.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'playback_reporter_provider.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$playbackReporterHash() => r'f5436d652e51c37bcc684acdaec94e17a97e68e5';
10 |
11 | /// See also [PlaybackReporter].
12 | @ProviderFor(PlaybackReporter)
13 | final playbackReporterProvider =
14 | AsyncNotifierProvider.internal(
15 | PlaybackReporter.new,
16 | name: r'playbackReporterProvider',
17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
18 | ? null
19 | : _$playbackReporterHash,
20 | dependencies: null,
21 | allTransitiveDependencies: null,
22 | );
23 |
24 | typedef _$PlaybackReporter = AsyncNotifier;
25 | // ignore_for_file: type=lint
26 | // 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
27 |
--------------------------------------------------------------------------------
/lib/features/player/core/init.dart:
--------------------------------------------------------------------------------
1 | import 'package:audio_service/audio_service.dart';
2 | import 'package:audio_session/audio_session.dart';
3 | import 'package:just_audio_background/just_audio_background.dart'
4 | show JustAudioBackground, NotificationConfig;
5 | import 'package:just_audio_media_kit/just_audio_media_kit.dart'
6 | show JustAudioMediaKit;
7 | import 'package:vaani/settings/app_settings_provider.dart';
8 | import 'package:vaani/settings/models/app_settings.dart';
9 |
10 | Future configurePlayer() async {
11 | // for playing audio on windows, linux
12 | JustAudioMediaKit.ensureInitialized();
13 |
14 | // for configuring how this app will interact with other audio apps
15 | final session = await AudioSession.instance;
16 | await session.configure(const AudioSessionConfiguration.speech());
17 |
18 | final appSettings = loadOrCreateAppSettings();
19 |
20 | // for playing audio in the background
21 | await JustAudioBackground.init(
22 | androidNotificationChannelId: 'com.vaani.bg_demo.channel.audio',
23 | androidNotificationChannelName: 'Audio playback',
24 | androidNotificationOngoing: false,
25 | androidStopForegroundOnPause: false,
26 | androidNotificationChannelDescription: 'Audio playback in the background',
27 | androidNotificationIcon: 'drawable/ic_stat_logo',
28 | rewindInterval: appSettings.notificationSettings.rewindInterval,
29 | fastForwardInterval: appSettings.notificationSettings.fastForwardInterval,
30 | androidShowNotificationBadge: false,
31 | notificationConfigBuilder: (state) {
32 | final controls = [
33 | if (appSettings.notificationSettings.mediaControls
34 | .contains(NotificationMediaControl.skipToPreviousChapter) &&
35 | state.hasPrevious)
36 | MediaControl.skipToPrevious,
37 | if (appSettings.notificationSettings.mediaControls
38 | .contains(NotificationMediaControl.rewind))
39 | MediaControl.rewind,
40 | if (state.playing) MediaControl.pause else MediaControl.play,
41 | if (appSettings.notificationSettings.mediaControls
42 | .contains(NotificationMediaControl.fastForward))
43 | MediaControl.fastForward,
44 | if (appSettings.notificationSettings.mediaControls
45 | .contains(NotificationMediaControl.skipToNextChapter) &&
46 | state.hasNext)
47 | MediaControl.skipToNext,
48 | if (appSettings.notificationSettings.mediaControls
49 | .contains(NotificationMediaControl.stop))
50 | MediaControl.stop,
51 | ];
52 | return NotificationConfig(
53 | controls: controls,
54 | systemActions: const {
55 | MediaAction.seek,
56 | MediaAction.seekForward,
57 | MediaAction.seekBackward,
58 | },
59 | );
60 | },
61 | );
62 | }
63 |
--------------------------------------------------------------------------------
/lib/features/player/playlist.dart:
--------------------------------------------------------------------------------
1 | import 'package:shelfsdk/audiobookshelf_api.dart';
2 |
3 | /// will manage the playlist of items
4 | ///
5 | /// you are responsible for updating the current index and sub index
6 | class AudiobookPlaylist {
7 | /// list of items in the playlist
8 | final List books;
9 |
10 | /// current index of the item in the playlist
11 | int _currentIndex;
12 |
13 | /// current index of the audio file in the current item
14 | int _subCurrentIndex;
15 |
16 | // wrappers for adding and removing items
17 | void add(BookExpanded item) => books.add(item);
18 | void remove(BookExpanded item) => books.remove(item);
19 | void clear() {
20 | books.clear();
21 | _currentIndex = 0;
22 | _subCurrentIndex = 0;
23 | }
24 |
25 | // move an item from one index to another
26 | void move(int from, int to) {
27 | final item = books.removeAt(from);
28 | books.insert(to, item);
29 | }
30 |
31 | /// the book being played
32 | BookExpanded? get currentBook {
33 | if (_currentIndex >= books.length || _currentIndex < 0 || books.isEmpty) {
34 | return null;
35 | }
36 | return books[_currentIndex];
37 | }
38 |
39 | /// of the book in the playlist
40 | int get currentIndex => _currentIndex;
41 | // every time current index changes, we need to update the sub index
42 | set currentIndex(int index) {
43 | // if the index is the same, do nothing
44 | if (_currentIndex == index) {
45 | return;
46 | }
47 | _currentIndex = index;
48 | subCurrentIndex = 0;
49 | }
50 |
51 | /// of the audio file in the current book
52 | int get subCurrentIndex => _subCurrentIndex;
53 |
54 | set subCurrentIndex(int index) {
55 | if (index < 0) {
56 | index = 0;
57 | }
58 | _subCurrentIndex = index;
59 | }
60 |
61 | AudiobookPlaylist({
62 | this.books = const [],
63 | currentIndex = 0,
64 | subCurrentIndex = 0,
65 | }) : _currentIndex = currentIndex,
66 | _subCurrentIndex = subCurrentIndex;
67 |
68 | // most important method, gets the audio file to play
69 | // this is needed as a library item is a list of audio files
70 | AudioTrack? getAudioTrack() {
71 | final book = currentBook;
72 | if (book == null) {
73 | return null;
74 | }
75 |
76 | if (subCurrentIndex > book.tracks.length || book.tracks.isEmpty) {
77 | return null;
78 | }
79 | return book.tracks[subCurrentIndex];
80 | }
81 |
82 | bool get isBookFinished => subCurrentIndex >= currentBook!.tracks.length;
83 |
84 | // a method to get the next audio file and advance the sub index
85 | }
86 |
--------------------------------------------------------------------------------
/lib/features/player/playlist_provider.dart:
--------------------------------------------------------------------------------
1 | import 'package:riverpod_annotation/riverpod_annotation.dart';
2 | import 'package:shelfsdk/audiobookshelf_api.dart';
3 | import 'package:vaani/features/player/playlist.dart';
4 |
5 | part 'playlist_provider.g.dart';
6 |
7 | @riverpod
8 | class Playlist extends _$Playlist {
9 | @override
10 | AudiobookPlaylist build() {
11 | return AudiobookPlaylist();
12 | }
13 |
14 | void add(BookExpanded item) {
15 | state.add(item);
16 | ref.notifyListeners();
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lib/features/player/playlist_provider.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'playlist_provider.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$playlistHash() => r'bed4642e4c2de829e4d0630cb5bf92bffeeb1f60';
10 |
11 | /// See also [Playlist].
12 | @ProviderFor(Playlist)
13 | final playlistProvider =
14 | AutoDisposeNotifierProvider.internal(
15 | Playlist.new,
16 | name: r'playlistProvider',
17 | debugGetCreateSourceHash:
18 | const bool.fromEnvironment('dart.vm.product') ? null : _$playlistHash,
19 | dependencies: null,
20 | allTransitiveDependencies: null,
21 | );
22 |
23 | typedef _$Playlist = AutoDisposeNotifier;
24 | // ignore_for_file: type=lint
25 | // 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
26 |
--------------------------------------------------------------------------------
/lib/features/player/providers/audiobook_player.dart:
--------------------------------------------------------------------------------
1 | import 'package:logging/logging.dart';
2 | import 'package:riverpod_annotation/riverpod_annotation.dart';
3 | import 'package:vaani/api/api_provider.dart';
4 | import 'package:vaani/features/player/core/audiobook_player.dart' as core;
5 |
6 | part 'audiobook_player.g.dart';
7 |
8 | final _logger = Logger('AudiobookPlayerProvider');
9 |
10 | const playerId = 'audiobook_player';
11 |
12 | /// Simple because it doesn't rebuild when the player state changes
13 | /// it only rebuilds when the token changes
14 | @Riverpod(keepAlive: true)
15 | class SimpleAudiobookPlayer extends _$SimpleAudiobookPlayer {
16 | @override
17 | core.AudiobookPlayer build() {
18 | final api = ref.watch(authenticatedApiProvider);
19 | final player = core.AudiobookPlayer(
20 | api.token!,
21 | api.baseUrl,
22 | );
23 |
24 | ref.onDispose(player.dispose);
25 | _logger.finer('created simple player');
26 |
27 | return player;
28 | }
29 | }
30 |
31 | @Riverpod(keepAlive: true)
32 | class AudiobookPlayer extends _$AudiobookPlayer {
33 | @override
34 | core.AudiobookPlayer build() {
35 | final player = ref.watch(simpleAudiobookPlayerProvider);
36 |
37 | ref.onDispose(player.dispose);
38 |
39 | // bind notify listeners to the player
40 | player.playerStateStream.listen((_) {
41 | ref.notifyListeners();
42 | });
43 |
44 | _logger.finer('created player');
45 |
46 | return player;
47 | }
48 |
49 | Future setSpeed(double speed) async {
50 | await state.setSpeed(speed);
51 | ref.notifyListeners();
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib/features/player/providers/audiobook_player.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'audiobook_player.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$simpleAudiobookPlayerHash() =>
10 | r'5e94bbff4314adceb5affa704fc4d079d4016afa';
11 |
12 | /// Simple because it doesn't rebuild when the player state changes
13 | /// it only rebuilds when the token changes
14 | ///
15 | /// Copied from [SimpleAudiobookPlayer].
16 | @ProviderFor(SimpleAudiobookPlayer)
17 | final simpleAudiobookPlayerProvider =
18 | NotifierProvider.internal(
19 | SimpleAudiobookPlayer.new,
20 | name: r'simpleAudiobookPlayerProvider',
21 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
22 | ? null
23 | : _$simpleAudiobookPlayerHash,
24 | dependencies: null,
25 | allTransitiveDependencies: null,
26 | );
27 |
28 | typedef _$SimpleAudiobookPlayer = Notifier;
29 | String _$audiobookPlayerHash() => r'0f180308067486896fec6a65a6afb0e6686ac4a0';
30 |
31 | /// See also [AudiobookPlayer].
32 | @ProviderFor(AudiobookPlayer)
33 | final audiobookPlayerProvider =
34 | NotifierProvider.internal(
35 | AudiobookPlayer.new,
36 | name: r'audiobookPlayerProvider',
37 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
38 | ? null
39 | : _$audiobookPlayerHash,
40 | dependencies: null,
41 | allTransitiveDependencies: null,
42 | );
43 |
44 | typedef _$AudiobookPlayer = Notifier;
45 | // ignore_for_file: type=lint
46 | // 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
47 |
--------------------------------------------------------------------------------
/lib/features/player/providers/currently_playing_provider.dart:
--------------------------------------------------------------------------------
1 | import 'package:hooks_riverpod/hooks_riverpod.dart';
2 | import 'package:logging/logging.dart';
3 | import 'package:riverpod_annotation/riverpod_annotation.dart';
4 | import 'package:shelfsdk/audiobookshelf_api.dart';
5 | import 'package:vaani/features/player/providers/audiobook_player.dart';
6 | import 'package:vaani/shared/extensions/model_conversions.dart';
7 |
8 | part 'currently_playing_provider.g.dart';
9 |
10 | final _logger = Logger('CurrentlyPlayingProvider');
11 |
12 | @riverpod
13 | BookExpanded? currentlyPlayingBook(Ref ref) {
14 | try {
15 | final player = ref.watch(audiobookPlayerProvider);
16 | return player.book;
17 | } catch (e) {
18 | _logger.warning('Error getting currently playing book: $e');
19 | return null;
20 | }
21 | }
22 |
23 | /// provided the current chapter of the book being played
24 | @riverpod
25 | BookChapter? currentPlayingChapter(Ref ref) {
26 | final player = ref.watch(audiobookPlayerProvider);
27 | player.slowPositionStream.listen((_) {
28 | ref.invalidateSelf();
29 | });
30 |
31 | return player.currentChapter;
32 | }
33 |
34 | /// provides the book metadata of the currently playing book
35 | @riverpod
36 | BookMetadataExpanded? currentBookMetadata(Ref ref) {
37 | final player = ref.watch(audiobookPlayerProvider);
38 | if (player.book == null) return null;
39 | return player.book!.metadata.asBookMetadataExpanded;
40 | }
41 |
42 | // /// volume of the player [0, 1]
43 | // @riverpod
44 | // double currentVolume(CurrentVolumeRef ref) {
45 | // return 1;
46 | // }
47 |
--------------------------------------------------------------------------------
/lib/features/player/providers/currently_playing_provider.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'currently_playing_provider.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$currentlyPlayingBookHash() =>
10 | r'e4258694c8f0d1e89651b330fae0f672ca13a484';
11 |
12 | /// See also [currentlyPlayingBook].
13 | @ProviderFor(currentlyPlayingBook)
14 | final currentlyPlayingBookProvider =
15 | AutoDisposeProvider.internal(
16 | currentlyPlayingBook,
17 | name: r'currentlyPlayingBookProvider',
18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
19 | ? null
20 | : _$currentlyPlayingBookHash,
21 | dependencies: null,
22 | allTransitiveDependencies: null,
23 | );
24 |
25 | @Deprecated('Will be removed in 3.0. Use Ref instead')
26 | // ignore: unused_element
27 | typedef CurrentlyPlayingBookRef = AutoDisposeProviderRef;
28 | String _$currentPlayingChapterHash() =>
29 | r'73db8b8a9058573bb0c68ec5d5f8aba9306f3d24';
30 |
31 | /// provided the current chapter of the book being played
32 | ///
33 | /// Copied from [currentPlayingChapter].
34 | @ProviderFor(currentPlayingChapter)
35 | final currentPlayingChapterProvider =
36 | AutoDisposeProvider.internal(
37 | currentPlayingChapter,
38 | name: r'currentPlayingChapterProvider',
39 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
40 | ? null
41 | : _$currentPlayingChapterHash,
42 | dependencies: null,
43 | allTransitiveDependencies: null,
44 | );
45 |
46 | @Deprecated('Will be removed in 3.0. Use Ref instead')
47 | // ignore: unused_element
48 | typedef CurrentPlayingChapterRef = AutoDisposeProviderRef;
49 | String _$currentBookMetadataHash() =>
50 | r'f537ef4ef19280bc952de658ecf6520c535ae344';
51 |
52 | /// provides the book metadata of the currently playing book
53 | ///
54 | /// Copied from [currentBookMetadata].
55 | @ProviderFor(currentBookMetadata)
56 | final currentBookMetadataProvider =
57 | AutoDisposeProvider.internal(
58 | currentBookMetadata,
59 | name: r'currentBookMetadataProvider',
60 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
61 | ? null
62 | : _$currentBookMetadataHash,
63 | dependencies: null,
64 | allTransitiveDependencies: null,
65 | );
66 |
67 | @Deprecated('Will be removed in 3.0. Use Ref instead')
68 | // ignore: unused_element
69 | typedef CurrentBookMetadataRef = AutoDisposeProviderRef;
70 | // ignore_for_file: type=lint
71 | // 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
72 |
--------------------------------------------------------------------------------
/lib/features/player/providers/player_form.dart:
--------------------------------------------------------------------------------
1 | // this provider is used to manage the player form state
2 | // it will inform about the percentage of the player expanded
3 |
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter/widgets.dart';
6 | import 'package:hooks_riverpod/hooks_riverpod.dart';
7 | import 'package:miniplayer/miniplayer.dart';
8 | import 'package:riverpod_annotation/riverpod_annotation.dart';
9 | import 'package:vaani/features/player/providers/audiobook_player.dart';
10 |
11 | part 'player_form.g.dart';
12 |
13 | /// The height of the player when it is minimized
14 | const double playerMinHeight = 70;
15 | // const miniplayerPercentageDeclaration = 0.2;
16 |
17 | extension on Ref {
18 | // We can move the previous logic to a Ref extension.
19 | // This enables reusing the logic between providers
20 | T disposeAndListenChangeNotifier(T notifier) {
21 | onDispose(notifier.dispose);
22 | notifier.addListener(notifyListeners);
23 | // We return the notifier to ease the usage a bit
24 | return notifier;
25 | }
26 | }
27 |
28 | @Riverpod(keepAlive: true)
29 | Raw> playerExpandProgressNotifier(
30 | Ref ref,
31 | ) {
32 | final ValueNotifier playerExpandProgress =
33 | ValueNotifier(playerMinHeight);
34 |
35 | return ref.disposeAndListenChangeNotifier(playerExpandProgress);
36 | }
37 |
38 | // @Riverpod(keepAlive: true)
39 | // Raw> dragDownPercentageNotifier(
40 | // DragDownPercentageNotifierRef ref,
41 | // ) {
42 | // final ValueNotifier notifier = ValueNotifier(0);
43 |
44 | // return ref.disposeAndListenChangeNotifier(notifier);
45 | // }
46 |
47 | // a provider that will listen to the playerExpandProgressNotifier and return the percentage of the player expanded
48 | @Riverpod(keepAlive: true)
49 | double playerHeight(
50 | Ref ref,
51 | ) {
52 | final playerExpandProgress = ref.watch(playerExpandProgressNotifierProvider);
53 |
54 | // on change of the playerExpandProgress invalidate
55 | playerExpandProgress.addListener(() {
56 | ref.invalidateSelf();
57 | });
58 |
59 | // listen to the playerExpandProgressNotifier and return the value
60 | return playerExpandProgress.value;
61 | }
62 |
63 | final audioBookMiniplayerController = MiniplayerController();
64 |
65 | @Riverpod(keepAlive: true)
66 | bool isPlayerActive(
67 | Ref ref,
68 | ) {
69 | try {
70 | final player = ref.watch(audiobookPlayerProvider);
71 | if (player.book != null) {
72 | return true;
73 | } else {
74 | final playerHeight = ref.watch(playerHeightProvider);
75 | return playerHeight < playerMinHeight;
76 | }
77 | } catch (e) {
78 | return false;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/lib/features/player/providers/player_form.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'player_form.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$playerExpandProgressNotifierHash() =>
10 | r'1ac7172d90a070f96222286edd1a176be197f378';
11 |
12 | /// See also [playerExpandProgressNotifier].
13 | @ProviderFor(playerExpandProgressNotifier)
14 | final playerExpandProgressNotifierProvider =
15 | Provider>>.internal(
16 | playerExpandProgressNotifier,
17 | name: r'playerExpandProgressNotifierProvider',
18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
19 | ? null
20 | : _$playerExpandProgressNotifierHash,
21 | dependencies: null,
22 | allTransitiveDependencies: null,
23 | );
24 |
25 | @Deprecated('Will be removed in 3.0. Use Ref instead')
26 | // ignore: unused_element
27 | typedef PlayerExpandProgressNotifierRef
28 | = ProviderRef>>;
29 | String _$playerHeightHash() => r'3f031eaffdffbb2c6ddf7eb1aba31bf1619260fc';
30 |
31 | /// See also [playerHeight].
32 | @ProviderFor(playerHeight)
33 | final playerHeightProvider = Provider.internal(
34 | playerHeight,
35 | name: r'playerHeightProvider',
36 | debugGetCreateSourceHash:
37 | const bool.fromEnvironment('dart.vm.product') ? null : _$playerHeightHash,
38 | dependencies: null,
39 | allTransitiveDependencies: null,
40 | );
41 |
42 | @Deprecated('Will be removed in 3.0. Use Ref instead')
43 | // ignore: unused_element
44 | typedef PlayerHeightRef = ProviderRef;
45 | String _$isPlayerActiveHash() => r'2c7ca125423126fb5f0ef218d37bc8fe0ca9ec98';
46 |
47 | /// See also [isPlayerActive].
48 | @ProviderFor(isPlayerActive)
49 | final isPlayerActiveProvider = Provider.internal(
50 | isPlayerActive,
51 | name: r'isPlayerActiveProvider',
52 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
53 | ? null
54 | : _$isPlayerActiveHash,
55 | dependencies: null,
56 | allTransitiveDependencies: null,
57 | );
58 |
59 | @Deprecated('Will be removed in 3.0. Use Ref instead')
60 | // ignore: unused_element
61 | typedef IsPlayerActiveRef = ProviderRef;
62 | // ignore_for_file: type=lint
63 | // 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
64 |
--------------------------------------------------------------------------------
/lib/features/player/view/mini_player_bottom_padding.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:vaani/features/player/providers/player_form.dart';
4 |
5 | class MiniPlayerBottomPadding extends HookConsumerWidget {
6 | const MiniPlayerBottomPadding({super.key});
7 | @override
8 | Widget build(BuildContext context, WidgetRef ref) {
9 | return AnimatedSize(
10 | duration: const Duration(milliseconds: 200),
11 | child: ref.watch(isPlayerActiveProvider)
12 | ? const SizedBox(height: playerMinHeight + 8)
13 | : const SizedBox.shrink(),
14 | );
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/features/player/view/widgets/audiobook_player_seek_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:vaani/constants/sizes.dart';
4 | import 'package:vaani/features/player/providers/audiobook_player.dart';
5 |
6 | class AudiobookPlayerSeekButton extends HookConsumerWidget {
7 | const AudiobookPlayerSeekButton({
8 | super.key,
9 | required this.isForward,
10 | });
11 |
12 | /// if true, the button seeks forward, else it seeks backwards
13 | final bool isForward;
14 |
15 | @override
16 | Widget build(BuildContext context, WidgetRef ref) {
17 | final player = ref.watch(audiobookPlayerProvider);
18 | return IconButton(
19 | icon: Icon(
20 | isForward ? Icons.forward_30 : Icons.replay_30,
21 | size: AppElementSizes.iconSizeSmall,
22 | ),
23 | onPressed: () {
24 | if (isForward) {
25 | player.seek(player.positionInBook + const Duration(seconds: 30));
26 | } else {
27 | player.seek(player.positionInBook - const Duration(seconds: 30));
28 | }
29 | },
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/lib/features/player/view/widgets/audiobook_player_seek_chapter_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:shelfsdk/audiobookshelf_api.dart';
4 | import 'package:vaani/constants/sizes.dart';
5 | import 'package:vaani/features/player/providers/audiobook_player.dart';
6 |
7 | class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
8 | const AudiobookPlayerSeekChapterButton({
9 | super.key,
10 | required this.isForward,
11 | });
12 |
13 | /// if true, the button seeks forward, else it seeks backwards
14 | final bool isForward;
15 |
16 | @override
17 | Widget build(BuildContext context, WidgetRef ref) {
18 | final player = ref.watch(audiobookPlayerProvider);
19 |
20 | // add a small offset so the display does not show the previous chapter for a split second
21 | const offset = Duration(milliseconds: 10);
22 |
23 | /// time into the current chapter to determine if we should go to the previous chapter or the start of the current chapter
24 | const doNotSeekBackIfLessThan = Duration(seconds: 5);
25 |
26 | /// seek forward to the next chapter
27 | void seekForward() {
28 | final index = player.book!.chapters.indexOf(player.currentChapter!);
29 | if (index < player.book!.chapters.length - 1) {
30 | player.seek(
31 | player.book!.chapters[index + 1].start + offset,
32 | );
33 | } else {
34 | player.seek(player.currentChapter!.end);
35 | }
36 | }
37 |
38 | /// seek backward to the previous chapter or the start of the current chapter
39 | void seekBackward() {
40 | final currentPlayingChapterIndex =
41 | player.book!.chapters.indexOf(player.currentChapter!);
42 | final chapterPosition =
43 | player.positionInBook - player.currentChapter!.start;
44 | BookChapter chapterToSeekTo;
45 | // if player position is less than 5 seconds into the chapter, go to the previous chapter
46 | if (chapterPosition < doNotSeekBackIfLessThan &&
47 | currentPlayingChapterIndex > 0) {
48 | chapterToSeekTo = player.book!.chapters[currentPlayingChapterIndex - 1];
49 | } else {
50 | chapterToSeekTo = player.currentChapter!;
51 | }
52 | player.seek(
53 | chapterToSeekTo.start + offset,
54 | );
55 | }
56 |
57 | return IconButton(
58 | icon: Icon(
59 | isForward ? Icons.skip_next : Icons.skip_previous,
60 | size: AppElementSizes.iconSizeSmall,
61 | ),
62 | onPressed: () {
63 | if (player.book == null) {
64 | return;
65 | }
66 | // if chapter does not exist, go to the start or end of the book
67 | if (player.currentChapter == null) {
68 | player.seek(isForward ? player.book!.duration : Duration.zero);
69 | return;
70 | }
71 | if (isForward) {
72 | seekForward();
73 | } else {
74 | seekBackward();
75 | }
76 | },
77 | );
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/lib/features/player/view/widgets/player_speed_adjust_button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:logging/logging.dart';
4 | import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart';
5 | import 'package:vaani/features/player/providers/audiobook_player.dart';
6 | import 'package:vaani/features/player/view/player_when_expanded.dart';
7 | import 'package:vaani/features/player/view/widgets/speed_selector.dart';
8 | import 'package:vaani/settings/app_settings_provider.dart';
9 |
10 | final _logger = Logger('PlayerSpeedAdjustButton');
11 |
12 | class PlayerSpeedAdjustButton extends HookConsumerWidget {
13 | const PlayerSpeedAdjustButton({
14 | super.key,
15 | });
16 |
17 | @override
18 | Widget build(BuildContext context, WidgetRef ref) {
19 | final player = ref.watch(audiobookPlayerProvider);
20 | final bookId = player.book?.libraryItemId ?? '_';
21 | final bookSettings = ref.watch(bookSettingsProvider(bookId));
22 | final appSettings = ref.watch(appSettingsProvider);
23 | final notifier = ref.watch(audiobookPlayerProvider.notifier);
24 | return TextButton(
25 | child: Text('${player.speed}x'),
26 | onPressed: () async {
27 | pendingPlayerModals++;
28 | _logger.fine('opening speed selector');
29 | await showModalBottomSheet(
30 | context: context,
31 | barrierLabel: 'Select Speed',
32 | builder: (context) {
33 | return SpeedSelector(
34 | onSpeedSelected: (speed) {
35 | notifier.setSpeed(speed);
36 | if (appSettings.playerSettings.configurePlayerForEveryBook) {
37 | ref
38 | .read(
39 | bookSettingsProvider(bookId).notifier,
40 | )
41 | .update(
42 | bookSettings.copyWith
43 | .playerSettings(preferredDefaultSpeed: speed),
44 | );
45 | } else {
46 | ref
47 | .read(
48 | appSettingsProvider.notifier,
49 | )
50 | .update(
51 | appSettings.copyWith
52 | .playerSettings(preferredDefaultSpeed: speed),
53 | );
54 | }
55 | },
56 | );
57 | },
58 | );
59 | pendingPlayerModals--;
60 | _logger.fine('Closing speed selector');
61 | },
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/features/shake_detection/core/shake_detector.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:math';
3 |
4 | import 'package:logging/logging.dart';
5 | import 'package:sensors_plus/sensors_plus.dart';
6 | import 'package:vaani/settings/models/app_settings.dart';
7 |
8 | final _logger = Logger('ShakeDetector');
9 |
10 | class ShakeDetector {
11 | final ShakeDetectionSettings _settings;
12 | final Function()? onShakeDetected;
13 |
14 | ShakeDetector(
15 | this._settings,
16 | this.onShakeDetected, {
17 | startImmediately = true,
18 | }) {
19 | _logger.fine('ShakeDetector created with settings: $_settings');
20 | if (startImmediately) {
21 | start();
22 | }
23 | }
24 |
25 | StreamSubscription? _accelerometerSubscription;
26 |
27 | int _currentShakeCount = 0;
28 |
29 | DateTime _lastShakeTime = DateTime.now();
30 |
31 | final StreamController
32 | _detectedShakeStreamController = StreamController.broadcast();
33 |
34 | void start() {
35 | if (_accelerometerSubscription != null) {
36 | _logger.warning('ShakeDetector is already running');
37 | return;
38 | }
39 | _accelerometerSubscription =
40 | userAccelerometerEventStream(samplingPeriod: _settings.samplingPeriod)
41 | .listen((event) {
42 | _logger.finest('RMS: ${event.rms}');
43 | if (event.rms > _settings.threshold) {
44 | _currentShakeCount++;
45 |
46 | if (_currentShakeCount >= _settings.shakeTriggerCount &&
47 | !isCoolDownNeeded()) {
48 | _logger.fine('Shake detected $_currentShakeCount times');
49 |
50 | onShakeDetected?.call();
51 | _detectedShakeStreamController.add(event);
52 |
53 | _lastShakeTime = DateTime.now();
54 | _currentShakeCount = 0;
55 | }
56 | } else {
57 | _currentShakeCount = 0;
58 | }
59 | });
60 |
61 | _logger.fine('ShakeDetector started');
62 | }
63 |
64 | void stop() {
65 | _currentShakeCount = 0;
66 | _accelerometerSubscription?.cancel();
67 | _accelerometerSubscription = null;
68 | _detectedShakeStreamController.close();
69 | _logger.fine('ShakeDetector stopped');
70 | }
71 |
72 | void dispose() {
73 | stop();
74 | }
75 |
76 | bool isCoolDownNeeded() {
77 | return _lastShakeTime
78 | .add(_settings.shakeTriggerCoolDown)
79 | .isAfter(DateTime.now());
80 | }
81 | }
82 |
83 | extension UserAccelerometerEventRMS on UserAccelerometerEvent {
84 | double get rms => sqrt(x * x + y * y + z * z);
85 | }
86 |
--------------------------------------------------------------------------------
/lib/features/shake_detection/providers/shake_detector.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'shake_detector.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$shakeDetectorHash() => r'2a380bab1d4021d05d2ae40fec964a5f33d3730c';
10 |
11 | /// See also [ShakeDetector].
12 | @ProviderFor(ShakeDetector)
13 | final shakeDetectorProvider =
14 | AutoDisposeNotifierProvider.internal(
15 | ShakeDetector.new,
16 | name: r'shakeDetectorProvider',
17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
18 | ? null
19 | : _$shakeDetectorHash,
20 | dependencies: null,
21 | allTransitiveDependencies: null,
22 | );
23 |
24 | typedef _$ShakeDetector = AutoDisposeNotifier;
25 | // ignore_for_file: type=lint
26 | // 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
27 |
--------------------------------------------------------------------------------
/lib/features/sleep_timer/providers/sleep_timer_provider.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:riverpod_annotation/riverpod_annotation.dart';
3 | import 'package:vaani/features/player/providers/audiobook_player.dart';
4 | import 'package:vaani/features/sleep_timer/core/sleep_timer.dart' as core;
5 | import 'package:vaani/settings/app_settings_provider.dart';
6 | import 'package:vaani/shared/extensions/time_of_day.dart';
7 |
8 | part 'sleep_timer_provider.g.dart';
9 |
10 | @Riverpod(keepAlive: true)
11 | class SleepTimer extends _$SleepTimer {
12 | @override
13 | core.SleepTimer? build() {
14 | final sleepTimerSettings = ref.watch(sleepTimerSettingsProvider);
15 | if (!sleepTimerSettings.autoTurnOnTimer) {
16 | return null;
17 | }
18 |
19 | if ((!sleepTimerSettings.alwaysAutoTurnOnTimer) &&
20 | !shouldBuildRightNow(
21 | sleepTimerSettings.autoTurnOnTime,
22 | sleepTimerSettings.autoTurnOffTime,
23 | )) {
24 | return null;
25 | }
26 |
27 | var sleepTimer = core.SleepTimer(
28 | duration: sleepTimerSettings.defaultDuration,
29 | player: ref.watch(simpleAudiobookPlayerProvider),
30 | );
31 | ref.onDispose(sleepTimer.dispose);
32 | return sleepTimer;
33 | }
34 |
35 | void setTimer(Duration? resultingDuration, {bool notifyListeners = true}) {
36 | if (resultingDuration == null || resultingDuration.inSeconds == 0) {
37 | cancelTimer();
38 | return;
39 | }
40 | if (state != null) {
41 | state!.duration = resultingDuration;
42 | if (notifyListeners) {
43 | ref.notifyListeners();
44 | }
45 | } else {
46 | final timer = core.SleepTimer(
47 | duration: resultingDuration,
48 | player: ref.watch(simpleAudiobookPlayerProvider),
49 | );
50 | ref.onDispose(timer.dispose);
51 | state = timer;
52 | state!.startCountDown();
53 | }
54 | }
55 |
56 | void restartTimer() {
57 | state?.restartTimer();
58 |
59 | // ref.notifyListeners(); // see https://github.com/Dr-Blank/Vaani/pull/40 for more information on why this is commented out
60 | }
61 |
62 | void cancelTimer() {
63 | state?.dispose();
64 | state = null;
65 | }
66 | }
67 |
68 | bool shouldBuildRightNow(Duration autoTurnOnTime, Duration autoTurnOffTime) {
69 | final now = TimeOfDay.now();
70 | return now.isBetween(
71 | autoTurnOnTime.toTimeOfDay(),
72 | autoTurnOffTime.toTimeOfDay(),
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'sleep_timer_provider.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$sleepTimerHash() => r'2679454a217d0630a833d730557ab4e4feac2e56';
10 |
11 | /// See also [SleepTimer].
12 | @ProviderFor(SleepTimer)
13 | final sleepTimerProvider =
14 | NotifierProvider.internal(
15 | SleepTimer.new,
16 | name: r'sleepTimerProvider',
17 | debugGetCreateSourceHash:
18 | const bool.fromEnvironment('dart.vm.product') ? null : _$sleepTimerHash,
19 | dependencies: null,
20 | allTransitiveDependencies: null,
21 | );
22 |
23 | typedef _$SleepTimer = Notifier;
24 | // ignore_for_file: type=lint
25 | // 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
26 |
--------------------------------------------------------------------------------
/lib/hacks/fix_autofill_losing_focus.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | /// A workaround for the issue where the autofill loses focus on Android
5 | ///
6 | /// Example usage:
7 | /// ```dart
8 | /// InactiveFocusScopeObserver(
9 | /// child: FormWithTheFeildsThatMayLooseFocus(),
10 | /// )
11 | /// ```
12 | ///
13 | // see https://github.com/flutter/flutter/issues/137760#issuecomment-1956816977
14 | class InactiveFocusScopeObserver extends StatefulWidget {
15 | final Widget child;
16 |
17 | const InactiveFocusScopeObserver({
18 | super.key,
19 | required this.child,
20 | });
21 |
22 | @override
23 | State createState() =>
24 | _InactiveFocusScopeObserverState();
25 | }
26 |
27 | class _InactiveFocusScopeObserverState
28 | extends State {
29 | final FocusScopeNode _focusScope = FocusScopeNode();
30 |
31 | AppLifecycleListener? _listener;
32 | FocusNode? _lastFocusedNode;
33 |
34 | @override
35 | void initState() {
36 | _registerListener();
37 |
38 | super.initState();
39 | }
40 |
41 | @override
42 | Widget build(BuildContext context) => FocusScope(
43 | node: _focusScope,
44 | child: widget.child,
45 | );
46 |
47 | @override
48 | void dispose() {
49 | _listener?.dispose();
50 | _focusScope.dispose();
51 |
52 | super.dispose();
53 | }
54 |
55 | void _registerListener() {
56 | /// optional if you want this workaround for any platform and not just for android
57 | if (defaultTargetPlatform != TargetPlatform.android) {
58 | return;
59 | }
60 |
61 | _listener = AppLifecycleListener(
62 | onInactive: () {
63 | _lastFocusedNode = _focusScope.focusedChild;
64 | },
65 | onResume: () {
66 | _lastFocusedNode = null;
67 | },
68 | );
69 |
70 | _focusScope.addListener(_onFocusChanged);
71 | }
72 |
73 | void _onFocusChanged() {
74 | if (_lastFocusedNode?.hasFocus == false) {
75 | _lastFocusedNode?.requestFocus();
76 | _lastFocusedNode = null;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/lib/models/error_response.dart:
--------------------------------------------------------------------------------
1 | import 'package:http/http.dart' as http;
2 | import 'package:logging/logging.dart';
3 | import 'package:vaani/shared/extensions/obfuscation.dart';
4 |
5 | final _logger = Logger('ErrorResponse');
6 |
7 | class ErrorResponseHandler {
8 | String? name;
9 | http.Response _response;
10 | bool logRawResponse;
11 |
12 | ErrorResponseHandler({
13 | this.name,
14 | http.Response? response,
15 | this.logRawResponse = false,
16 | }) : _response = response ?? http.Response('', 418);
17 |
18 | void storeError(http.Response response, [Object? error]) {
19 | if (logRawResponse) {
20 | _logger.fine('for $name got response: ${response.obfuscate()}');
21 | }
22 | _response = response;
23 | }
24 |
25 | http.Response get response => _response;
26 | }
27 |
--------------------------------------------------------------------------------
/lib/pages/library_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_hooks/flutter_hooks.dart';
3 | import 'package:hooks_riverpod/hooks_riverpod.dart';
4 | import 'package:vaani/api/api_provider.dart';
5 | import 'package:vaani/main.dart';
6 | import 'package:vaani/settings/api_settings_provider.dart';
7 |
8 | import '../shared/widgets/drawer.dart';
9 | import '../shared/widgets/shelves/home_shelf.dart';
10 |
11 | // TODO: implement the library page
12 | class LibraryPage extends HookConsumerWidget {
13 | const LibraryPage({this.libraryId, super.key});
14 |
15 | final String? libraryId;
16 | @override
17 | Widget build(BuildContext context, WidgetRef ref) {
18 | // set the library id as the active library
19 | if (libraryId != null) {
20 | ref.read(apiSettingsProvider.notifier).updateState(
21 | ref.watch(apiSettingsProvider).copyWith(activeLibraryId: libraryId),
22 | );
23 | }
24 |
25 | final views = ref.watch(personalizedViewProvider);
26 | final scrollController = useScrollController();
27 |
28 | return Scaffold(
29 | appBar: AppBar(
30 | title: GestureDetector(
31 | child: const Text('Vaani'),
32 | onTap: () {
33 | // scroll to the top of the page
34 | scrollController.animateTo(
35 | 0,
36 | duration: const Duration(milliseconds: 300),
37 | curve: Curves.easeInOut,
38 | );
39 | // refresh the view
40 | ref.invalidate(personalizedViewProvider);
41 | },
42 | ),
43 | ),
44 | drawer: const MyDrawer(),
45 | body: Container(
46 | child: views.when(
47 | data: (data) {
48 | final shelvesToDisplay = data
49 | // .where((element) => !element.id.contains('discover'))
50 | .map((shelf) {
51 | appLogger.fine('building shelf ${shelf.label}');
52 | return HomeShelf(
53 | title: shelf.label,
54 | shelf: shelf,
55 | );
56 | }).toList();
57 | return RefreshIndicator(
58 | onRefresh: () async {
59 | return ref.refresh(personalizedViewProvider);
60 | },
61 | child: ListView.separated(
62 | itemBuilder: (context, index) => shelvesToDisplay[index],
63 | separatorBuilder: (context, index) => Divider(
64 | color: Theme.of(context).dividerColor.withValues(alpha: 0.1),
65 | indent: 16,
66 | endIndent: 16,
67 | ),
68 | itemCount: shelvesToDisplay.length,
69 | controller: scrollController,
70 | ),
71 | );
72 | },
73 | loading: () => const LibraryPageSkeleton(),
74 | error: (error, stack) {
75 | return Text('Error: $error');
76 | },
77 | ),
78 | ),
79 | );
80 | }
81 | }
82 |
83 | class LibraryPageSkeleton extends StatelessWidget {
84 | const LibraryPageSkeleton({super.key});
85 |
86 | @override
87 | Widget build(BuildContext context) {
88 | return const Scaffold(
89 | body: Center(
90 | child: CircularProgressIndicator(),
91 | ),
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/lib/router/models/library_item_extras.dart:
--------------------------------------------------------------------------------
1 | // a freezed class to store the settings of the app
2 |
3 | import 'package:freezed_annotation/freezed_annotation.dart';
4 | import 'package:shelfsdk/audiobookshelf_api.dart';
5 |
6 | part 'library_item_extras.freezed.dart';
7 |
8 | /// any extras when navigating to a library item
9 | ///
10 | /// [shelfId] is the id of the shelf that the item was on before navigating to the item
11 | /// [book] is the book that the item represents
12 | /// [heroTagSuffix] is the suffix to use for the hero tag to avoid conflicts
13 | @freezed
14 | class LibraryItemExtras with _$LibraryItemExtras {
15 | const factory LibraryItemExtras({
16 | BookMinified? book,
17 | @Default('') String heroTagSuffix,
18 | }) = _LibraryItemExtras;
19 | }
20 |
--------------------------------------------------------------------------------
/lib/router/transitions/slide.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:go_router/go_router.dart';
3 |
4 | // class CustomSlideTransition extends CustomTransitionPage {
5 | // CustomSlideTransition({super.key, required super.child})
6 | // : super(
7 | // transitionDuration: const Duration(milliseconds: 250),
8 | // transitionsBuilder: (_, animation, __, child) {
9 | // return SlideTransition(
10 | // position: animation.drive(
11 | // Tween(
12 | // begin: const Offset(1.5, 0),
13 | // end: Offset.zero,
14 | // ).chain(
15 | // CurveTween(curve: Curves.ease),
16 | // ),
17 | // ),
18 | // child: child,
19 | // );
20 | // },
21 | // );
22 | // }
23 |
24 | CustomTransitionPage buildPageWithDefaultTransition({
25 | required BuildContext context,
26 | required GoRouterState state,
27 | required Widget child,
28 | }) {
29 | return CustomTransitionPage(
30 | key: state.pageKey,
31 | // transitionDuration: 1250.ms,
32 | // reverseTransitionDuration: 1250.ms,
33 | child: child,
34 | transitionsBuilder: (context, animation, secondaryAnimation, child) =>
35 | FadeTransition(
36 | opacity: animation,
37 | child: SlideTransition(
38 | position: animation.drive(
39 | Tween(
40 | begin: const Offset(0, 1.50),
41 | end: Offset.zero,
42 | ).chain(
43 | CurveTween(curve: Curves.easeOut),
44 | ),
45 | ),
46 | child: child,
47 | ),
48 | ),
49 | );
50 | }
51 |
52 | Page Function(BuildContext, GoRouterState) defaultPageBuilder(
53 | Widget child,
54 | ) =>
55 | (BuildContext context, GoRouterState state) {
56 | return buildPageWithDefaultTransition(
57 | context: context,
58 | state: state,
59 | child: child,
60 | );
61 | };
62 |
--------------------------------------------------------------------------------
/lib/settings/api_settings_provider.dart:
--------------------------------------------------------------------------------
1 | // this provider is used to provide the Api settings to the app
2 |
3 | import 'package:logging/logging.dart';
4 | import 'package:riverpod_annotation/riverpod_annotation.dart';
5 | import 'package:vaani/db/available_boxes.dart';
6 | import 'package:vaani/settings/models/api_settings.dart' as model;
7 | import 'package:vaani/shared/extensions/obfuscation.dart';
8 |
9 | part 'api_settings_provider.g.dart';
10 |
11 | final _box = AvailableHiveBoxes.apiSettingsBox;
12 |
13 | final _logger = Logger('ApiSettingsProvider');
14 |
15 | @Riverpod(keepAlive: true)
16 | class ApiSettings extends _$ApiSettings {
17 | @override
18 | model.ApiSettings build() {
19 | state = readFromBoxOrCreate();
20 | ref.listenSelf((_, __) {
21 | writeToBox();
22 | });
23 |
24 | return state;
25 | }
26 |
27 | model.ApiSettings readFromBoxOrCreate() {
28 | // see if the settings are already in the box
29 | if (_box.isNotEmpty) {
30 | var foundSettings = _box.getAt(0);
31 | // foundSettings.activeServer ??= foundSettings.activeUser?.server;
32 | // foundSettings =foundSettings.copyWith(activeServer: foundSettings.activeUser?.server);
33 | if (foundSettings.activeServer == null) {
34 | foundSettings = foundSettings.copyWith(
35 | activeServer: foundSettings.activeUser?.server,
36 | );
37 | }
38 | _logger.fine('found api settings in box: ${foundSettings.obfuscate()}');
39 | return foundSettings;
40 | } else {
41 | // create a new settings object
42 | const settings = model.ApiSettings();
43 | _logger.fine('created new api settings: $settings');
44 | return settings;
45 | }
46 | }
47 |
48 | // write the settings to the box
49 | void writeToBox() {
50 | _box.clear();
51 | _box.add(state);
52 | _logger.fine('wrote api settings to box: ${state.obfuscate()}');
53 | }
54 |
55 | void updateState(model.ApiSettings newSettings, {bool force = false}) {
56 | // check if the settings are different
57 |
58 | if (state == newSettings && !force) {
59 | return;
60 | }
61 | state = newSettings;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/settings/api_settings_provider.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'api_settings_provider.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$apiSettingsHash() => r'5bc1e16e9d72b77fb10637aabadf08e8947da580';
10 |
11 | /// See also [ApiSettings].
12 | @ProviderFor(ApiSettings)
13 | final apiSettingsProvider =
14 | NotifierProvider.internal(
15 | ApiSettings.new,
16 | name: r'apiSettingsProvider',
17 | debugGetCreateSourceHash:
18 | const bool.fromEnvironment('dart.vm.product') ? null : _$apiSettingsHash,
19 | dependencies: null,
20 | allTransitiveDependencies: null,
21 | );
22 |
23 | typedef _$ApiSettings = Notifier;
24 | // ignore_for_file: type=lint
25 | // 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
26 |
--------------------------------------------------------------------------------
/lib/settings/app_settings_provider.dart:
--------------------------------------------------------------------------------
1 | // this provider is used to provide the app settings to the app
2 |
3 | import 'package:logging/logging.dart';
4 | import 'package:riverpod_annotation/riverpod_annotation.dart';
5 | import 'package:vaani/db/available_boxes.dart';
6 | import 'package:vaani/settings/models/app_settings.dart' as model;
7 |
8 | part 'app_settings_provider.g.dart';
9 |
10 | final _box = AvailableHiveBoxes.userPrefsBox;
11 |
12 | final _logger = Logger('AppSettingsProvider');
13 |
14 | model.AppSettings loadOrCreateAppSettings() {
15 | // see if the settings are already in the box
16 | model.AppSettings? settings;
17 | if (_box.isNotEmpty) {
18 | try {
19 | settings = _box.getAt(0);
20 | _logger.fine('found settings in box: $settings');
21 | } catch (e) {
22 | _logger.warning('error reading settings from box: $e'
23 | '\nclearing box');
24 | _box.clear();
25 | }
26 | } else {
27 | _logger.fine('no settings found in box, creating new settings');
28 | }
29 | return settings ?? const model.AppSettings();
30 | }
31 |
32 | @Riverpod(keepAlive: true)
33 | class AppSettings extends _$AppSettings {
34 | @override
35 | model.AppSettings build() {
36 | state = loadOrCreateAppSettings();
37 | ref.listenSelf((_, __) {
38 | writeToBox();
39 | });
40 | return state;
41 | }
42 |
43 | // write the settings to the box
44 | void writeToBox() {
45 | _box.clear();
46 | _box.add(state);
47 | _logger.fine('wrote settings to box: $state');
48 | }
49 |
50 | void update(model.AppSettings newSettings) {
51 | state = newSettings;
52 | }
53 |
54 | void reset() {
55 | state = const model.AppSettings();
56 | }
57 | }
58 |
59 | // SleepTimerSettings provider but only rebuilds when the sleep timer settings change
60 | @Riverpod(keepAlive: true)
61 | class SleepTimerSettings extends _$SleepTimerSettings {
62 | @override
63 | model.SleepTimerSettings build() {
64 | final settings = ref.read(appSettingsProvider).sleepTimerSettings;
65 | state = settings;
66 | ref.listen(appSettingsProvider, (a, b) {
67 | if (a?.sleepTimerSettings != b.sleepTimerSettings) {
68 | state = b.sleepTimerSettings;
69 | }
70 | });
71 | return state;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/settings/app_settings_provider.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'app_settings_provider.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$appSettingsHash() => r'314d7936f54550f57d308056a99230402342a6d0';
10 |
11 | /// See also [AppSettings].
12 | @ProviderFor(AppSettings)
13 | final appSettingsProvider =
14 | NotifierProvider.internal(
15 | AppSettings.new,
16 | name: r'appSettingsProvider',
17 | debugGetCreateSourceHash:
18 | const bool.fromEnvironment('dart.vm.product') ? null : _$appSettingsHash,
19 | dependencies: null,
20 | allTransitiveDependencies: null,
21 | );
22 |
23 | typedef _$AppSettings = Notifier;
24 | String _$sleepTimerSettingsHash() =>
25 | r'85bb3d3fb292b9a3a5b771d86e5fc57718519c69';
26 |
27 | /// See also [SleepTimerSettings].
28 | @ProviderFor(SleepTimerSettings)
29 | final sleepTimerSettingsProvider =
30 | NotifierProvider.internal(
31 | SleepTimerSettings.new,
32 | name: r'sleepTimerSettingsProvider',
33 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
34 | ? null
35 | : _$sleepTimerSettingsHash,
36 | dependencies: null,
37 | allTransitiveDependencies: null,
38 | );
39 |
40 | typedef _$SleepTimerSettings = Notifier;
41 | // ignore_for_file: type=lint
42 | // 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 |
--------------------------------------------------------------------------------
/lib/settings/constants.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart' show immutable;
2 |
3 | @immutable
4 | class AppMetadata {
5 | const AppMetadata._();
6 | // TODO: use the packageinfo package to get the app name
7 | static const String appName = 'Vaani';
8 |
9 | // for deeplinking
10 | static const String appScheme = 'vaani';
11 |
12 | static const version = '1.0.0';
13 | static const author = 'Dr.Blank';
14 |
15 | static Uri githubRepo = Uri.parse('https://github.com/Dr-Blank/Vaani');
16 |
17 | static get appNameLowerCase => appName.toLowerCase().replaceAll(' ', '_');
18 | }
19 |
--------------------------------------------------------------------------------
/lib/settings/metadata/metadata_provider.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'metadata_provider.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$deviceNameHash() => r'9e38adda74e70a91851a682f05228bd759356dcc';
10 |
11 | /// See also [deviceName].
12 | @ProviderFor(deviceName)
13 | final deviceNameProvider = FutureProvider.internal(
14 | deviceName,
15 | name: r'deviceNameProvider',
16 | debugGetCreateSourceHash:
17 | const bool.fromEnvironment('dart.vm.product') ? null : _$deviceNameHash,
18 | dependencies: null,
19 | allTransitiveDependencies: null,
20 | );
21 |
22 | @Deprecated('Will be removed in 3.0. Use Ref instead')
23 | // ignore: unused_element
24 | typedef DeviceNameRef = FutureProviderRef;
25 | String _$deviceModelHash() => r'922b13d9e35b5b5c5b8e96f2f2c2ae594f4f41f2';
26 |
27 | /// See also [deviceModel].
28 | @ProviderFor(deviceModel)
29 | final deviceModelProvider = FutureProvider.internal(
30 | deviceModel,
31 | name: r'deviceModelProvider',
32 | debugGetCreateSourceHash:
33 | const bool.fromEnvironment('dart.vm.product') ? null : _$deviceModelHash,
34 | dependencies: null,
35 | allTransitiveDependencies: null,
36 | );
37 |
38 | @Deprecated('Will be removed in 3.0. Use Ref instead')
39 | // ignore: unused_element
40 | typedef DeviceModelRef = FutureProviderRef;
41 | String _$deviceSdkVersionHash() => r'33178d80590808d1f4cca2be8a3b52c6f6724cac';
42 |
43 | /// See also [deviceSdkVersion].
44 | @ProviderFor(deviceSdkVersion)
45 | final deviceSdkVersionProvider = FutureProvider.internal(
46 | deviceSdkVersion,
47 | name: r'deviceSdkVersionProvider',
48 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
49 | ? null
50 | : _$deviceSdkVersionHash,
51 | dependencies: null,
52 | allTransitiveDependencies: null,
53 | );
54 |
55 | @Deprecated('Will be removed in 3.0. Use Ref instead')
56 | // ignore: unused_element
57 | typedef DeviceSdkVersionRef = FutureProviderRef;
58 | String _$deviceManufacturerHash() =>
59 | r'39250767deb8635fa7c7e18bae23576b9b863e04';
60 |
61 | /// See also [deviceManufacturer].
62 | @ProviderFor(deviceManufacturer)
63 | final deviceManufacturerProvider = FutureProvider.internal(
64 | deviceManufacturer,
65 | name: r'deviceManufacturerProvider',
66 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
67 | ? null
68 | : _$deviceManufacturerHash,
69 | dependencies: null,
70 | allTransitiveDependencies: null,
71 | );
72 |
73 | @Deprecated('Will be removed in 3.0. Use Ref instead')
74 | // ignore: unused_element
75 | typedef DeviceManufacturerRef = FutureProviderRef;
76 | // ignore_for_file: type=lint
77 | // 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
78 |
--------------------------------------------------------------------------------
/lib/settings/models/api_settings.dart:
--------------------------------------------------------------------------------
1 | // a freezed class to store the settings of the app
2 |
3 | import 'package:freezed_annotation/freezed_annotation.dart';
4 | import 'package:vaani/settings/models/audiobookshelf_server.dart';
5 | import 'package:vaani/settings/models/authenticated_user.dart';
6 |
7 | part 'api_settings.freezed.dart';
8 | part 'api_settings.g.dart';
9 |
10 | /// stores the settings for the active server and user
11 | ///
12 | /// all settings that are needed to interact with the server are stored here
13 | @freezed
14 | class ApiSettings with _$ApiSettings {
15 | const factory ApiSettings({
16 | AudiobookShelfServer? activeServer,
17 | AuthenticatedUser? activeUser,
18 | String? activeLibraryId,
19 | }) = _ApiSettings;
20 |
21 | factory ApiSettings.fromJson(Map json) =>
22 | _$ApiSettingsFromJson(json);
23 | }
24 |
--------------------------------------------------------------------------------
/lib/settings/models/api_settings.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'api_settings.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$ApiSettingsImpl _$$ApiSettingsImplFromJson(Map json) =>
10 | _$ApiSettingsImpl(
11 | activeServer: json['activeServer'] == null
12 | ? null
13 | : AudiobookShelfServer.fromJson(
14 | json['activeServer'] as Map),
15 | activeUser: json['activeUser'] == null
16 | ? null
17 | : AuthenticatedUser.fromJson(
18 | json['activeUser'] as Map),
19 | activeLibraryId: json['activeLibraryId'] as String?,
20 | );
21 |
22 | Map _$$ApiSettingsImplToJson(_$ApiSettingsImpl instance) =>
23 | {
24 | 'activeServer': instance.activeServer,
25 | 'activeUser': instance.activeUser,
26 | 'activeLibraryId': instance.activeLibraryId,
27 | };
28 |
--------------------------------------------------------------------------------
/lib/settings/models/audiobookshelf_server.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 |
3 | part 'audiobookshelf_server.freezed.dart';
4 | part 'audiobookshelf_server.g.dart';
5 |
6 | typedef AudiobookShelfUri = Uri;
7 |
8 | /// Represents a audiobookshelf server
9 | @freezed
10 | class AudiobookShelfServer with _$AudiobookShelfServer {
11 | const factory AudiobookShelfServer({
12 | required AudiobookShelfUri serverUrl,
13 | // String? serverName,
14 | }) = _AudiobookShelfServer;
15 |
16 | factory AudiobookShelfServer.fromJson(Map json) =>
17 | _$AudiobookShelfServerFromJson(json);
18 | }
19 |
--------------------------------------------------------------------------------
/lib/settings/models/audiobookshelf_server.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'audiobookshelf_server.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$AudiobookShelfServerImpl _$$AudiobookShelfServerImplFromJson(
10 | Map json) =>
11 | _$AudiobookShelfServerImpl(
12 | serverUrl: Uri.parse(json['serverUrl'] as String),
13 | );
14 |
15 | Map _$$AudiobookShelfServerImplToJson(
16 | _$AudiobookShelfServerImpl instance) =>
17 | {
18 | 'serverUrl': instance.serverUrl.toString(),
19 | };
20 |
--------------------------------------------------------------------------------
/lib/settings/models/authenticated_user.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 | import 'package:vaani/settings/models/audiobookshelf_server.dart';
3 |
4 | part 'authenticated_user.freezed.dart';
5 | part 'authenticated_user.g.dart';
6 |
7 | /// authenticated user with server and credentials
8 | @freezed
9 | class AuthenticatedUser with _$AuthenticatedUser {
10 | const factory AuthenticatedUser({
11 | required AudiobookShelfServer server,
12 | required String authToken,
13 | required String id,
14 | String? username,
15 | }) = _AuthenticatedUser;
16 |
17 | factory AuthenticatedUser.fromJson(Map json) =>
18 | _$AuthenticatedUserFromJson(json);
19 | }
20 |
--------------------------------------------------------------------------------
/lib/settings/models/authenticated_user.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'authenticated_user.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$AuthenticatedUserImpl _$$AuthenticatedUserImplFromJson(
10 | Map json) =>
11 | _$AuthenticatedUserImpl(
12 | server:
13 | AudiobookShelfServer.fromJson(json['server'] as Map),
14 | authToken: json['authToken'] as String,
15 | id: json['id'] as String,
16 | username: json['username'] as String?,
17 | );
18 |
19 | Map _$$AuthenticatedUserImplToJson(
20 | _$AuthenticatedUserImpl instance) =>
21 | {
22 | 'server': instance.server,
23 | 'authToken': instance.authToken,
24 | 'id': instance.id,
25 | 'username': instance.username,
26 | };
27 |
--------------------------------------------------------------------------------
/lib/settings/models/models.dart:
--------------------------------------------------------------------------------
1 | export 'api_settings.dart';
2 | export 'app_settings.dart';
3 | export 'audiobookshelf_server.dart';
4 | export 'authenticated_user.dart';
5 |
--------------------------------------------------------------------------------
/lib/settings/settings.dart:
--------------------------------------------------------------------------------
1 | export 'app_settings_provider.dart';
2 | export 'constants.dart';
3 |
--------------------------------------------------------------------------------
/lib/settings/view/buttons.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class OkButton extends StatelessWidget {
4 | const OkButton({
5 | super.key,
6 | this.onPressed,
7 | });
8 |
9 | final void Function()? onPressed;
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return TextButton(
14 | onPressed: onPressed,
15 | child: const Text('OK'),
16 | );
17 | }
18 | }
19 |
20 | class CancelButton extends StatelessWidget {
21 | const CancelButton({
22 | super.key,
23 | this.onPressed,
24 | });
25 |
26 | final void Function()? onPressed;
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | return TextButton(
31 | onPressed: () {
32 | onPressed?.call();
33 | Navigator.of(context).pop();
34 | },
35 | child: const Text('Cancel'),
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/settings/view/simple_settings_page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_settings_ui/flutter_settings_ui.dart';
3 | import 'package:hooks_riverpod/hooks_riverpod.dart';
4 | import 'package:vaani/features/player/view/mini_player_bottom_padding.dart';
5 |
6 | class SimpleSettingsPage extends HookConsumerWidget {
7 | const SimpleSettingsPage({
8 | super.key,
9 | this.title,
10 | this.sections,
11 | });
12 |
13 | final Widget? title;
14 | final List? sections;
15 |
16 | @override
17 | Widget build(BuildContext context, WidgetRef ref) {
18 | return Scaffold(
19 | // appBar: AppBar(
20 | // title: title,
21 | // ),
22 | // body: body,
23 | // an app bar which is bigger than the default app bar but on scroll shrinks to the default app bar with the title being animated
24 | body: CustomScrollView(
25 | slivers: [
26 | SliverAppBar(
27 | expandedHeight: 200.0,
28 | floating: false,
29 | pinned: true,
30 | flexibleSpace: FlexibleSpaceBar(
31 | title: title,
32 | // background: Theme.of(context).primaryColor,
33 | ),
34 | ),
35 | if (sections != null)
36 | SliverList(
37 | delegate: SliverChildListDelegate(
38 | [
39 | ClipRRect(
40 | borderRadius: const BorderRadius.all(Radius.circular(20)),
41 | child: SettingsList(
42 | shrinkWrap: true,
43 | physics: const NeverScrollableScrollPhysics(),
44 | sections: sections!,
45 | ),
46 | ),
47 | ],
48 | ),
49 | ),
50 | // some padding at the bottom
51 | const SliverPadding(padding: EdgeInsets.only(bottom: 20)),
52 | SliverToBoxAdapter(child: MiniPlayerBottomPadding()),
53 | ],
54 | ),
55 | );
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/settings/view/widgets/navigation_with_switch_tile.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_settings_ui/flutter_settings_ui.dart';
3 |
4 | class NavigationWithSwitchTile extends AbstractSettingsTile {
5 | const NavigationWithSwitchTile({
6 | this.leading,
7 | // this.trailing,
8 | required this.value,
9 | required this.title,
10 | this.description,
11 | this.descriptionInlineIos = false,
12 | this.onPressed,
13 | this.enabled = true,
14 | this.backgroundColor,
15 | super.key,
16 | this.onToggle,
17 | });
18 |
19 | final Widget title;
20 | final Widget? description;
21 | final Color? backgroundColor;
22 | final bool descriptionInlineIos;
23 | final bool enabled;
24 | final Widget? leading;
25 | final Function(BuildContext)? onPressed;
26 | final bool value;
27 | final Function(bool)? onToggle;
28 |
29 | @override
30 | Widget build(BuildContext context) {
31 | return SettingsTile.navigation(
32 | title: title,
33 | description: description,
34 | backgroundColor: backgroundColor,
35 | descriptionInlineIos: descriptionInlineIos,
36 | enabled: enabled,
37 | leading: leading,
38 | onPressed: onPressed,
39 | trailing: IntrinsicHeight(
40 | child: Row(
41 | children: [
42 | VerticalDivider(
43 | color: Theme.of(context).dividerColor.withValues(alpha: 0.5),
44 | indent: 8.0,
45 | endIndent: 8.0,
46 | ),
47 | Switch.adaptive(
48 | value: value,
49 | onChanged: onToggle,
50 | ),
51 | ],
52 | ),
53 | ),
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib/shared/extensions/chapter.dart:
--------------------------------------------------------------------------------
1 | import 'package:shelfsdk/audiobookshelf_api.dart';
2 |
3 | extension ChapterDuration on BookChapter {
4 | Duration get duration {
5 | // end - start
6 | return end - start;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/lib/shared/extensions/duration_format.dart:
--------------------------------------------------------------------------------
1 | extension DurationFormat on Duration {
2 | /// formats the duration using only 2 units
3 | ///
4 | /// if the duration is more than 1 hour, it will return `10h 30m`
5 | /// if the duration is less than 1 hour, it will return `30m 20s`
6 | /// if the duration is less than 1 minute, it will return `20s`
7 | String get smartBinaryFormat {
8 | final hours = inHours;
9 | final minutes = inMinutes.remainder(60);
10 | final seconds = inSeconds.remainder(60);
11 | if (hours > 0) {
12 | // skip minutes if it's 0
13 | if (minutes == 0) {
14 | return smartSingleFormat;
15 | }
16 | return '${Duration(hours: hours).smartBinaryFormat} ${Duration(minutes: minutes).smartSingleFormat}';
17 | } else if (minutes > 0) {
18 | if (seconds == 0) {
19 | return smartSingleFormat;
20 | }
21 | return '${Duration(minutes: minutes).smartSingleFormat} ${Duration(seconds: seconds).smartSingleFormat}';
22 | } else {
23 | return smartSingleFormat;
24 | }
25 | }
26 |
27 | /// formats the duration using only 1 unit
28 | /// if the duration is more than 1 hour, it will return `10h`
29 | /// if the duration is less than 1 hour, it will return `30m`
30 | /// if the duration is less than 1 minute, it will return `20s`
31 | ///
32 | /// rest of the duration will be ignored
33 | String get smartSingleFormat {
34 | if (inHours > 0) {
35 | return '${inHours}h';
36 | } else if (inMinutes > 0) {
37 | return '${inMinutes}m';
38 | } else {
39 | return '${inSeconds}s';
40 | }
41 | }
42 | }
43 |
44 | extension OnlyTime on DateTime {
45 | // in format HH:MM:ss
46 | // padding with 0
47 | String get time =>
48 | '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}:${second.toString().padLeft(2, '0')}';
49 | }
50 |
--------------------------------------------------------------------------------
/lib/shared/extensions/enum.dart:
--------------------------------------------------------------------------------
1 | extension TitleCase on Enum {
2 | String get properName {
3 | final name = toString().split('.').last;
4 | return name[0].toUpperCase() + name.substring(1);
5 | }
6 |
7 | String get titleCase {
8 | return name
9 | .replaceAllMapped(RegExp(r'([A-Z])'), (match) => ' ${match.group(0)}')
10 | .trim();
11 | }
12 |
13 | String get pascalCase {
14 | // capitalize the first letter of each word
15 | return name
16 | .replaceAllMapped(
17 | RegExp(r'([A-Z])'),
18 | (match) => ' ${match.group(0)}',
19 | )
20 | .trim()
21 | .split(' ')
22 | .map((word) => word[0].toUpperCase() + word.substring(1))
23 | .join(' ');
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/shared/extensions/inverse_lerp.dart:
--------------------------------------------------------------------------------
1 | extension InverseLerp on num {
2 | /// Returns the fraction of this value between [min] and [max].
3 | double inverseLerp(num min, num max) {
4 | return (this - min) / (max - min);
5 | }
6 | }
7 |
8 | extension Lerp on double {
9 | /// Returns the value between [min] and [max] given the fraction [t].
10 | double lerp(double min, double max) {
11 | return min + ((max - min) * this);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lib/shared/extensions/item_files.dart:
--------------------------------------------------------------------------------
1 | import 'package:shelfsdk/audiobookshelf_api.dart';
2 |
3 | extension TotalSize on LibraryItemExpanded {
4 | int get totalSize {
5 | return libraryFiles.fold(
6 | 0,
7 | (previousValue, element) => previousValue + element.metadata.size,
8 | );
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/lib/shared/extensions/model_conversions.dart:
--------------------------------------------------------------------------------
1 | import 'package:shelfsdk/audiobookshelf_api.dart';
2 |
3 | extension LibraryItemConversion on LibraryItem {
4 | LibraryItemExpanded get asExpanded => LibraryItemExpanded.fromJson(toJson());
5 |
6 | LibraryItemMinified get asMinified => LibraryItemMinified.fromJson(toJson());
7 | }
8 |
9 | extension MediaConversion on Media {
10 | Book get asBook => Book.fromJson(toJson());
11 | BookExpanded get asBookExpanded => BookExpanded.fromJson(toJson());
12 | BookMinified get asBookMinified => BookMinified.fromJson(toJson());
13 |
14 | Podcast get asPodcast => Podcast.fromJson(toJson());
15 | PodcastExpanded get asPodcastExpanded => PodcastExpanded.fromJson(toJson());
16 | PodcastMinified get asPodcastMinified => PodcastMinified.fromJson(toJson());
17 | }
18 |
19 | extension MediaMetadataConversion on MediaMetadata {
20 | BookMetadata get asBookMetadata => BookMetadata.fromJson(toJson());
21 | BookMetadataExpanded get asBookMetadataExpanded =>
22 | BookMetadataExpanded.fromJson(toJson());
23 | BookMetadataMinified get asBookMetadataMinified =>
24 | BookMetadataMinified.fromJson(toJson());
25 |
26 | BookMetadataSeriesFilter get asBookMetadataSeriesFilter =>
27 | BookMetadataSeriesFilter.fromJson(toJson());
28 | BookMetadataMinifiedSeriesFilter get asBookMetadataMinifiedSeriesFilter =>
29 | BookMetadataMinifiedSeriesFilter.fromJson(toJson());
30 |
31 | PodcastMetadata get asPodcastMetadata => PodcastMetadata.fromJson(toJson());
32 | PodcastMetadataExpanded get asPodcastMetadataExpanded =>
33 | PodcastMetadataExpanded.fromJson(toJson());
34 | }
35 |
36 | extension AuthorConversion on Author {
37 | AuthorExpanded get asExpanded => AuthorExpanded.fromJson(toJson());
38 | AuthorMinified get asMinified => AuthorMinified.fromJson(toJson());
39 | }
40 |
41 | extension ShelfConversion on Shelf {
42 | LibraryItemShelf get asLibraryItemShelf =>
43 | LibraryItemShelf.fromJson(toJson());
44 | SeriesShelf get asSeriesShelf => SeriesShelf.fromJson(toJson());
45 | AuthorShelf get asAuthorShelf => AuthorShelf.fromJson(toJson());
46 | }
47 |
48 | extension UserConversion on User {
49 | UserWithSessionAndMostRecentProgress
50 | get asUserWithSessionAndMostRecentProgress =>
51 | UserWithSessionAndMostRecentProgress.fromJson(toJson());
52 | User get asUser => User.fromJson(toJson());
53 | }
54 |
55 | extension ContentUrl on LibraryFile {
56 | Uri url(String baseUrl, String itemId, String token) {
57 | // /api/items/{itemId}/file/{ino}?{token}
58 | // return Uri.parse('$baseUrl/api/items/$itemId/file/$ino?token=$token');
59 | var baseUri = Uri.parse(baseUrl);
60 | return Uri(
61 | scheme: baseUri.scheme,
62 | host: baseUri.host,
63 | path: '/api/items/$itemId/file/$ino',
64 | queryParameters: {'token': token},
65 | );
66 | }
67 |
68 | Uri downloadUrl(String baseUrl, String itemId, String token) {
69 | // /api/items/{itemId}/file/{ino}/download?{token}
70 | // return Uri.parse(
71 | // '$baseUrl/api/items/$itemId/file/$ino/download?token=$token',
72 | // );
73 | var baseUri = Uri.parse(baseUrl);
74 | return Uri(
75 | scheme: baseUri.scheme,
76 | host: baseUri.host,
77 | path: '/api/items/$itemId/file/$ino/download',
78 | queryParameters: {'token': token},
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/lib/shared/extensions/time_of_day.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | extension ToTimeOfDay on Duration {
4 | TimeOfDay toTimeOfDay() {
5 | return TimeOfDay(
6 | hour: inHours % 24,
7 | minute: inMinutes % 60,
8 | );
9 | }
10 | }
11 |
12 | extension ToDuration on TimeOfDay {
13 | Duration toDuration() {
14 | return Duration(hours: hour, minutes: minute);
15 | }
16 | }
17 |
18 | extension TimeOfDayExtension on TimeOfDay {
19 | int compareTo(TimeOfDay other) {
20 | if (hour < other.hour) return -1;
21 | if (hour > other.hour) return 1;
22 | if (minute < other.minute) return -1;
23 | if (minute > other.minute) return 1;
24 | return 0;
25 | }
26 |
27 | bool operator <(TimeOfDay other) => compareTo(other) < 0;
28 | bool operator >(TimeOfDay other) => compareTo(other) > 0;
29 | bool operator <=(TimeOfDay other) => compareTo(other) <= 0;
30 | bool operator >=(TimeOfDay other) => compareTo(other) >= 0;
31 |
32 | bool isBefore(TimeOfDay other) => this < other;
33 | bool isAfter(TimeOfDay other) => this > other;
34 |
35 | bool isBetween(TimeOfDay start, TimeOfDay end) {
36 | // needs more logic to handle the case where start is after end
37 | //but on the other day
38 | if (start == end) {
39 | return this == start;
40 | }
41 | if (start < end) {
42 | return this >= start && this <= end;
43 | }
44 | return this >= start || this <= end;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lib/shared/hooks.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_hooks/flutter_hooks.dart';
5 |
6 | void useInterval(VoidCallback callback, Duration delay) {
7 | final savedCallback = useRef(callback);
8 | savedCallback.value = callback;
9 |
10 | useEffect(
11 | () {
12 | final timer = Timer.periodic(delay, (_) => savedCallback.value());
13 | return timer.cancel;
14 | },
15 | [delay],
16 | );
17 | }
18 |
19 | void useTimer(VoidCallback callback, Duration delay) {
20 | final savedCallback = useRef(callback);
21 | savedCallback.value = callback;
22 |
23 | useEffect(
24 | () {
25 | final timer = Timer(delay, savedCallback.value);
26 | return timer.cancel;
27 | },
28 | [delay],
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/lib/shared/utils.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:url_launcher/url_launcher.dart';
3 |
4 | Future handleLaunchUrl(Uri url) async {
5 | if (!await launchUrl(
6 | url,
7 | mode: LaunchMode.platformDefault,
8 | webOnlyWindowName: '_blank',
9 | )) {
10 | // throw Exception('Could not launch $url');
11 | debugPrint('Could not launch $url');
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/lib/shared/widgets/drawer.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:go_router/go_router.dart';
3 | import 'package:vaani/features/you/view/server_manager.dart';
4 | import 'package:vaani/router/router.dart';
5 |
6 | class MyDrawer extends StatelessWidget {
7 | const MyDrawer({
8 | super.key,
9 | });
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return Drawer(
14 | child: ListView(
15 | children: [
16 | const DrawerHeader(
17 | child: Text(
18 | 'Vaani',
19 | style: TextStyle(
20 | fontStyle: FontStyle.italic,
21 | fontSize: 30,
22 | ),
23 | ),
24 | ),
25 | ListTile(
26 | title: const Text('server Settings'),
27 | onTap: () {
28 | Navigator.of(context).push(
29 | MaterialPageRoute(
30 | builder: (context) => const ServerManagerPage(),
31 | ),
32 | );
33 | },
34 | ),
35 | ListTile(
36 | title: const Text('App Settings'),
37 | onTap: () {
38 | context.goNamed(Routes.settings.name);
39 | },
40 | ),
41 | ],
42 | ),
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/shared/widgets/not_implemented.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | void showNotImplementedToast(BuildContext context) {
4 | ScaffoldMessenger.of(context).showSnackBar(
5 | const SnackBar(
6 | content: Text("Not implemented"),
7 | showCloseIcon: true,
8 | ),
9 | );
10 | }
11 |
--------------------------------------------------------------------------------
/lib/shared/widgets/shelves/author_shelf.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:shelfsdk/audiobookshelf_api.dart';
4 | import 'package:vaani/shared/extensions/model_conversions.dart';
5 | import 'package:vaani/shared/widgets/shelves/home_shelf.dart';
6 |
7 | /// A shelf that displays Authors on the home page
8 | class AuthorHomeShelf extends HookConsumerWidget {
9 | const AuthorHomeShelf({
10 | super.key,
11 | required this.shelf,
12 | required this.title,
13 | });
14 |
15 | final String title;
16 | final AuthorShelf shelf;
17 |
18 | @override
19 | Widget build(BuildContext context, WidgetRef ref) {
20 | return SimpleHomeShelf(
21 | title: title,
22 | children: shelf.entities
23 | .map(
24 | (item) => AuthorOnShelf(item: item),
25 | )
26 | .toList(),
27 | );
28 | }
29 | }
30 |
31 | // a widget to display a item on the shelf
32 | class AuthorOnShelf extends HookConsumerWidget {
33 | const AuthorOnShelf({
34 | super.key,
35 | required this.item,
36 | });
37 |
38 | final Author item;
39 |
40 | @override
41 | Widget build(BuildContext context, WidgetRef ref) {
42 | final author = item.asMinified;
43 | // final coverImage = ref.watch(coverImageProvider(item));
44 |
45 | return Container(
46 | margin: const EdgeInsets.only(right: 10, bottom: 10),
47 | constraints: const BoxConstraints(maxWidth: 100),
48 | child: Column(
49 | children: [
50 | ClipRRect(
51 | borderRadius: BorderRadius.circular(50),
52 | child: AspectRatio(
53 | aspectRatio: 1,
54 | child: Container(
55 | constraints: const BoxConstraints(maxWidth: 50),
56 | // child: coverImage.when(
57 | // data: (image) {
58 | // return Image.memory(image, fit: BoxFit.cover);
59 | // },
60 | // loading: () {
61 | // return const Center(child: CircularProgressIndicator());
62 | // },
63 | // error: (error, stack) {
64 | // return const Icon(Icons.error);
65 | // },
66 | // ),
67 | ),
68 | ),
69 | ),
70 | Container(
71 | margin: const EdgeInsets.all(5),
72 | child: Text(
73 | author.name,
74 | maxLines: 1,
75 | overflow: TextOverflow.ellipsis,
76 | ),
77 | ),
78 | ],
79 | ),
80 | );
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/lib/shared/widgets/vaani_logo.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class VaaniLogo extends StatelessWidget {
4 | const VaaniLogo({
5 | super.key,
6 | this.size,
7 | this.duration = const Duration(milliseconds: 750),
8 | this.curve = Curves.fastOutSlowIn,
9 | });
10 |
11 | final double? size;
12 | final Duration duration;
13 | final Curve curve;
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | final IconThemeData iconTheme = IconTheme.of(context);
18 | final double? iconSize = size ?? iconTheme.size;
19 | return AnimatedContainer(
20 | width: iconSize,
21 | height: iconSize,
22 | duration: duration,
23 | curve: curve,
24 | child: Image.asset('assets/images/vaani_logo_foreground.png'),
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/theme/providers/system_theme_provider.dart:
--------------------------------------------------------------------------------
1 | import 'package:dynamic_color/dynamic_color.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter/services.dart';
4 | import 'package:hooks_riverpod/hooks_riverpod.dart';
5 | import 'package:logging/logging.dart';
6 | import 'package:material_color_utilities/material_color_utilities.dart';
7 | import 'package:riverpod_annotation/riverpod_annotation.dart';
8 |
9 | part 'system_theme_provider.g.dart';
10 |
11 | final _logger = Logger('SystemThemeProvider');
12 |
13 | /// copied from [DynamicColorBuilder]
14 | @Riverpod(keepAlive: true)
15 | FutureOr<(ColorScheme light, ColorScheme dark)?> systemTheme(
16 | Ref ref, {
17 | bool highContrast = false,
18 | }) async {
19 | _logger.fine('Generating system theme');
20 | ColorScheme? schemeLight;
21 | ColorScheme? schemeDark;
22 | // Platform messages may fail, so we use a try/catch PlatformException.
23 | try {
24 | CorePalette? corePalette = await DynamicColorPlugin.getCorePalette();
25 |
26 | if (corePalette != null) {
27 | _logger.fine('dynamic_color: Core palette detected.');
28 | schemeLight = corePalette.toColorScheme(brightness: Brightness.light);
29 | schemeDark = corePalette.toColorScheme(brightness: Brightness.dark);
30 | }
31 | } on PlatformException {
32 | _logger.warning('dynamic_color: Failed to obtain core palette.');
33 | }
34 |
35 | if (schemeLight == null || schemeDark == null) {
36 | try {
37 | final Color? accentColor = await DynamicColorPlugin.getAccentColor();
38 |
39 | if (accentColor != null) {
40 | _logger.fine('dynamic_color: Accent color detected.');
41 | schemeLight = ColorScheme.fromSeed(
42 | seedColor: accentColor,
43 | brightness: Brightness.light,
44 | );
45 | schemeDark = ColorScheme.fromSeed(
46 | seedColor: accentColor,
47 | brightness: Brightness.dark,
48 | );
49 | }
50 | } on PlatformException {
51 | _logger.warning('dynamic_color: Failed to obtain accent color.');
52 | }
53 | }
54 |
55 | if (schemeLight == null || schemeDark == null) {
56 | _logger
57 | .warning('dynamic_color: Dynamic color not detected on this device.');
58 | return null;
59 | }
60 | // set high contrast theme
61 | if (highContrast) {
62 | schemeLight = schemeLight
63 | .copyWith(
64 | surface: Colors.white,
65 | )
66 | .harmonized();
67 | schemeDark = schemeDark
68 | .copyWith(
69 | surface: Colors.black,
70 | )
71 | .harmonized();
72 | }
73 | return (schemeLight, schemeDark);
74 | }
75 |
--------------------------------------------------------------------------------
/lib/theme/providers/theme_from_cover_provider.dart:
--------------------------------------------------------------------------------
1 | import 'package:dynamic_color/dynamic_color.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_animate/flutter_animate.dart';
4 | import 'package:hooks_riverpod/hooks_riverpod.dart';
5 | import 'package:logging/logging.dart';
6 | import 'package:riverpod_annotation/riverpod_annotation.dart';
7 | import 'package:vaani/api/image_provider.dart';
8 |
9 | part 'theme_from_cover_provider.g.dart';
10 |
11 | final _logger = Logger('ThemeFromCoverProvider');
12 |
13 | @Riverpod(keepAlive: true)
14 | Future> themeFromCover(
15 | Ref ref,
16 | ImageProvider