├── .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 | Audiobookshelf Banner 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 | [Get it on Obtainium](http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/Dr-Blank/Vaani) 22 | [Get it on Google Play](https://play.google.com/store/apps/details?id=dr.blank.vaani) 23 | [Get it on IzzyOnDroid](https://apt.izzysoft.de/fdroid/index/apk/dr.blank.vaani) 24 | [Get it on GitHub](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 | [Download Linux (.deb)](https://github.com/Dr-Blank/Vaani/releases/latest/download/vaani-linux-amd64.deb) 31 | [Download Linux (AppImage)](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 | 5 | 6 | Created by potrace 1.16, written by Peter Selinger 2001-2019 7 | 8 | 9 | 11 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 35 | 36 | -------------------------------------------------------------------------------- /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 img, { 17 | Brightness brightness = Brightness.dark, 18 | bool highContrast = false, 19 | }) async { 20 | // ! add deliberate delay to simulate a long running task as it interferes with other animations 21 | await Future.delayed(500.ms); 22 | 23 | _logger.fine('Generating color scheme from cover image'); 24 | var theme = await ColorScheme.fromImageProvider( 25 | provider: img, 26 | brightness: brightness, 27 | ); 28 | // set high contrast theme 29 | if (highContrast) { 30 | theme = theme 31 | .copyWith( 32 | surface: brightness == Brightness.light ? Colors.white : Colors.black, 33 | ) 34 | .harmonized(); 35 | } 36 | return theme; 37 | // TODO isolate is not working 38 | // see https://github.com/flutter/flutter/issues/119207 39 | // use isolate to generate the color scheme 40 | // RootIsolateToken? token = RootIsolateToken.instance; 41 | // final scheme = await Isolate.run( 42 | // () async { 43 | // _logger.fine('Isolate running ${Isolate.current.debugName}'); 44 | // try { 45 | // BackgroundIsolateBinaryMessenger.ensureInitialized(token!); 46 | // WidgetsFlutterBinding.ensureInitialized(); 47 | // return await ColorScheme.fromImageProvider( 48 | // provider: img, 49 | // brightness: brightness, 50 | // ); 51 | // } catch (e) { 52 | // _logger.fine('Error in isolate: $e'); 53 | // return null; 54 | // } 55 | // }, 56 | // ); 57 | // return scheme; 58 | } 59 | 60 | @Riverpod(keepAlive: true) 61 | FutureOr themeOfLibraryItem( 62 | Ref ref, 63 | String? itemId, { 64 | Brightness brightness = Brightness.dark, 65 | bool highContrast = false, 66 | }) async { 67 | if (itemId == null) { 68 | return null; 69 | } 70 | final coverImage = await ref.watch(coverImageProvider(itemId).future); 71 | final val = await ref.watch( 72 | themeFromCoverProvider( 73 | MemoryImage(coverImage), 74 | brightness: brightness, 75 | highContrast: highContrast, 76 | ).future, 77 | ); 78 | return val; 79 | // coverImage.when( 80 | // data: (value) async { 81 | // _logger.fine('CoverImage: $value'); 82 | // final val = ref.watch(themeFromCoverProvider(MemoryImage(value))); 83 | // _logger.fine('ColorScheme generated: $val'); 84 | // ref.invalidateSelf(); 85 | // return val; 86 | // }, 87 | // loading: () => null, 88 | // error: (error, stackTrace) => null, 89 | // ); 90 | // return null; 91 | } 92 | -------------------------------------------------------------------------------- /lib/theme/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | // brand color rgb(49, 27, 146) rgb(96, 76, 236) 4 | const brandColor = Color(0xFF311B92); 5 | const brandColorLight = Color(0xFF604CEC); 6 | 7 | final brandLightColorScheme = ColorScheme.fromSeed( 8 | seedColor: brandColor, 9 | brightness: Brightness.light, 10 | ); 11 | 12 | final brandDarkColorScheme = ColorScheme.fromSeed( 13 | seedColor: brandColor, 14 | brightness: Brightness.dark, 15 | ); 16 | -------------------------------------------------------------------------------- /linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | void fl_register_plugins(FlPluginRegistry* registry) { 15 | g_autoptr(FlPluginRegistrar) dynamic_color_registrar = 16 | fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); 17 | dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); 18 | g_autoptr(FlPluginRegistrar) isar_flutter_libs_registrar = 19 | fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin"); 20 | isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar); 21 | g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = 22 | fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); 23 | media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); 24 | g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = 25 | fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); 26 | url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); 27 | } 28 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | dynamic_color 7 | isar_flutter_libs 8 | media_kit_libs_linux 9 | url_launcher_linux 10 | ) 11 | 12 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 13 | ) 14 | 15 | set(PLUGIN_BUNDLED_LIBRARIES) 16 | 17 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 18 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 19 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 20 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 21 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 22 | endforeach(plugin) 23 | 24 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 25 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 26 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 27 | endforeach(ffi_plugin) 28 | -------------------------------------------------------------------------------- /linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /linux/packaging/appimage/make_config.yaml: -------------------------------------------------------------------------------- 1 | display_name: Vaani 2 | package_name: vaani 3 | 4 | maintainer: 5 | name: Dr.Blank 6 | email: drblankdev@gmail.com 7 | 8 | priority: optional 9 | 10 | section: x11 11 | 12 | installed_size: 75700 13 | 14 | essential: false 15 | 16 | icon: assets/icon/logo.png 17 | 18 | postuninstall_scripts: 19 | - echo "Sorry to see you go." 20 | 21 | keywords: 22 | - Audiobook 23 | - Audiobook Player 24 | - Audiobookshelf 25 | 26 | generic_name: Audiobook Player 27 | 28 | categories: 29 | - AudioVideo 30 | - Audio 31 | - Player 32 | 33 | startup_notify: true 34 | # TODO: Review and update fields for AppImage specifics (e.g., icon, metadata). 35 | -------------------------------------------------------------------------------- /linux/packaging/deb/make_config.yaml: -------------------------------------------------------------------------------- 1 | display_name: Vaani 2 | package_name: vaani 3 | 4 | maintainer: 5 | name: Dr.Blank 6 | email: drblankdev@gmail.com 7 | 8 | priority: optional 9 | 10 | section: x11 11 | 12 | installed_size: 75700 13 | 14 | essential: false 15 | 16 | icon: assets/icon/logo.png 17 | 18 | description: 19 | short: Beautiful, Fast and Functional Audiobook Player for your Audiobookshelf server. 20 | long: | 21 | Vaani is a client for your (self-hosted) Audiobookshelf server. 22 | 23 | Features: 24 | - Functional Player: Speed Control, Sleep Timer, Shake to Control Player 25 | - Save data with Offline listening and caching 26 | - Material Design 27 | - Extensive Settings to customize every tiny detail 28 | 29 | Note: you need an Audiobookshelf server setup for this app to work. 30 | Please see https://www.audiobookshelf.org/ on how to setup one if not already. 31 | 32 | postuninstall_scripts: 33 | - echo "Sorry to see you go." 34 | 35 | keywords: 36 | - Audiobook 37 | - Audiobook Player 38 | - Audiobookshelf 39 | 40 | generic_name: Audiobook Player 41 | 42 | categories: 43 | - AudioVideo 44 | - Audio 45 | - Player 46 | 47 | startup_notify: true 48 | 49 | # https://github.com/llfbandit/app_links/blob/051f53fa6039cbfaef0fcde73df20fef9e248cab/doc/README_linux.md 50 | supported_mime_type: 51 | - x-scheme-handler/vaani 52 | -------------------------------------------------------------------------------- /privacy-policy.md: -------------------------------------------------------------------------------- 1 | **Privacy Policy** 2 | 3 | This privacy policy applies to the Vaani app (hereby referred to as "Application") for mobile devices that was created by Dr. Blank (hereby referred to as "Service Provider") as an Open Source service. This service is intended for use "AS IS". 4 | 5 | **Information Collection and Use** 6 | 7 | The Application does not collect any information by itself. It is a client for a self-hosted server, meaning that users are responsible for setting up their own servers to use the Application. Any information collected will be by the user's server and not by the Service Provider. 8 | 9 | **Changes** 10 | 11 | This Privacy Policy may be updated from time to time for any reason. The Service Provider will notify you of any changes to the Privacy Policy by updating this page with the new Privacy Policy. You are advised to consult this Privacy Policy regularly for any changes, as continued use is deemed approval of all changes. 12 | 13 | This privacy policy is effective as of 2024-10-01 14 | 15 | **Your Consent** 16 | 17 | By using the Application, you are consenting to the processing of your information as set forth in this Privacy Policy now and as amended by us. 18 | 19 | **Contact Us** 20 | 21 | If you have any questions regarding privacy while using the Application, or have questions about the practices, please contact the Service Provider via email at drblankdev@gmail.com. 22 | 23 | * * * 24 | 25 | This privacy policy page was generated by [App Privacy Policy Generator](https://app-privacy-policy-generator.nisrulz.com/) 26 | -------------------------------------------------------------------------------- /test/features/logging/providers/logs_provider_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:logging/logging.dart'; 3 | import 'package:logging_appenders/logging_appenders.dart'; 4 | import 'package:vaani/features/logging/providers/logs_provider.dart'; 5 | 6 | void main() { 7 | test( 8 | 'Should parse log line', 9 | () async { 10 | final formatter = DefaultLogRecordFormatter(); 11 | final logRecord = LogRecord( 12 | Level.INFO, 13 | 'getting location for name: "logs"', 14 | 'GoRouter', 15 | ); 16 | final expected = parseLogLine( 17 | formatter.format(logRecord), 18 | ); 19 | expect(logRecord.message, expected.message); 20 | expect(logRecord.level, expected.level); 21 | expect(logRecord.loggerName, expected.loggerName); 22 | }, 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/web/favicon.png -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/web/icons/Icon-512.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | vaani 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vaani", 3 | "short_name": "vaani", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | void RegisterPlugins(flutter::PluginRegistry* registry) { 17 | DynamicColorPluginCApiRegisterWithRegistrar( 18 | registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); 19 | IsarFlutterLibsPluginRegisterWithRegistrar( 20 | registry->GetRegistrarForPlugin("IsarFlutterLibsPlugin")); 21 | MediaKitLibsWindowsAudioPluginCApiRegisterWithRegistrar( 22 | registry->GetRegistrarForPlugin("MediaKitLibsWindowsAudioPluginCApi")); 23 | PermissionHandlerWindowsPluginRegisterWithRegistrar( 24 | registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); 25 | SharePlusWindowsPluginCApiRegisterWithRegistrar( 26 | registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); 27 | UrlLauncherWindowsRegisterWithRegistrar( 28 | registry->GetRegistrarForPlugin("UrlLauncherWindows")); 29 | } 30 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | dynamic_color 7 | isar_flutter_libs 8 | media_kit_libs_windows_audio 9 | permission_handler_windows 10 | share_plus 11 | url_launcher_windows 12 | ) 13 | 14 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 15 | ) 16 | 17 | set(PLUGIN_BUNDLED_LIBRARIES) 18 | 19 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 20 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 21 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 23 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 24 | endforeach(plugin) 25 | 26 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 27 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 28 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 29 | endforeach(ffi_plugin) 30 | -------------------------------------------------------------------------------- /windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"vaani", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dr-Blank/Vaani/07aea41c6e048109d87dff5d1b3b93088ddd64b3/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | --------------------------------------------------------------------------------