├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
└── workflows
│ └── dart.yml
├── .gitignore
├── .metadata
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── TRANSLATION.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── res
│ │ │ └── xml
│ │ │ └── network_security_config.xml
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── ic_launcher-playstore.png
│ │ ├── ic_launcher_monochrome-playstore.png
│ │ ├── kotlin
│ │ └── uk
│ │ │ └── me
│ │ │ └── amugofjava
│ │ │ └── anytime_podcast_player
│ │ │ └── MainActivity.kt
│ │ └── res
│ │ ├── drawable-anydpi-v24
│ │ ├── ic_action_fastforward.xml
│ │ ├── ic_action_pause_circle_outline.xml
│ │ ├── ic_action_play_circle_outline.xml
│ │ └── ic_action_rewind.xml
│ │ ├── drawable-hdpi
│ │ ├── ic_action_fastforward.png
│ │ ├── ic_action_pause.png
│ │ ├── ic_action_pause_circle_outline.png
│ │ ├── ic_action_play_arrow.png
│ │ ├── ic_action_play_circle_outline.png
│ │ ├── ic_action_rewind.png
│ │ ├── ic_action_stop.png
│ │ ├── ic_stat_anytime_logo_notification.png
│ │ └── ic_stat_name.png
│ │ ├── drawable-mdpi
│ │ ├── ic_action_fastforward.png
│ │ ├── ic_action_pause.png
│ │ ├── ic_action_pause_circle_outline.png
│ │ ├── ic_action_play_arrow.png
│ │ ├── ic_action_play_circle_outline.png
│ │ ├── ic_action_rewind.png
│ │ ├── ic_action_stop.png
│ │ ├── ic_stat_anytime_logo_notification.png
│ │ └── ic_stat_name.png
│ │ ├── drawable-xhdpi
│ │ ├── ic_action_fastforward.png
│ │ ├── ic_action_pause.png
│ │ ├── ic_action_pause_circle_outline.png
│ │ ├── ic_action_play_arrow.png
│ │ ├── ic_action_play_circle_outline.png
│ │ ├── ic_action_rewind.png
│ │ ├── ic_action_stop.png
│ │ ├── ic_stat_anytime_logo_notification.png
│ │ └── ic_stat_name.png
│ │ ├── drawable-xxhdpi
│ │ ├── ic_action_fastforward.png
│ │ ├── ic_action_pause.png
│ │ ├── ic_action_pause_circle_outline.png
│ │ ├── ic_action_play_arrow.png
│ │ ├── ic_action_play_circle_outline.png
│ │ ├── ic_action_rewind.png
│ │ ├── ic_action_stop.png
│ │ ├── ic_stat_anytime_logo_notification.png
│ │ └── ic_stat_name.png
│ │ ├── drawable-xxxhdpi
│ │ ├── ic_action_pause.png
│ │ ├── ic_action_play_arrow.png
│ │ ├── ic_action_stop.png
│ │ ├── ic_stat_anytime_logo_notification.png
│ │ └── ic_stat_name.png
│ │ ├── drawable
│ │ ├── ic_action_fastforward_30.xml
│ │ ├── ic_action_rewind_10.xml
│ │ ├── ic_launcher_background.xml
│ │ └── launch_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ ├── ic_launcher_monochrome.xml
│ │ ├── ic_launcher_monochrome_round.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ ├── ic_launcher_monochrome.webp
│ │ ├── ic_launcher_monochrome_foreground.webp
│ │ ├── ic_launcher_monochrome_round.webp
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ ├── ic_launcher_monochrome.webp
│ │ ├── ic_launcher_monochrome_foreground.webp
│ │ ├── ic_launcher_monochrome_round.webp
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ ├── ic_launcher_monochrome.webp
│ │ ├── ic_launcher_monochrome_foreground.webp
│ │ ├── ic_launcher_monochrome_round.webp
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ ├── ic_launcher_monochrome.webp
│ │ ├── ic_launcher_monochrome_foreground.webp
│ │ ├── ic_launcher_monochrome_round.webp
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ ├── ic_launcher_foreground.png
│ │ ├── ic_launcher_monochrome.webp
│ │ ├── ic_launcher_monochrome_foreground.webp
│ │ ├── ic_launcher_monochrome_round.webp
│ │ └── ic_launcher_round.png
│ │ ├── values
│ │ ├── color.xml
│ │ ├── ic_launcher_monochrome_background.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── network_security_config.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
├── settings.gradle
└── settings_aar.gradle
├── assets
├── ca
│ ├── globalsign-gcc-r6-alphassl-ca-2023.pem
│ └── lets-encrypt-r3.pem
├── fonts
│ ├── Montserrat-Bold.otf
│ ├── Montserrat-Medium.otf
│ └── Montserrat-Regular.otf
└── images
│ ├── anytime-logo-ios.png
│ ├── anytime-logo-s.png
│ ├── anytime-logo.png
│ └── anytime-placeholder-logo.png
├── docs
├── amazon-appstore-badge-english-black.png
├── apple.png
├── apple.svg
├── architecture.png
├── architecture_small.png
├── google-play-badge.png
├── kofi.jpg
├── logo.png
├── logo_small.png
├── screenshot1.png
├── screenshot1b.png
├── screenshot2.png
├── screenshot2b.png
├── screenshot3.png
├── screenshot3b.png
└── screenshot4b.png
├── ios
├── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Podfile
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
└── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── Icon-App-1024x1024@1x.png
│ │ ├── Icon-App-20x20@1x.png
│ │ ├── Icon-App-20x20@2x.png
│ │ ├── Icon-App-20x20@3x.png
│ │ ├── Icon-App-29x29@1x.png
│ │ ├── Icon-App-29x29@2x.png
│ │ ├── Icon-App-29x29@3x.png
│ │ ├── Icon-App-40x40@1x.png
│ │ ├── Icon-App-40x40@2x.png
│ │ ├── Icon-App-40x40@3x.png
│ │ ├── Icon-App-60x60@2x.png
│ │ ├── Icon-App-60x60@3x.png
│ │ ├── Icon-App-76x76@1x.png
│ │ ├── Icon-App-76x76@2x.png
│ │ └── Icon-App-83.5x83.5@2x.png
│ └── LaunchImage.imageset
│ │ ├── Contents.json
│ │ ├── LaunchImage.png
│ │ ├── LaunchImage@2x.png
│ │ ├── LaunchImage@3x.png
│ │ └── README.md
│ ├── Base.lproj
│ ├── LaunchScreen.storyboard
│ └── Main.storyboard
│ ├── Info.plist
│ └── Runner-Bridging-Header.h
├── lib
├── api
│ └── podcast
│ │ ├── mobile_podcast_api.dart
│ │ └── podcast_api.dart
├── bloc
│ ├── bloc.dart
│ ├── discovery
│ │ ├── discovery_bloc.dart
│ │ └── discovery_state_event.dart
│ ├── podcast
│ │ ├── audio_bloc.dart
│ │ ├── episode_bloc.dart
│ │ ├── opml_bloc.dart
│ │ ├── podcast_bloc.dart
│ │ └── queue_bloc.dart
│ ├── search
│ │ ├── search_bloc.dart
│ │ └── search_state_event.dart
│ ├── settings
│ │ └── settings_bloc.dart
│ └── ui
│ │ └── pager_bloc.dart
├── core
│ ├── annotations.dart
│ ├── environment.dart
│ ├── extensions.dart
│ └── utils.dart
├── entities
│ ├── app_settings.dart
│ ├── chapter.dart
│ ├── downloadable.dart
│ ├── episode.dart
│ ├── feed.dart
│ ├── funding.dart
│ ├── persistable.dart
│ ├── person.dart
│ ├── podcast.dart
│ ├── queue.dart
│ ├── search_providers.dart
│ ├── sleep.dart
│ └── transcript.dart
├── l10n
│ ├── L.dart
│ ├── intl_de.arb
│ ├── intl_en.arb
│ ├── intl_es.arb
│ ├── intl_it.arb
│ ├── intl_messages.arb
│ ├── intl_nl.arb
│ ├── messages_all.dart
│ ├── messages_all_locales.dart
│ ├── messages_de.dart
│ ├── messages_en.dart
│ ├── messages_es.dart
│ ├── messages_it.dart
│ ├── messages_messages.dart
│ └── messages_nl.dart
├── main.dart
├── navigation
│ └── navigation_route_observer.dart
├── repository
│ ├── repository.dart
│ └── sembast
│ │ ├── sembast_database_service.dart
│ │ └── sembast_repository.dart
├── services
│ ├── audio
│ │ ├── audio_player_service.dart
│ │ └── default_audio_player_service.dart
│ ├── download
│ │ ├── download_manager.dart
│ │ ├── download_service.dart
│ │ ├── mobile_download_manager.dart
│ │ └── mobile_download_service.dart
│ ├── podcast
│ │ ├── mobile_opml_service.dart
│ │ ├── mobile_podcast_service.dart
│ │ ├── opml_service.dart
│ │ └── podcast_service.dart
│ └── settings
│ │ ├── mobile_settings_service.dart
│ │ └── settings_service.dart
├── state
│ ├── bloc_state.dart
│ ├── episode_state.dart
│ ├── opml_state.dart
│ ├── persistent_state.dart
│ ├── queue_event_state.dart
│ └── transcript_state_event.dart
└── ui
│ ├── anytime_podcast_app.dart
│ ├── library
│ ├── discovery.dart
│ ├── discovery_results.dart
│ ├── downloads.dart
│ ├── episodes.dart
│ ├── library.dart
│ ├── opml.dart
│ ├── opml_export.dart
│ └── opml_import.dart
│ ├── podcast
│ ├── chapter_selector.dart
│ ├── dot_decoration.dart
│ ├── episode_details.dart
│ ├── funding_menu.dart
│ ├── mini_player.dart
│ ├── now_playing.dart
│ ├── now_playing_floating_player.dart
│ ├── now_playing_options.dart
│ ├── person_avatar.dart
│ ├── playback_error_listener.dart
│ ├── player_position_controls.dart
│ ├── player_transport_controls.dart
│ ├── podcast_context_menu.dart
│ ├── podcast_details.dart
│ ├── podcast_episode_list.dart
│ ├── show_notes.dart
│ ├── transcript_view.dart
│ ├── transport_controls.dart
│ └── up_next_view.dart
│ ├── search
│ ├── search.dart
│ ├── search_bar.dart
│ └── search_results.dart
│ ├── settings
│ ├── episode_refresh.dart
│ ├── search_provider.dart
│ ├── settings.dart
│ ├── settings_section_label.dart
│ └── theme_select.dart
│ ├── themes.dart
│ └── widgets
│ ├── action_text.dart
│ ├── decorated_icon_button.dart
│ ├── delayed_progress_indicator.dart
│ ├── download_button.dart
│ ├── draggable_episode_tile.dart
│ ├── episode_filter_selector.dart
│ ├── episode_sort_selector.dart
│ ├── episode_tile.dart
│ ├── layout_selector.dart
│ ├── placeholder_builder.dart
│ ├── platform_back_button.dart
│ ├── platform_progress_indicator.dart
│ ├── play_pause_button.dart
│ ├── podcast_grid_tile.dart
│ ├── podcast_html.dart
│ ├── podcast_image.dart
│ ├── podcast_list.dart
│ ├── podcast_tile.dart
│ ├── search_slide_route.dart
│ ├── sleep_selector.dart
│ ├── slider_handle.dart
│ ├── speed_selector.dart
│ ├── sync_spinner.dart
│ └── tile_image.dart
├── metadata
└── en-US
│ ├── full_description.txt
│ ├── images
│ ├── featureGraphic.png
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── 1.png
│ │ ├── 2.png
│ │ ├── 3.png
│ │ ├── 4.png
│ │ └── 5.png
│ ├── short_description.txt
│ └── title.txt
├── pubspec.lock
├── pubspec.yaml
├── test
└── unit
│ ├── mocks
│ ├── mock_path_provider.dart
│ ├── mock_podcast_api.dart
│ └── mock_settings_service.dart
│ ├── opml
│ └── opml_service_test.dart
│ ├── persistence
│ └── sembast_test.dart
│ └── services
│ └── settings_test.dart
├── test_resources
├── opml_import_test1.opml
└── podcast1.rss
├── useful_commands.txt
└── xl10n.yaml
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: amugofjava
2 | ko_fi: amugofjava
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Smartphone (please complete the following information):**
27 | - Device: [e.g. Pixel 4]
28 | - OS: [e.g. Android 9]
29 |
30 | **Additional context**
31 | Add any other context about the problem here.
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/workflows/dart.yml:
--------------------------------------------------------------------------------
1 | name: Flutter CI
2 |
3 | on: [ push ]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v3
12 | - uses: actions/setup-java@v2
13 | with:
14 | distribution: 'zulu'
15 | java-version: '17'
16 | - uses: subosito/flutter-action@v2
17 | with:
18 | channel: stable
19 | flutter-version-file: pubspec.yaml
20 | - run: flutter pub get
21 | - run: flutter test
22 | - run: flutter build apk --debug
23 | - run: flutter build appbundle --debug
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.lock
4 | *.log
5 | *.pyc
6 | *.swp
7 | .DS_Store
8 | .atom/
9 | .buildlog/
10 | .history
11 | .svn/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # Visual Studio Code related
20 | .vscode/
21 |
22 | # Flutter/Dart/Pub related
23 | **/doc/api/
24 | .dart_tool/
25 | .flutter-plugins
26 | .flutter-plugins-dependencies
27 | .packages
28 | .pub-cache/
29 | .pub/
30 | build/
31 | coverage/
32 |
33 | # Android related
34 | **/android/**/gradle-wrapper.jar
35 | **/android/key.properties
36 | **/android/.gradle
37 | **/android/captures/
38 | **/android/gradlew
39 | **/android/gradlew.bat
40 | **/android/local.properties
41 | **/android/**/GeneratedPluginRegistrant.java
42 | **/.cxx
43 |
44 | # iOS/XCode related
45 | **/ios/**/*.mode1v3
46 | **/ios/**/*.mode2v3
47 | **/ios/**/*.moved-aside
48 | **/ios/**/*.pbxuser
49 | **/ios/**/*.perspectivev3
50 | **/ios/**/*sync/
51 | **/ios/**/.sconsign.dblite
52 | **/ios/**/.tags*
53 | **/ios/**/.vagrant/
54 | **/ios/**/DerivedData/
55 | **/ios/**/Icon?
56 | **/ios/**/Pods/
57 | **/ios/**/.symlinks/
58 | **/ios/**/profile
59 | **/ios/**/xcuserdata
60 | **/ios/.generated/
61 | **/ios/Flutter/App.framework
62 | **/ios/Flutter/Flutter.framework
63 | **/ios/Flutter/Generated.xcconfig
64 | **/ios/Flutter/app.flx
65 | **/ios/Flutter/app.zip
66 | **/ios/Flutter/flutter_assets/
67 | **/ios/ServiceDefinitions.json
68 | **/ios/Runner/GeneratedPluginRegistrant.*
69 |
70 | # Exceptions to above rules.
71 | !**/ios/**/default.mode1v3
72 | !**/ios/**/default.mode2v3
73 | !**/ios/**/default.pbxuser
74 | !**/ios/**/default.perspectivev3
75 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
76 |
--------------------------------------------------------------------------------
/.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: 985ccb6d14c6ce5ce74823a4d366df2438eac44f
8 | channel: beta
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Welcome
2 |
3 | If you're here because you want to contribute to Anytime - thank you! There are many ways in which you can
4 | help this project.
5 |
6 | ## Testing
7 |
8 | This is by far one of the biggest ways you can support the project. Please try out Anytime and
9 | report back any issues or bugs you find. If you have suggestions for improvements or new
10 | features, please report them too.
11 |
12 | ## Reporting a bug or feature request
13 |
14 | Report a bug or feature request
15 | by [opening a new issue](https://github.com/amugofjava/anytime_podcast_player/issues) -
16 | it's that easy!
17 |
18 | ## Contributing code
19 | If you would like to work on a feature that has been requested or fix a bug that has been reported
20 | via an issue, add
21 | a comment to it so that other people know that you are working on it. If you would like to work on
22 | a feature that has
23 | not been reported, please first discuss the change you wish to make either
24 | via
25 | an [issue](https://github.com/amugofjava/anytime_podcast_player/issues)
26 | or [email](mailto:hello@anytimeplayer.app).
27 |
28 | Once you are ready to code:
29 |
30 | - Fork the repo and create a branch from `master`.
31 | - If you've added code that should be tested, add tests and ensure the test suite passes.
32 | - If the code you are contributing is non-trivial, please ensure your code is commented. This not
33 | only helps when reviewing pull requests, but also helps other developers who may be new to the
34 | code.
35 | - Make sure your code lints.
36 | - If your code contains labels and strings, they will need translating. Check out [TRANSLATION.md](TRANSLATION.md) for
37 | more information on how Anytime handles multiple languages.
38 | - If your code alters the UI, ensure the changes are accessible and work with both Android TalkBack and iOS VoiceOver. If
39 | you are unable to test on both Android & iOS, please reach out to me via a comment and I can help with the testing.
40 | - Format your code with `dartfmt --line-length 120`. Note that the project uses 120 chars and not
41 | the default 80. Most IDEs will do this for you on save but, if not, you may need to do this
42 | manually.
43 | - Squash your commits. If you have made several commits, but they are all part of the same logical
44 | change, please squash them down to a single commit; this both helps the pull request
45 | approval process and keeps the history cleaner. If you've never squashed commits before there is
46 | a good
47 | article [here](https://medium.com/@slamflipstrom/a-beginners-guide-to-squashing-commits-with-git-rebase-8185cf6e62ec),
48 | or you can reach out to [me](mailto:anytime@amugofjava.me.uk) if you need some help.
49 | - Issue a pull request.
50 |
51 | ## Translating Anytime
52 |
53 | If you would like to translate the app into another language or improve an existing translation,
54 | please
55 | take a look at [TRANSLATION.md](TRANSLATION.md) for further details.
56 |
57 | ## License
58 |
59 | By contributing, you agree that your contributions will be under the BSD-style license defined in
60 | the [LICENSE](LICENSE) file.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020 Ben Hills and the project contributors. All rights reserved.
2 |
3 | Redistribution and use in source and binary forms, with or without modification,
4 | are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this list of
7 | conditions and the following disclaimer.
8 |
9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of
10 | conditions and the following disclaimer in the documentation and/or other materials
11 | provided with the distribution.
12 |
13 | 3. Neither the name of the copyright holder nor the names of its contributors may be used
14 | to endorse or promote products derived from this software without specific prior written
15 | permission.
16 |
17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
18 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
19 | AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
20 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
24 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25 | POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/TRANSLATION.md:
--------------------------------------------------------------------------------
1 | # Translation
2 |
3 | Anytime uses the [intl](https://pub.dev/packages/intl)
4 | and [intl_translation](https://pub.dev/packages/intl_translation)
5 | packages for handling string translations, and a custom `LocalizationsDelegate` to allow Anytime to
6 | use custom
7 | language resources when required.
8 |
9 | All the language resources can be found under the `lib/l10n` directory.
10 |
11 | ### Translation Process
12 |
13 | The translation process requires a few steps: create the language reference in code, generate the
14 | ARB
15 | file(s), translate the new strings into the desired locales then generate the language bindings.
16 |
17 | #### New locales
18 |
19 | If you are translating Anytime into a new language, add the locale to the `supportedLocales` as part
20 | of
21 | the `MaterialApp` construction within the main `AnytimePodcastApp` class.
22 |
23 | Add the language code to the `isSupported` methods in `L.dart`.
24 |
25 | #### Define messages
26 |
27 | Check the `L.dart` file to see if it already contains the string you are looking for.
28 |
29 | If not, create the new message in `L.dart`. Use an existing message as a template ensuring the
30 | message name makes it
31 | clear what/where the message is used.
32 |
33 | #### Generate ARB files
34 |
35 | Open a terminal or command line window and, from the project case, run the following command:
36 |
37 | `dart run intl_translation:extract_to_arb --output-dir=lib/l10n lib/l10n/L.dart`
38 |
39 | This will add the new messages to the master `intl_messages.arb` file.
40 |
41 | #### Translate ARB files.
42 |
43 | Copy the new entries in the `intl_messages.arb` file to the `intl_en.arb` file and the locale file
44 | you are translating
45 | to. Translate the new messages in the locale ARB file.
46 |
47 | Once translation is complete, run the following command to generate the language bindings:
48 |
49 | `dart run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/l10n/L.dart lib/l10n/intl_*.arb`
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:flutter_lints/flutter.yaml
2 |
3 | analyzer:
4 | exclude: [ build/**, lib/**/*.g.dart, lib/l10n/** ]
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020-2022 Ben Hills. All rights reserved.
3 | * Use of this source code is governed by a BSD-style license that can be
4 | * found in the LICENSE file.
5 | */
6 |
7 | plugins {
8 | id "com.android.application"
9 | id "kotlin-android"
10 | id "dev.flutter.flutter-gradle-plugin"
11 | }
12 |
13 | def localProperties = new Properties()
14 | def localPropertiesFile = rootProject.file('local.properties')
15 | if (localPropertiesFile.exists()) {
16 | localPropertiesFile.withReader('UTF-8') { reader ->
17 | localProperties.load(reader)
18 | }
19 | }
20 |
21 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
22 | if (flutterVersionCode == null) {
23 | flutterVersionCode = '1'
24 | }
25 |
26 | def flutterVersionName = localProperties.getProperty('flutter.versionName')
27 | if (flutterVersionName == null) {
28 | flutterVersionName = '1.0'
29 | }
30 |
31 | def keystoreProperties = new Properties()
32 | def keystorePropertiesFile = rootProject.file('key.properties')
33 | if (keystorePropertiesFile.exists()) {
34 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
35 | }
36 |
37 | android {
38 | compileSdk 35
39 | ndkVersion "27.0.12077973"
40 |
41 | compileOptions {
42 | sourceCompatibility JavaVersion.VERSION_11
43 | targetCompatibility JavaVersion.VERSION_11
44 | }
45 |
46 | dependenciesInfo {
47 | includeInApk = false
48 | includeInBundle = true
49 | }
50 |
51 | kotlinOptions {
52 | jvmTarget = JavaVersion.VERSION_11
53 | }
54 |
55 | packagingOptions {
56 | jniLibs {
57 | useLegacyPackaging = true
58 | }
59 | }
60 |
61 | defaultConfig {
62 | applicationId "uk.me.amugofjava.anytime"
63 | minSdkVersion 22
64 | targetSdkVersion 35
65 | versionCode flutterVersionCode.toInteger()
66 | versionName flutterVersionName
67 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
68 | }
69 |
70 | signingConfigs {
71 | release {
72 | keyAlias keystoreProperties['keyAlias']
73 | keyPassword keystoreProperties['keyPassword']
74 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
75 | storePassword keystoreProperties['storePassword']
76 | }
77 | }
78 |
79 | buildTypes {
80 | debug {
81 | signingConfig signingConfigs.debug
82 | shrinkResources false
83 | }
84 |
85 | release {
86 | signingConfig signingConfigs.release
87 | shrinkResources false
88 | proguardFiles getDefaultProguardFile('proguard-android.txt')
89 | }
90 | }
91 |
92 | namespace 'uk.me.amugofjava.anytime'
93 |
94 | lint {
95 | abortOnError false
96 | disable 'InvalidPackage'
97 | }
98 | }
99 |
100 | flutter {
101 | source '../..'
102 | }
103 |
104 | dependencies {
105 | implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
106 | testImplementation 'junit:junit:4.13.2'
107 | androidTestImplementation 'com.android.support.test:runner:1.0.2'
108 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
109 | }
110 |
--------------------------------------------------------------------------------
/android/app/src/debug/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/android/app/src/main/ic_launcher_monochrome-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/ic_launcher_monochrome-playstore.png
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/uk/me/amugofjava/anytime_podcast_player/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package uk.me.amugofjava.anytime_podcast_player
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity() {
6 | }
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-anydpi-v24/ic_action_fastforward.xml:
--------------------------------------------------------------------------------
1 |
7 |
9 |
12 |
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-anydpi-v24/ic_action_pause_circle_outline.xml:
--------------------------------------------------------------------------------
1 |
7 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-anydpi-v24/ic_action_play_circle_outline.xml:
--------------------------------------------------------------------------------
1 |
7 |
9 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-anydpi-v24/ic_action_rewind.xml:
--------------------------------------------------------------------------------
1 |
7 |
9 |
12 |
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_action_fastforward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-hdpi/ic_action_fastforward.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_action_pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-hdpi/ic_action_pause.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_action_pause_circle_outline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-hdpi/ic_action_pause_circle_outline.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_action_play_arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-hdpi/ic_action_play_arrow.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_action_play_circle_outline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-hdpi/ic_action_play_circle_outline.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_action_rewind.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-hdpi/ic_action_rewind.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_action_stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-hdpi/ic_action_stop.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_stat_anytime_logo_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-hdpi/ic_stat_anytime_logo_notification.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/ic_stat_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-hdpi/ic_stat_name.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_action_fastforward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-mdpi/ic_action_fastforward.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_action_pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-mdpi/ic_action_pause.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_action_pause_circle_outline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-mdpi/ic_action_pause_circle_outline.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_action_play_arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-mdpi/ic_action_play_arrow.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_action_play_circle_outline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-mdpi/ic_action_play_circle_outline.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_action_rewind.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-mdpi/ic_action_rewind.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_action_stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-mdpi/ic_action_stop.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_stat_anytime_logo_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-mdpi/ic_stat_anytime_logo_notification.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/ic_stat_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-mdpi/ic_stat_name.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_action_fastforward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xhdpi/ic_action_fastforward.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_action_pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xhdpi/ic_action_pause.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_action_pause_circle_outline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xhdpi/ic_action_pause_circle_outline.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_action_play_arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xhdpi/ic_action_play_arrow.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_action_play_circle_outline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xhdpi/ic_action_play_circle_outline.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_action_rewind.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xhdpi/ic_action_rewind.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_action_stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xhdpi/ic_action_stop.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_stat_anytime_logo_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xhdpi/ic_stat_anytime_logo_notification.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/ic_stat_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xhdpi/ic_stat_name.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_action_fastforward.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxhdpi/ic_action_fastforward.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_action_pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxhdpi/ic_action_pause.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_action_pause_circle_outline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxhdpi/ic_action_pause_circle_outline.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_action_play_arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxhdpi/ic_action_play_arrow.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_action_play_circle_outline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxhdpi/ic_action_play_circle_outline.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_action_rewind.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxhdpi/ic_action_rewind.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_action_stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxhdpi/ic_action_stop.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_stat_anytime_logo_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxhdpi/ic_stat_anytime_logo_notification.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/ic_stat_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxhdpi/ic_stat_name.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_action_pause.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxxhdpi/ic_action_pause.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_action_play_arrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxxhdpi/ic_action_play_arrow.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_action_stop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxxhdpi/ic_action_stop.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_stat_anytime_logo_notification.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxxhdpi/ic_stat_anytime_logo_notification.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/drawable-xxxhdpi/ic_stat_name.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_action_fastforward_30.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/ic_action_rewind_10.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | -
15 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_monochrome.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_monochrome_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_foreground.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome_round.webp
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/color.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | #FF9800
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/ic_launcher_monochrome_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3DDC84
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
22 |
23 |
--------------------------------------------------------------------------------
/android/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020-2022 Ben Hills. All rights reserved.
3 | * Use of this source code is governed by a BSD-style license that can be
4 | * found in the LICENSE file.
5 | */
6 |
7 | allprojects {
8 | repositories {
9 | google()
10 | mavenCentral()
11 | }
12 | }
13 |
14 | rootProject.buildDir = '../build'
15 | subprojects {
16 | project.buildDir = "${rootProject.buildDir}/${project.name}"
17 | }
18 | subprojects {
19 | project.evaluationDependsOn(':app')
20 | }
21 |
22 | tasks.register("clean", Delete) {
23 | delete rootProject.buildDir
24 | }
25 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2020-2022 Ben Hills. All rights reserved.
3 | # Use of this source code is governed by a BSD-style license that can be
4 | # found in the LICENSE file.
5 | #
6 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
7 | android.useAndroidX=true
8 | android.enableJetifier=true
9 | android.enableR8=true
10 | # Below is deprecated have replaced with packagingOptions declaration.
11 | # android.bundle.enableUncompressedNativeLibs=false
12 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright 2020-2022 Ben Hills. All rights reserved.
3 | # Use of this source code is governed by a BSD-style license that can be
4 | # found in the LICENSE file.
5 | #
6 | #Fri Oct 18 15:14:44 BST 2019
7 | distributionBase=GRADLE_USER_HOME
8 | distributionPath=wrapper/dists
9 | zipStoreBase=GRADLE_USER_HOME
10 | zipStorePath=wrapper/dists
11 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
12 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020-2022 Ben Hills. All rights reserved.
3 | * Use of this source code is governed by a BSD-style license that can be
4 | * found in the LICENSE file.
5 | */
6 |
7 | pluginManagement {
8 | def flutterSdkPath = {
9 | def properties = new Properties()
10 | file("local.properties").withInputStream { properties.load(it) }
11 | def flutterSdkPath = properties.getProperty("flutter.sdk")
12 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
13 | return flutterSdkPath
14 | }()
15 |
16 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
17 |
18 | repositories {
19 | google()
20 | mavenCentral()
21 | gradlePluginPortal()
22 | }
23 | }
24 |
25 | plugins {
26 | id "dev.flutter.flutter-plugin-loader" version "1.0.0"
27 | id "com.android.application" version "8.7.3" apply false
28 | id "org.jetbrains.kotlin.android" version "2.1.0" apply false
29 | }
30 |
31 | include ":app"
32 |
--------------------------------------------------------------------------------
/android/settings_aar.gradle:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2020-2022 Ben Hills. All rights reserved.
3 | * Use of this source code is governed by a BSD-style license that can be
4 | * found in the LICENSE file.
5 | */
6 |
7 | include ':app'
8 |
--------------------------------------------------------------------------------
/assets/fonts/Montserrat-Bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/assets/fonts/Montserrat-Bold.otf
--------------------------------------------------------------------------------
/assets/fonts/Montserrat-Medium.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/assets/fonts/Montserrat-Medium.otf
--------------------------------------------------------------------------------
/assets/fonts/Montserrat-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/assets/fonts/Montserrat-Regular.otf
--------------------------------------------------------------------------------
/assets/images/anytime-logo-ios.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/assets/images/anytime-logo-ios.png
--------------------------------------------------------------------------------
/assets/images/anytime-logo-s.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/assets/images/anytime-logo-s.png
--------------------------------------------------------------------------------
/assets/images/anytime-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/assets/images/anytime-logo.png
--------------------------------------------------------------------------------
/assets/images/anytime-placeholder-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/assets/images/anytime-placeholder-logo.png
--------------------------------------------------------------------------------
/docs/amazon-appstore-badge-english-black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/amazon-appstore-badge-english-black.png
--------------------------------------------------------------------------------
/docs/apple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/apple.png
--------------------------------------------------------------------------------
/docs/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/architecture.png
--------------------------------------------------------------------------------
/docs/architecture_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/architecture_small.png
--------------------------------------------------------------------------------
/docs/google-play-badge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/google-play-badge.png
--------------------------------------------------------------------------------
/docs/kofi.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/kofi.jpg
--------------------------------------------------------------------------------
/docs/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/logo.png
--------------------------------------------------------------------------------
/docs/logo_small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/logo_small.png
--------------------------------------------------------------------------------
/docs/screenshot1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/screenshot1.png
--------------------------------------------------------------------------------
/docs/screenshot1b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/screenshot1b.png
--------------------------------------------------------------------------------
/docs/screenshot2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/screenshot2.png
--------------------------------------------------------------------------------
/docs/screenshot2b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/screenshot2b.png
--------------------------------------------------------------------------------
/docs/screenshot3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/screenshot3.png
--------------------------------------------------------------------------------
/docs/screenshot3b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/screenshot3b.png
--------------------------------------------------------------------------------
/docs/screenshot4b.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/docs/screenshot4b.png
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/app.flx
22 | Flutter/app.zip
23 | Flutter/flutter_assets/
24 | Flutter/flutter_export_environment.sh
25 | ServiceDefinitions.json
26 | Runner/GeneratedPluginRegistrant.*
27 |
28 | # Exceptions to above rules.
29 | !default.mode1v3
30 | !default.mode2v3
31 | !default.pbxuser
32 | !default.perspectivev3
33 |
--------------------------------------------------------------------------------
/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 12.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | platform :ios, '12.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def flutter_root
14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
15 | unless File.exist?(generated_xcode_build_settings_path)
16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
17 | end
18 |
19 | File.foreach(generated_xcode_build_settings_path) do |line|
20 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
21 | return matches[1].strip if matches
22 | end
23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
24 | end
25 |
26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
27 |
28 | flutter_ios_podfile_setup
29 |
30 | target 'Runner' do
31 | use_frameworks!
32 | use_modular_headers!
33 |
34 | # Temporary workaround for DK issue
35 | #pod 'DKImagePickerController', '4.3.4'
36 |
37 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
38 | end
39 |
40 | post_install do |installer|
41 | installer.pods_project.targets.each do |target|
42 | flutter_additional_ios_build_settings(target)
43 |
44 | target.build_configurations.each do |config|
45 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
46 | '$(inherited)',
47 |
48 | ## Audio session - disable microphone
49 | 'AUDIO_SESSION_MICROPHONE=0',
50 |
51 | ## dart: PermissionGroup.calendar
52 | 'PERMISSION_EVENTS=0',
53 |
54 | ## dart: PermissionGroup.reminders
55 | 'PERMISSION_REMINDERS=0',
56 |
57 | ## dart: PermissionGroup.contacts
58 | 'PERMISSION_CONTACTS=0',
59 |
60 | ## dart: PermissionGroup.camera
61 | 'PERMISSION_CAMERA=0',
62 |
63 | ## dart: PermissionGroup.microphone
64 | 'PERMISSION_MICROPHONE=0',
65 |
66 | ## dart: PermissionGroup.speech
67 | 'PERMISSION_SPEECH_RECOGNIZER=0',
68 |
69 | ## dart: PermissionGroup.photos
70 | 'PERMISSION_PHOTOS=0',
71 |
72 | ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
73 | 'PERMISSION_LOCATION=0',
74 |
75 | ## dart: PermissionGroup.notification
76 | 'PERMISSION_NOTIFICATIONS=0',
77 |
78 | ## dart: PermissionGroup.mediaLibrary
79 | 'PERMISSION_MEDIA_LIBRARY=0',
80 |
81 | ## dart: PermissionGroup.sensors
82 | 'PERMISSION_SENSORS=0',
83 |
84 | ## dart: PermissionGroup.bluetooth
85 | 'PERMISSION_BLUETOOTH=0',
86 |
87 | ## dart: PermissionGroup.appTrackingTransparency
88 | 'PERMISSION_APP_TRACKING_TRANSPARENCY=0',
89 |
90 | ## dart: PermissionGroup.criticalAlerts
91 | 'PERMISSION_CRITICAL_ALERTS=0'
92 | ]
93 | end
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
41 |
42 |
52 |
54 |
60 |
61 |
62 |
63 |
69 |
71 |
77 |
78 |
79 |
80 |
82 |
83 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 | import flutter_downloader
4 |
5 | @main
6 | @objc class AppDelegate: FlutterAppDelegate {
7 | override func application(
8 | _ application: UIApplication,
9 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
10 | ) -> Bool {
11 | GeneratedPluginRegistrant.register(with: self)
12 | FlutterDownloaderPlugin.setPluginRegistrantCallback(registerPlugins)
13 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
14 | }
15 | }
16 |
17 | private func registerPlugins(registry: FlutterPluginRegistry) {
18 | if (!registry.hasPlugin("FlutterDownloaderPlugin")) {
19 | FlutterDownloaderPlugin.register(with: registry.registrar(forPlugin: "FlutterDownloaderPlugin")!)
20 | }
21 | }
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/lib/api/podcast/podcast_api.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/entities/transcript.dart';
6 | import 'package:podcast_search/podcast_search.dart' as pslib;
7 |
8 | /// A simple wrapper class that interacts with the search API via
9 | /// the podcast_search package.
10 | ///
11 | /// TODO: Make this more generic so it's not tied to podcast_search
12 | abstract class PodcastApi {
13 | /// Search for podcasts matching the search criteria. Returns a
14 | /// [SearchResult] instance.
15 | Future search(
16 | String term, {
17 | String? country,
18 | String? attribute,
19 | int? limit,
20 | String? language,
21 | int version = 0,
22 | bool explicit = false,
23 | String? searchProvider,
24 | });
25 |
26 | /// Request the top podcast charts from iTunes, and at most [size] records.
27 | Future charts({
28 | int? size,
29 | String? searchProvider,
30 | String? genre,
31 | String? countryCode,
32 | String? languageCode,
33 | });
34 |
35 | List genres(
36 | String searchProvider,
37 | );
38 |
39 | /// URL representing the RSS feed for a podcast.
40 | Future loadFeed(String url);
41 |
42 | Future feedLastUpdated(String url);
43 |
44 | /// Load episode chapters via JSON file.
45 | Future loadChapters(String url);
46 |
47 | /// Load episode transcript via SRT or JSON file.
48 | Future loadTranscript(TranscriptUrl transcriptUrl);
49 |
50 | /// Allow adding of custom certificates. Required as default context
51 | /// does not apply when running in separate Isolate.
52 | void addClientAuthorityBytes(List certificateAuthorityBytes);
53 | }
54 |
--------------------------------------------------------------------------------
/lib/bloc/bloc.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/bloc/podcast/audio_bloc.dart';
6 | import 'package:rxdart/rxdart.dart';
7 |
8 | /// Base class for all BLoCs to give each a hook into the mobile
9 | /// lifecycle state of paused, resume or detached.
10 | abstract class Bloc {
11 | /// Handle lifecycle events
12 | final PublishSubject _lifecycleSubject = PublishSubject(sync: true);
13 |
14 | Bloc() {
15 | _init();
16 | }
17 |
18 | void _init() {
19 | _lifecycleSubject.listen((state) async {
20 | if (state == LifecycleState.resume) {
21 | resume();
22 | } else if (state == LifecycleState.pause) {
23 | pause();
24 | } else if (state == LifecycleState.detach) {
25 | detach();
26 | }
27 | });
28 | }
29 |
30 | void dispose() {
31 | if (_lifecycleSubject.hasListener) {
32 | _lifecycleSubject.close();
33 | }
34 | }
35 |
36 | void resume() {}
37 |
38 | void pause() {}
39 |
40 | void detach() {}
41 |
42 | void Function(LifecycleState) get transitionLifecycleState => _lifecycleSubject.sink.add;
43 | }
44 |
--------------------------------------------------------------------------------
/lib/bloc/discovery/discovery_state_event.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | /// Events
6 | class DiscoveryEvent {}
7 |
8 | class DiscoveryChartEvent extends DiscoveryEvent {
9 | final int count;
10 | String genre;
11 | String countryCode;
12 | String languageCode;
13 |
14 | DiscoveryChartEvent({
15 | required this.count,
16 | this.genre = '',
17 | this.countryCode = '',
18 | this.languageCode = '',
19 | });
20 | }
21 |
22 | /// States
23 | class DiscoveryState {}
24 |
25 | class DiscoveryLoadingState extends DiscoveryState {}
26 |
27 | class DiscoveryPopulatedState extends DiscoveryState {
28 | final String? genre;
29 | final int index;
30 | final T? results;
31 |
32 | DiscoveryPopulatedState({
33 | this.genre,
34 | this.index = 0,
35 | this.results,
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/lib/bloc/podcast/opml_bloc.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/bloc/bloc.dart';
6 | import 'package:anytime/services/podcast/opml_service.dart';
7 | import 'package:anytime/state/opml_state.dart';
8 | import 'package:logging/logging.dart';
9 | import 'package:rxdart/rxdart.dart';
10 |
11 | /// OPML (Outline Processor Markup Language) is an XML format for outlines, which is used in Podcast
12 | /// apps for transferring podcast subscriptions/follows from/to other podcast apps.
13 | ///
14 | /// Anytime supports both import and export of OPML.
15 | class OPMLBloc extends Bloc {
16 | final log = Logger('OPMLBloc');
17 |
18 | final PublishSubject _opmlEvent = PublishSubject();
19 | final PublishSubject _opmlState = PublishSubject();
20 | final OPMLService opmlService;
21 |
22 | OPMLBloc({required this.opmlService}) {
23 | _listenOpmlEvents();
24 | }
25 |
26 | void _listenOpmlEvents() {
27 | _opmlEvent.listen((event) {
28 | if (event is OPMLImportEvent) {
29 | if (event.file != null) {
30 | opmlService.loadOPMLFile(event.file!).listen((state) {
31 | _opmlState.add(state);
32 | });
33 | }
34 | } else if (event is OPMLExportEvent) {
35 | opmlService.saveOPMLFile().listen((state) {
36 | _opmlState.add(state);
37 | });
38 | } else if (event is OPMLCancelEvent) {
39 | opmlService.cancel();
40 | }
41 | });
42 | }
43 |
44 | void Function(OPMLEvent) get opmlEvent => _opmlEvent.add;
45 |
46 | Stream get opmlState => _opmlState.stream;
47 | }
48 |
--------------------------------------------------------------------------------
/lib/bloc/podcast/queue_bloc.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/bloc/bloc.dart';
6 | import 'package:anytime/services/audio/audio_player_service.dart';
7 | import 'package:anytime/services/podcast/podcast_service.dart';
8 | import 'package:anytime/state/queue_event_state.dart';
9 | import 'package:rxdart/rxdart.dart';
10 |
11 | /// Handles interaction with the Queue via an [AudioPlayerService].
12 | class QueueBloc extends Bloc {
13 | final AudioPlayerService audioPlayerService;
14 | final PodcastService podcastService;
15 | final PublishSubject _queueEvent = PublishSubject();
16 |
17 | QueueBloc({
18 | required this.audioPlayerService,
19 | required this.podcastService,
20 | }) {
21 | _handleQueueEvents();
22 | }
23 |
24 | void _handleQueueEvents() {
25 | _queueEvent.listen((QueueEvent event) async {
26 | if (event is QueueAddEvent) {
27 | var e = event.episode;
28 | if (e != null) {
29 | await audioPlayerService.addUpNextEpisode(e);
30 | }
31 | } else if (event is QueueRemoveEvent) {
32 | var e = event.episode;
33 | if (e != null) {
34 | await audioPlayerService.removeUpNextEpisode(e);
35 | }
36 | } else if (event is QueueMoveEvent) {
37 | var e = event.episode;
38 | if (e != null) {
39 | await audioPlayerService.moveUpNextEpisode(e, event.oldIndex, event.newIndex);
40 | }
41 | } else if (event is QueueClearEvent) {
42 | await audioPlayerService.clearUpNext();
43 | }
44 | });
45 |
46 | audioPlayerService.queueState!.debounceTime(const Duration(seconds: 2)).listen((event) {
47 | podcastService.saveQueue(event.queue).then((value) {
48 | /// Queue saved.
49 | });
50 | });
51 | }
52 |
53 | Function(QueueEvent) get queueEvent => _queueEvent.sink.add;
54 |
55 | Stream? get queue => audioPlayerService.queueState;
56 |
57 | @override
58 | void dispose() {
59 | _queueEvent.close();
60 |
61 | super.dispose();
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/bloc/search/search_state_event.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | /// Events
6 | class SearchEvent {}
7 |
8 | class SearchTermEvent extends SearchEvent {
9 | final String term;
10 |
11 | SearchTermEvent(this.term);
12 | }
13 |
14 | class SearchChartsEvent extends SearchEvent {}
15 |
16 | class SearchClearEvent extends SearchEvent {}
17 |
18 | /// States
19 | class SearchState {}
20 |
21 | class SearchLoadingState extends SearchState {}
22 |
--------------------------------------------------------------------------------
/lib/bloc/ui/pager_bloc.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:rxdart/rxdart.dart';
6 |
7 | /// This BLoC provides a sink and stream to set and listen for the current
8 | /// page/tab on a bottom navigation bar.
9 | class PagerBloc {
10 | final BehaviorSubject page = BehaviorSubject.seeded(0);
11 |
12 | Function(int) get changePage => page.add;
13 |
14 | Stream get currentPage => page.stream;
15 |
16 | void dispose() {
17 | page.close();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/core/annotations.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | /// Simple marker to indicate a field is transient and is not intended to be persisted
6 | class Transient {
7 | const Transient();
8 | }
9 |
--------------------------------------------------------------------------------
/lib/core/environment.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:io';
6 |
7 | /// The key required when searching via PodcastIndex.org.
8 | const podcastIndexKey = String.fromEnvironment('PINDEX_KEY', defaultValue: '');
9 |
10 | /// The secret required when searching via PodcastIndex.org.
11 | const podcastIndexSecret = String.fromEnvironment('PINDEX_SECRET', defaultValue: '');
12 |
13 | /// Allows a user to override the default user agent string.
14 | const userAgentAppString = String.fromEnvironment('USER_AGENT', defaultValue: '');
15 |
16 | /// Link to a feedback form. This will be shown in the main overflow menu if set
17 | const feedbackUrl = String.fromEnvironment('FEEDBACK_URL', defaultValue: '');
18 |
19 | /// This class stores version information for Anytime, including project version and
20 | /// build number. This is then used for user agent strings when interacting with
21 | /// APIs and RSS feeds.
22 | ///
23 | /// The user agent string can be overridden by passing in the USER_AGENT variable
24 | /// using dart-define.
25 | class Environment {
26 | static const _applicationName = 'Anytime';
27 | static const _applicationUrl = 'https://github.com/amugofjava/anytime_podcast_player';
28 | static const _projectVersion = '1.3.13';
29 | static const _build = '190';
30 |
31 | static var _agentString = userAgentAppString;
32 |
33 | static String userAgent() {
34 | if (_agentString.isEmpty) {
35 | var platform = '${Platform.operatingSystem} ${Platform.operatingSystemVersion}'.trim();
36 |
37 | _agentString = '$_applicationName/$_projectVersion b$_build (phone;$platform) $_applicationUrl';
38 | }
39 |
40 | return _agentString;
41 | }
42 |
43 | static String get projectVersion => '$_projectVersion b$_build';
44 | }
45 |
--------------------------------------------------------------------------------
/lib/core/extensions.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:math';
6 |
7 | extension IterableExtensions on Iterable {
8 | Iterable> chunk(int size) sync* {
9 | if (length <= 0) {
10 | yield [];
11 | return;
12 | }
13 |
14 | var skip = 0;
15 |
16 | while (skip < length) {
17 | final chunk = this.skip(skip).take(size);
18 |
19 | yield chunk.toList(growable: false);
20 |
21 | skip += size;
22 |
23 | if (chunk.length < size) {
24 | return;
25 | }
26 | }
27 | }
28 | }
29 |
30 | extension ExtString on String? {
31 | String get forceHttps {
32 | if (this != null) {
33 | final url = Uri.tryParse(this!);
34 |
35 | if (url == null || !url.isScheme('http')) return this!;
36 |
37 | return url.replace(scheme: 'https').toString();
38 | }
39 |
40 | return this ?? '';
41 | }
42 | }
43 |
44 | extension ExtDouble on double {
45 | double get toTenth {
46 | var mod = pow(10.0, 1).toDouble();
47 | return ((this * mod).round().toDouble() / mod);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/entities/chapter.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/core/extensions.dart';
6 |
7 | /// A class that represents an individual chapter within an [Episode].
8 | ///
9 | /// Chapters may, or may not, exist for an episode.
10 | ///
11 | /// Part of the [podcast namespace](https://github.com/Podcastindex-org/podcast-namespace)
12 | class Chapter {
13 | /// Title of this chapter.
14 | final String title;
15 |
16 | /// URL for the chapter image if one is available.
17 | final String? imageUrl;
18 |
19 | /// URL of an external link for this chapter if available.
20 | final String? url;
21 |
22 | /// Table of contents flag. If this is false the chapter should be treated as
23 | /// meta data only and not be displayed.
24 | final bool toc;
25 |
26 | /// The start time of the chapter in seconds.
27 | final double startTime;
28 |
29 | /// The optional end time of the chapter in seconds.
30 | final double? endTime;
31 |
32 | Chapter({
33 | required this.title,
34 | required String? imageUrl,
35 | required this.startTime,
36 | String? url,
37 | this.toc = true,
38 | this.endTime,
39 | }) : imageUrl = imageUrl?.forceHttps,
40 | url = url?.forceHttps;
41 |
42 | Map toMap() {
43 | return {
44 | 'title': title,
45 | 'imageUrl': imageUrl,
46 | 'url': url,
47 | 'toc': toc ? 'true' : 'false',
48 | 'startTime': startTime.toString(),
49 | 'endTime': endTime.toString(),
50 | };
51 | }
52 |
53 | static Chapter fromMap(Map chapter) {
54 | return Chapter(
55 | title: chapter['title'] as String,
56 | imageUrl: chapter['imageUrl'] as String?,
57 | url: chapter['url'] as String?,
58 | toc: chapter['toc'] == 'false' ? false : true,
59 | startTime: double.parse(chapter['startTime'] as String),
60 | endTime: double.parse(chapter['endTime'] as String),
61 | );
62 | }
63 |
64 | @override
65 | bool operator ==(Object other) =>
66 | identical(this, other) ||
67 | other is Chapter && runtimeType == other.runtimeType && title == other.title && startTime == other.startTime;
68 |
69 | @override
70 | int get hashCode => title.hashCode ^ startTime.hashCode;
71 | }
72 |
--------------------------------------------------------------------------------
/lib/entities/downloadable.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | enum DownloadState { none, queued, downloading, failed, cancelled, paused, downloaded }
6 |
7 | /// A Downloadble is an object that holds information about a podcast episode
8 | /// and its download status.
9 | ///
10 | /// Downloadables can be used to determine if a download has been successful and
11 | /// if an episode can be played from the filesystem.
12 | class Downloadable {
13 | /// Database ID
14 | int? id;
15 |
16 | /// Unique identifier for the download
17 | final String guid;
18 |
19 | /// URL of the file to download
20 | final String url;
21 |
22 | /// Destination directory
23 | String directory;
24 |
25 | /// Name of file
26 | String filename;
27 |
28 | /// Current task ID for the download
29 | String taskId;
30 |
31 | /// Current state of the download
32 | DownloadState state;
33 |
34 | /// Percentage of MP3 downloaded
35 | int? percentage;
36 |
37 | Downloadable({
38 | required this.guid,
39 | required this.url,
40 | required this.directory,
41 | required this.filename,
42 | required this.taskId,
43 | required this.state,
44 | this.percentage,
45 | });
46 |
47 | Map toMap() {
48 | return {
49 | 'guid': guid,
50 | 'url': url,
51 | 'filename': filename,
52 | 'directory': directory,
53 | 'taskId': taskId,
54 | 'state': state.index,
55 | 'percentage': percentage.toString(),
56 | };
57 | }
58 |
59 | static Downloadable fromMap(Map downloadable) {
60 | return Downloadable(
61 | guid: downloadable['guid'] as String,
62 | url: downloadable['url'] as String,
63 | directory: downloadable['directory'] as String,
64 | filename: downloadable['filename'] as String,
65 | taskId: downloadable['taskId'] as String,
66 | state: _determineState(downloadable['state'] as int?),
67 | percentage: int.parse(downloadable['percentage'] as String),
68 | );
69 | }
70 |
71 | static DownloadState _determineState(int? index) {
72 | switch (index) {
73 | case 0:
74 | return DownloadState.none;
75 | case 1:
76 | return DownloadState.queued;
77 | case 2:
78 | return DownloadState.downloading;
79 | case 3:
80 | return DownloadState.failed;
81 | case 4:
82 | return DownloadState.cancelled;
83 | case 5:
84 | return DownloadState.paused;
85 | case 6:
86 | return DownloadState.downloaded;
87 | }
88 |
89 | return DownloadState.none;
90 | }
91 |
92 | @override
93 | bool operator ==(Object other) =>
94 | identical(this, other) || other is Downloadable && runtimeType == other.runtimeType && guid == other.guid;
95 |
96 | @override
97 | int get hashCode => guid.hashCode;
98 | }
99 |
--------------------------------------------------------------------------------
/lib/entities/feed.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/entities/podcast.dart';
6 |
7 | /// This class is used when loading a [Podcast] feed.
8 | ///
9 | /// The key information is contained within the [Podcast] instance, but as the
10 | /// iTunes API also returns large and thumbnail artwork within its search results
11 | /// this class also contains properties to represent those.
12 | class Feed {
13 | /// The podcast to load
14 | final Podcast podcast;
15 |
16 | /// The full-size artwork for the podcast.
17 | String? imageUrl;
18 |
19 | /// The thumbnail artwork for the podcast,
20 | String? thumbImageUrl;
21 |
22 | /// If true the podcast is loaded regardless of if it's currently cached or on disk.
23 | bool forceFetch;
24 |
25 | /// If true, will also perform an additional background refresh.
26 | bool backgroundFetch;
27 |
28 | /// If true any error can be ignored.
29 | bool errorSilently;
30 |
31 | Feed({
32 | required this.podcast,
33 | this.imageUrl,
34 | this.thumbImageUrl,
35 | this.forceFetch = false,
36 | this.backgroundFetch = false,
37 | this.errorSilently = false,
38 | });
39 | }
40 |
--------------------------------------------------------------------------------
/lib/entities/funding.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/core/extensions.dart';
6 |
7 | /// part of a [Podcast].
8 | ///
9 | /// Part of the [podcast namespace](https://github.com/Podcastindex-org/podcast-namespace)
10 | class Funding {
11 | /// The URL to the funding/donation/information page.
12 | final String url;
13 |
14 | /// The label for the link which will be presented to the user.
15 | final String value;
16 |
17 | Funding({
18 | required String url,
19 | required this.value,
20 | }) : url = url.forceHttps;
21 |
22 | Map toMap() {
23 | return {
24 | 'url': url,
25 | 'value': value,
26 | };
27 | }
28 |
29 | static Funding fromMap(Map chapter) {
30 | return Funding(
31 | url: chapter['url'] as String,
32 | value: chapter['value'] as String,
33 | );
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/entities/persistable.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | enum LastState { none, completed, stopped, paused }
6 |
7 | /// This class is used to persist information about the currently playing episode to disk.
8 | ///
9 | /// This allows the background audio service to persist state (whilst the UI is not visible)
10 | /// and for the episode play and position details to be restored when the UI becomes visible
11 | /// again - either when bringing it to the foreground or upon next start.
12 | class Persistable {
13 | /// The Podcast GUID.
14 | String pguid;
15 |
16 | /// The episode ID (provided by the DB layer).
17 | int episodeId;
18 |
19 | /// The current position in seconds;
20 | int position;
21 |
22 | /// The current playback state.
23 | LastState state;
24 |
25 | /// Date & time episode was last updated.
26 | DateTime? lastUpdated;
27 |
28 | Persistable({
29 | required this.pguid,
30 | required this.episodeId,
31 | required this.position,
32 | required this.state,
33 | this.lastUpdated,
34 | });
35 |
36 | Persistable.empty()
37 | : pguid = '',
38 | episodeId = 0,
39 | position = 0,
40 | state = LastState.none,
41 | lastUpdated = DateTime.now();
42 |
43 | Map toMap() {
44 | return {
45 | 'pguid': pguid,
46 | 'episodeId': episodeId,
47 | 'position': position,
48 | 'state': state.toString(),
49 | 'lastUpdated': lastUpdated == null ? DateTime.now().millisecondsSinceEpoch : lastUpdated!.millisecondsSinceEpoch,
50 | };
51 | }
52 |
53 | static Persistable fromMap(Map persistable) {
54 | var stateString = persistable['state'] as String?;
55 | var state = LastState.none;
56 |
57 | if (stateString != null) {
58 | switch (stateString) {
59 | case 'LastState.completed':
60 | state = LastState.completed;
61 | break;
62 | case 'LastState.stopped':
63 | state = LastState.stopped;
64 | break;
65 | case 'LastState.paused':
66 | state = LastState.paused;
67 | break;
68 | }
69 | }
70 |
71 | var lastUpdated = persistable['lastUpdated'] as int?;
72 |
73 | return Persistable(
74 | pguid: persistable['pguid'] as String,
75 | episodeId: persistable['episodeId'] as int,
76 | position: persistable['position'] as int,
77 | state: state,
78 | lastUpdated: lastUpdated == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(lastUpdated),
79 | );
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/lib/entities/person.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/core/extensions.dart';
6 |
7 | /// This class represents a person of interest to the podcast.
8 | ///
9 | /// It is primarily intended to identify people like hosts, co-hosts and guests.
10 | class Person {
11 | final String name;
12 | final String role;
13 | final String group;
14 | final String? image;
15 | final String? link;
16 |
17 | Person({
18 | required this.name,
19 | this.role = '',
20 | this.group = '',
21 | String? image = '',
22 | String? link = '',
23 | }) : image = image?.forceHttps,
24 | link = link?.forceHttps;
25 |
26 | Map toMap() {
27 | return {
28 | 'name': name,
29 | 'role': role,
30 | 'group': group,
31 | 'image': image,
32 | 'link': link,
33 | };
34 | }
35 |
36 | static Person fromMap(Map chapter) {
37 | return Person(
38 | name: chapter['name'] as String? ?? '',
39 | role: chapter['role'] as String? ?? '',
40 | group: chapter['group'] as String? ?? '',
41 | image: chapter['image'] as String? ?? '',
42 | link: chapter['link'] as String? ?? '',
43 | );
44 | }
45 |
46 | @override
47 | bool operator ==(Object other) =>
48 | identical(this, other) ||
49 | other is Person &&
50 | runtimeType == other.runtimeType &&
51 | name == other.name &&
52 | role == other.role &&
53 | group == other.group &&
54 | image == other.image &&
55 | link == other.link;
56 |
57 | @override
58 | int get hashCode => name.hashCode ^ role.hashCode ^ group.hashCode ^ image.hashCode ^ link.hashCode;
59 |
60 | @override
61 | String toString() {
62 | return 'Person{name: $name, role: $role, group: $group, image: $image, link: $link}';
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/entities/queue.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | /// The current persistable queue.
6 | class Queue {
7 | List guids = [];
8 |
9 | Queue({
10 | required this.guids,
11 | });
12 |
13 | Map toMap() {
14 | return {
15 | 'q': guids,
16 | };
17 | }
18 |
19 | static Queue fromMap(int key, Map guids) {
20 | var g = guids['q'] as List?;
21 | var result = [];
22 |
23 | if (g != null) {
24 | result = g.map((dynamic e) => e.toString()).toList();
25 | }
26 |
27 | return Queue(
28 | guids: result,
29 | );
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/lib/entities/search_providers.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | /// Anytime can support multiple search providers.
6 | ///
7 | /// This class represents a provider.
8 | class SearchProvider {
9 | final String key;
10 | final String name;
11 |
12 | SearchProvider({
13 | required this.key,
14 | required this.name,
15 | });
16 | }
17 |
--------------------------------------------------------------------------------
/lib/entities/sleep.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | enum SleepType {
6 | none,
7 | time,
8 | episode,
9 | }
10 |
11 | final class Sleep {
12 | final SleepType type;
13 | final Duration duration;
14 | late DateTime endTime;
15 |
16 | Sleep({
17 | required this.type,
18 | this.duration = const Duration(milliseconds: 0),
19 | }) {
20 | endTime = DateTime.now().add(duration);
21 | }
22 |
23 | @override
24 | bool operator ==(Object other) =>
25 | identical(this, other) ||
26 | other is Sleep && runtimeType == other.runtimeType && type == other.type && duration == other.duration;
27 |
28 | @override
29 | int get hashCode => type.hashCode ^ duration.hashCode;
30 |
31 | Duration get timeRemaining => endTime.difference(DateTime.now());
32 | }
33 |
--------------------------------------------------------------------------------
/lib/l10n/messages_all.dart:
--------------------------------------------------------------------------------
1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
2 | // This is a library that looks up messages for specific locales by
3 | // delegating to the appropriate library.
4 | // @dart=2.12
5 | export 'messages_all_locales.dart'
6 | show initializeMessages;
7 |
8 |
--------------------------------------------------------------------------------
/lib/l10n/messages_all_locales.dart:
--------------------------------------------------------------------------------
1 | // DO NOT EDIT. This is code generated via package:intl/generate_localized.dart
2 | // This is a library that looks up messages for specific locales by
3 | // delegating to the appropriate library.
4 | // @dart=2.12
5 | // Ignore issues from commonly used lints in this file.
6 | // ignore_for_file:implementation_imports, file_names
7 | // ignore_for_file:unnecessary_brace_in_string_interps, directives_ordering
8 | // ignore_for_file:argument_type_not_assignable, invalid_assignment
9 | // ignore_for_file:prefer_single_quotes, prefer_generic_function_type_aliases
10 | // ignore_for_file:comment_references
11 |
12 | import 'package:intl/intl.dart';
13 | import 'package:intl/message_lookup_by_library.dart';
14 | import 'package:intl/src/intl_helpers.dart';
15 |
16 | import 'messages_de.dart' as messages_de;
17 | import 'messages_en.dart' as messages_en;
18 | import 'messages_es.dart' as messages_es;
19 | import 'messages_it.dart' as messages_it;
20 | import 'messages_messages.dart' as messages_messages;
21 | import 'messages_nl.dart' as messages_nl;
22 |
23 | typedef Future LibraryLoader();
24 | Map _deferredLibraries = {
25 | 'de': () => Future.value(null),
26 | 'en': () => Future.value(null),
27 | 'es': () => Future.value(null),
28 | 'it': () => Future.value(null),
29 | 'messages': () => Future.value(null),
30 | 'nl': () => Future.value(null),
31 | };
32 |
33 | MessageLookupByLibrary? _findExact(String localeName) {
34 | switch (localeName) {
35 | case 'de':
36 | return messages_de.messages;
37 | case 'en':
38 | return messages_en.messages;
39 | case 'es':
40 | return messages_es.messages;
41 | case 'it':
42 | return messages_it.messages;
43 | case 'messages':
44 | return messages_messages.messages;
45 | case 'nl':
46 | return messages_nl.messages;
47 | default:
48 | return null;
49 | }
50 | }
51 |
52 | /// User programs should call this before using [localeName] for messages.
53 | Future initializeMessages(String? localeName) async {
54 | var availableLocale = Intl.verifiedLocale(
55 | localeName,
56 | (locale) => _deferredLibraries[locale] != null,
57 | onFailure: (_) => null);
58 | if (availableLocale == null) {
59 | return Future.value(false);
60 | }
61 | var lib = _deferredLibraries[availableLocale];
62 | await (lib == null ? Future.value(false) : lib());
63 | initializeInternalMessageLookup(() => CompositeMessageLookup());
64 | messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
65 | return Future.value(true);
66 | }
67 |
68 | bool _messagesExistFor(String locale) {
69 | try {
70 | return _findExact(locale) != null;
71 | } catch (e) {
72 | return false;
73 | }
74 | }
75 |
76 | MessageLookupByLibrary? _findGeneratedMessagesFor(String locale) {
77 | var actualLocale = Intl.verifiedLocale(locale, _messagesExistFor,
78 | onFailure: (_) => null);
79 | if (actualLocale == null) return null;
80 | return _findExact(actualLocale);
81 | }
82 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:io';
6 |
7 | import 'package:anytime/services/settings/mobile_settings_service.dart';
8 | import 'package:anytime/ui/anytime_podcast_app.dart';
9 | import 'package:device_info_plus/device_info_plus.dart';
10 | import 'package:flutter/material.dart';
11 | import 'package:flutter/services.dart';
12 | import 'package:logging/logging.dart';
13 |
14 | // ignore_for_file: avoid_print
15 | void main() async {
16 | List certificateAuthorityBytes = [];
17 | WidgetsFlutterBinding.ensureInitialized();
18 | SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
19 |
20 | Logger.root.level = Level.FINE;
21 |
22 | Logger.root.onRecord.listen((record) {
23 | print('${record.level.name}: - ${record.time}: ${record.loggerName}: ${record.message}');
24 | });
25 |
26 | var mobileSettingsService = (await MobileSettingsService.instance())!;
27 | certificateAuthorityBytes = await setupCertificateAuthority();
28 |
29 | runApp(AnytimePodcastApp(
30 | mobileSettingsService: mobileSettingsService,
31 | certificateAuthorityBytes: certificateAuthorityBytes,
32 | ));
33 | }
34 |
35 | /// When certificate authorities certificates expire, older devices may not be able to handle
36 | /// the re-issued certificate resulting in SSL errors being thrown. This routine is called to
37 | /// manually install the newer certificates on older devices so they continue to work.
38 | Future> setupCertificateAuthority() async {
39 | List ca = [];
40 | var loadedCerts = false;
41 |
42 | if (Platform.isAndroid) {
43 | DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
44 | AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
45 | var major = androidInfo.version.release.split('.');
46 |
47 | if ((int.tryParse(major[0]) ?? 100.0) < 8.0) {
48 | ByteData data = await PlatformAssetBundle().load('assets/ca/lets-encrypt-r3.pem');
49 | ca.addAll(data.buffer.asUint8List());
50 | loadedCerts = true;
51 | }
52 |
53 | if ((int.tryParse(major[0]) ?? 100.0) < 10.0) {
54 | ByteData data = await PlatformAssetBundle().load('assets/ca/globalsign-gcc-r6-alphassl-ca-2023.pem');
55 | ca.addAll(data.buffer.asUint8List());
56 | loadedCerts = true;
57 | }
58 |
59 | if (loadedCerts) {
60 | SecurityContext.defaultContext.setTrustedCertificatesBytes(ca);
61 | }
62 | }
63 |
64 | return ca;
65 | }
66 |
--------------------------------------------------------------------------------
/lib/navigation/navigation_route_observer.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:flutter/material.dart';
6 | import 'package:flutter/widgets.dart';
7 |
8 | /// This class will observe the current route.
9 | ///
10 | /// This gives us an easy way to tell what screen we are on from elsewhere within
11 | /// the application. This is useful, for example, when responding to external links
12 | /// and determining if we need to display the podcast details or just update the
13 | /// current screen.
14 | class NavigationRouteObserver extends NavigatorObserver {
15 | final List?> _routeStack = ?>[];
16 |
17 | static final NavigationRouteObserver _instance = NavigationRouteObserver._internal();
18 |
19 | NavigationRouteObserver._internal();
20 |
21 | factory NavigationRouteObserver() {
22 | return _instance;
23 | }
24 |
25 | @override
26 | void didPop(Route route, Route? previousRoute) {
27 | _routeStack.removeLast();
28 | }
29 |
30 | @override
31 | void didPush(Route route, Route? previousRoute) {
32 | _routeStack.add(route);
33 | }
34 |
35 | @override
36 | void didRemove(Route route, Route? previousRoute) {
37 | _routeStack.remove(route);
38 | }
39 |
40 | @override
41 | void didReplace({Route? newRoute, Route? oldRoute}) {
42 | int oldRouteIndex = _routeStack.indexOf(oldRoute);
43 |
44 | _routeStack.replaceRange(oldRouteIndex, oldRouteIndex + 1, [newRoute]);
45 | }
46 |
47 | Route? get top => _routeStack.last;
48 | }
49 |
--------------------------------------------------------------------------------
/lib/repository/repository.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/entities/episode.dart';
6 | import 'package:anytime/entities/podcast.dart';
7 | import 'package:anytime/entities/transcript.dart';
8 | import 'package:anytime/state/episode_state.dart';
9 |
10 | /// An abstract class that represent the actions supported by the chosen
11 | /// database or storage implementation.
12 | abstract class Repository {
13 | /// General
14 | Future close();
15 |
16 | /// Podcasts
17 | Future findPodcastById(num id);
18 |
19 | Future findPodcastByGuid(String guid);
20 |
21 | Future savePodcast(Podcast podcast, {bool withEpisodes = true});
22 |
23 | Future deletePodcast(Podcast podcast);
24 |
25 | Future> subscriptions();
26 |
27 | /// Episodes
28 | Future> findAllEpisodes();
29 |
30 | Future findEpisodeById(int id);
31 |
32 | Future findEpisodeByGuid(String guid);
33 |
34 | Future> findEpisodesByPodcastGuid(
35 | String pguid, {
36 | PodcastEpisodeFilter filter = PodcastEpisodeFilter.none,
37 | PodcastEpisodeSort sort = PodcastEpisodeSort.none,
38 | });
39 |
40 | Future findEpisodeByTaskId(String taskId);
41 |
42 | Future findNextPlayableEpisode(Episode episode);
43 |
44 | Future saveEpisode(Episode episode, [bool updateIfSame = false]);
45 |
46 | Future> saveEpisodes(List episodes, [bool updateIfSame = false]);
47 |
48 | Future deleteEpisode(Episode episode);
49 |
50 | Future deleteEpisodes(List episodes);
51 |
52 | Future> findDownloadsByPodcastGuid(String pguid);
53 |
54 | Future> findDownloads();
55 |
56 | Future findTranscriptById(int id);
57 |
58 | Future saveTranscript(Transcript transcript);
59 |
60 | Future deleteTranscriptById(int id);
61 |
62 | Future deleteTranscriptsById(List id);
63 |
64 | /// Queue
65 | Future saveQueue(List episodes);
66 |
67 | Future> loadQueue();
68 |
69 | /// Event listeners
70 | late Stream podcastListener;
71 | late Stream episodeListener;
72 | }
73 |
--------------------------------------------------------------------------------
/lib/repository/sembast/sembast_database_service.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:async';
6 |
7 | import 'package:path/path.dart';
8 | import 'package:path_provider/path_provider.dart';
9 | import 'package:sembast/sembast_io.dart';
10 |
11 | typedef DatabaseUpgrade = Future Function(Database, int, int);
12 |
13 | /// Provides a database instance to other services and handles the opening
14 | /// of the Sembast DB.
15 | class DatabaseService {
16 | Completer? _databaseCompleter;
17 | String databaseName;
18 | int? version = 1;
19 | DatabaseUpgrade? upgraderCallback;
20 |
21 | DatabaseService(
22 | this.databaseName, {
23 | this.version,
24 | this.upgraderCallback,
25 | });
26 |
27 | Future get database async {
28 | if (_databaseCompleter == null) {
29 | _databaseCompleter = Completer();
30 | await _openDatabase();
31 | }
32 |
33 | return _databaseCompleter!.future;
34 | }
35 |
36 | Future _openDatabase() async {
37 | final appDocumentDir = await getApplicationDocumentsDirectory();
38 | final dbPath = join(appDocumentDir.path, databaseName);
39 | final database = await databaseFactoryIo.openDatabase(
40 | dbPath,
41 | version: version,
42 | onVersionChanged: (db, oldVersion, newVersion) async {
43 | if (upgraderCallback != null) {
44 | await upgraderCallback!(db, oldVersion, newVersion);
45 | }
46 | },
47 | );
48 |
49 | _databaseCompleter!.complete(database);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/lib/services/download/download_manager.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:async';
6 |
7 | import 'package:anytime/entities/downloadable.dart';
8 |
9 | class DownloadProgress {
10 | final String id;
11 | final int percentage;
12 | final DownloadState status;
13 |
14 | DownloadProgress(
15 | this.id,
16 | this.percentage,
17 | this.status,
18 | );
19 | }
20 |
21 | abstract class DownloadManager {
22 | Future enqueueTask(String url, String downloadPath, String fileName);
23 |
24 | Stream get downloadProgress;
25 |
26 | void dispose();
27 | }
28 |
--------------------------------------------------------------------------------
/lib/services/download/download_service.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/entities/episode.dart';
6 |
7 | abstract class DownloadService {
8 | Future downloadEpisode(Episode episode);
9 |
10 | Future findEpisodeByTaskId(String taskId);
11 |
12 | void dispose();
13 | }
14 |
--------------------------------------------------------------------------------
/lib/services/podcast/opml_service.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/state/opml_state.dart';
6 |
7 | /// This service handles the import and export of Podcasts via
8 | /// the OPML format.
9 | abstract class OPMLService {
10 | Stream loadOPMLFile(String file);
11 |
12 | Stream saveOPMLFile();
13 |
14 | void cancel();
15 | }
16 |
--------------------------------------------------------------------------------
/lib/services/settings/settings_service.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/entities/app_settings.dart';
6 |
7 | abstract class SettingsService {
8 | AppSettings? get settings;
9 |
10 | set settings(AppSettings? settings);
11 |
12 | String get theme;
13 |
14 | set theme(String mode);
15 |
16 | bool get markDeletedEpisodesAsPlayed;
17 |
18 | set markDeletedEpisodesAsPlayed(bool value);
19 |
20 | bool get deleteDownloadedPlayedEpisodes;
21 |
22 | set deleteDownloadedPlayedEpisodes(bool value);
23 |
24 | bool get storeDownloadsSDCard;
25 |
26 | set storeDownloadsSDCard(bool value);
27 |
28 | set playbackSpeed(double playbackSpeed);
29 |
30 | double get playbackSpeed;
31 |
32 | set searchProvider(String provider);
33 |
34 | String get searchProvider;
35 |
36 | set externalLinkConsent(bool consent);
37 |
38 | bool get externalLinkConsent;
39 |
40 | set autoOpenNowPlaying(bool autoOpenNowPlaying);
41 |
42 | bool get autoOpenNowPlaying;
43 |
44 | set showFunding(bool show);
45 |
46 | bool get showFunding;
47 |
48 | set autoUpdateEpisodePeriod(int period);
49 |
50 | int get autoUpdateEpisodePeriod;
51 |
52 | set trimSilence(bool trim);
53 |
54 | bool get trimSilence;
55 |
56 | set volumeBoost(bool boost);
57 |
58 | bool get volumeBoost;
59 |
60 | set layoutMode(int mode);
61 |
62 | int get layoutMode;
63 |
64 | set autoPlay(bool autoPlay);
65 |
66 | bool get autoPlay;
67 |
68 | Stream get settingsListener;
69 | }
70 |
--------------------------------------------------------------------------------
/lib/state/bloc_state.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | /// The BLoCs in this application share common states, such as loading, error
6 | /// or populated.
7 | ///
8 | /// Rather than having a separate selection of state classes, we create this generic one.
9 | enum BlocErrorType { unknown, connectivity, timeout }
10 |
11 | abstract class BlocState {}
12 |
13 | class BlocDefaultState extends BlocState {}
14 |
15 | class BlocLoadingState extends BlocState {
16 | final T? data;
17 |
18 | BlocLoadingState([this.data]);
19 | }
20 |
21 | class BlocBackgroundLoadingState extends BlocState {
22 | final T? data;
23 |
24 | BlocBackgroundLoadingState([this.data]);
25 | }
26 |
27 | class BlocSuccessfulState extends BlocState {}
28 |
29 | class BlocEmptyState extends BlocState {}
30 |
31 | class BlocErrorState extends BlocState {
32 | final BlocErrorType error;
33 |
34 | BlocErrorState({
35 | this.error = BlocErrorType.unknown,
36 | });
37 | }
38 |
39 | class BlocNoInputState extends BlocState {}
40 |
41 | class BlocPopulatedState extends BlocState {
42 | final T? results;
43 |
44 | BlocPopulatedState({this.results});
45 | }
46 |
--------------------------------------------------------------------------------
/lib/state/episode_state.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/entities/episode.dart';
6 |
7 | abstract class EpisodeState {
8 | final Episode episode;
9 |
10 | EpisodeState(this.episode);
11 | }
12 |
13 | class EpisodeUpdateState extends EpisodeState {
14 | EpisodeUpdateState(super.episode);
15 | }
16 |
17 | class EpisodeDeleteState extends EpisodeState {
18 | EpisodeDeleteState(super.episode);
19 | }
20 |
--------------------------------------------------------------------------------
/lib/state/opml_state.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | abstract class OPMLState {}
6 |
7 | class OPMLNoneState extends OPMLState {}
8 |
9 | class OPMLParsingState extends OPMLState {}
10 |
11 | class OPMLLoadingState extends OPMLState {
12 | final int current;
13 | final int total;
14 | final String? podcast;
15 |
16 | OPMLLoadingState({
17 | required this.current,
18 | required this.total,
19 | this.podcast,
20 | });
21 | }
22 |
23 | class OPMLCompletedState extends OPMLState {}
24 |
25 | class OPMLErrorState extends OPMLState {}
26 |
27 | abstract class OPMLEvent {}
28 |
29 | class OPMLImportEvent extends OPMLEvent {
30 | final String? file;
31 |
32 | OPMLImportEvent({
33 | this.file,
34 | });
35 | }
36 |
37 | class OPMLExportEvent extends OPMLEvent {}
38 |
39 | class OPMLCancelEvent extends OPMLEvent {}
40 |
--------------------------------------------------------------------------------
/lib/state/persistent_state.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:convert';
6 | import 'dart:io';
7 |
8 | import 'package:anytime/entities/persistable.dart';
9 | import 'package:path/path.dart';
10 | import 'package:path_provider/path_provider.dart';
11 |
12 | class PersistentState {
13 | static Future persistState(Persistable persistable) async {
14 | var d = await getApplicationSupportDirectory();
15 |
16 | var file = File(join(d.path, 'state.json'));
17 | var sink = file.openWrite();
18 | var json = jsonEncode(persistable.toMap());
19 |
20 | sink.write(json);
21 | await sink.flush();
22 | await sink.close();
23 | }
24 |
25 | static Future fetchState() async {
26 | var d = await getApplicationSupportDirectory();
27 |
28 | var file = File(join(d.path, 'state.json'));
29 | var p = Persistable.empty();
30 |
31 | if (file.existsSync()) {
32 | var result = file.readAsStringSync();
33 |
34 | if (result.isNotEmpty) {
35 | var data = jsonDecode(result) as Map;
36 |
37 | p = Persistable.fromMap(data);
38 | }
39 | }
40 |
41 | return Future.value(p);
42 | }
43 |
44 | static Future clearState() async {
45 | var file = await _getFile();
46 |
47 | if (file.existsSync()) {
48 | file.delete();
49 | }
50 | }
51 |
52 | static Future writeInt(String name, int value) async {
53 | return _writeValue(name, value.toString());
54 | }
55 |
56 | static Future readInt(String name) async {
57 | var result = await _readValue(name);
58 |
59 | return result.isEmpty ? 0 : int.parse(result);
60 | }
61 |
62 | static Future writeString(String name, String value) async {
63 | return _writeValue(name, value);
64 | }
65 |
66 | static Future readString(String name) async {
67 | return _readValue(name);
68 | }
69 |
70 | static Future _readValue(String name) async {
71 | var d = await getApplicationSupportDirectory();
72 |
73 | var file = File(join(d.path, name));
74 | var result = file.readAsStringSync();
75 |
76 | return result;
77 | }
78 |
79 | static Future _writeValue(String name, String value) async {
80 | var d = await getApplicationSupportDirectory();
81 |
82 | var file = File(join(d.path, name));
83 | var sink = file.openWrite();
84 |
85 | sink.write(value.toString());
86 | await sink.flush();
87 | await sink.close();
88 | }
89 |
90 | static Future _getFile() async {
91 | var d = await getApplicationSupportDirectory();
92 |
93 | return File(join(d.path, 'state.json'));
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/lib/state/queue_event_state.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/entities/episode.dart';
6 |
7 | abstract class QueueEvent {
8 | Episode? episode;
9 | int? position;
10 |
11 | QueueEvent({
12 | this.episode,
13 | this.position,
14 | });
15 | }
16 |
17 | class QueueAddEvent extends QueueEvent {
18 | QueueAddEvent({required Episode super.episode, super.position});
19 | }
20 |
21 | class QueueRemoveEvent extends QueueEvent {
22 | QueueRemoveEvent({required Episode episode}) : super(episode: episode);
23 | }
24 |
25 | class QueueMoveEvent extends QueueEvent {
26 | final int oldIndex;
27 | final int newIndex;
28 |
29 | QueueMoveEvent({
30 | required Episode episode,
31 | required this.oldIndex,
32 | required this.newIndex,
33 | }) : super(episode: episode);
34 | }
35 |
36 | class QueueClearEvent extends QueueEvent {}
37 |
38 | abstract class QueueState {
39 | final Episode? playing;
40 | final List queue;
41 |
42 | QueueState({
43 | required this.playing,
44 | required this.queue,
45 | });
46 | }
47 |
48 | class QueueListState extends QueueState {
49 | QueueListState({
50 | required super.playing,
51 | required super.queue,
52 | });
53 | }
54 |
55 | class QueueEmptyState extends QueueState {
56 | QueueEmptyState()
57 | : super(playing: Episode(guid: '', pguid: '', podcast: '', title: '', description: ''), queue: []);
58 | }
59 |
--------------------------------------------------------------------------------
/lib/state/transcript_state_event.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/entities/transcript.dart';
6 |
7 | /// Events
8 | abstract class TranscriptEvent {}
9 |
10 | class TranscriptClearEvent extends TranscriptEvent {}
11 |
12 | class TranscriptFilterEvent extends TranscriptEvent {
13 | final String search;
14 |
15 | TranscriptFilterEvent({required this.search});
16 | }
17 |
18 | /// State
19 | abstract class TranscriptState {
20 | final Transcript? transcript;
21 | final bool isFiltered;
22 |
23 | TranscriptState({
24 | this.transcript,
25 | this.isFiltered = false,
26 | });
27 | }
28 |
29 | class TranscriptUnavailableState extends TranscriptState {}
30 |
31 | class TranscriptLoadingState extends TranscriptState {}
32 |
33 | class TranscriptUpdateState extends TranscriptState {
34 | TranscriptUpdateState({required Transcript transcript}) : super(transcript: transcript);
35 | }
36 |
--------------------------------------------------------------------------------
/lib/ui/library/downloads.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/bloc/podcast/episode_bloc.dart';
6 | import 'package:anytime/entities/episode.dart';
7 | import 'package:anytime/l10n/L.dart';
8 | import 'package:anytime/state/bloc_state.dart';
9 | import 'package:anytime/ui/podcast/podcast_episode_list.dart';
10 | import 'package:anytime/ui/widgets/platform_progress_indicator.dart';
11 | import 'package:flutter/material.dart';
12 | import 'package:provider/provider.dart';
13 |
14 | /// Displays a list of currently downloaded podcast episodes.
15 | class Downloads extends StatefulWidget {
16 | const Downloads({
17 | super.key,
18 | });
19 |
20 | @override
21 | State createState() => _DownloadsState();
22 | }
23 |
24 | class _DownloadsState extends State {
25 | @override
26 | void initState() {
27 | super.initState();
28 |
29 | final bloc = Provider.of(context, listen: false);
30 |
31 | bloc.fetchDownloads(false);
32 | }
33 |
34 | @override
35 | Widget build(BuildContext context) {
36 | final bloc = Provider.of(context);
37 |
38 | return StreamBuilder(
39 | stream: bloc.downloads,
40 | builder: (BuildContext context, AsyncSnapshot snapshot) {
41 | final state = snapshot.data;
42 |
43 | if (state is BlocPopulatedState>) {
44 | return PodcastEpisodeList(
45 | episodes: state.results,
46 | play: true,
47 | download: false,
48 | icon: Icons.cloud_download,
49 | emptyMessage: L.of(context)!.no_downloads_message,
50 | );
51 | } else {
52 | if (state is BlocLoadingState) {
53 | return const SliverFillRemaining(
54 | hasScrollBody: false,
55 | child: Column(
56 | mainAxisAlignment: MainAxisAlignment.center,
57 | crossAxisAlignment: CrossAxisAlignment.center,
58 | children: [
59 | PlatformProgressIndicator(),
60 | ],
61 | ),
62 | );
63 | } else if (state is BlocErrorState) {
64 | return const SliverFillRemaining(
65 | hasScrollBody: false,
66 | child: Text('ERROR'),
67 | );
68 | }
69 |
70 | return SliverFillRemaining(
71 | hasScrollBody: false,
72 | child: Container(),
73 | );
74 | }
75 | },
76 | );
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/lib/ui/library/opml.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/l10n/L.dart';
6 | import 'package:anytime/ui/library/opml_import.dart';
7 | import 'package:file_picker/file_picker.dart';
8 | import 'package:flutter/cupertino.dart';
9 | import 'package:flutter/foundation.dart';
10 | import 'package:flutter/material.dart';
11 |
12 | class OPMLSelect extends StatefulWidget {
13 | const OPMLSelect({super.key});
14 |
15 | @override
16 | State createState() => _OPMLSelectState();
17 | }
18 |
19 | class _OPMLSelectState extends State {
20 | @override
21 | Widget build(BuildContext context) {
22 | switch (defaultTargetPlatform) {
23 | case TargetPlatform.android:
24 | return _buildAndroid(context);
25 | case TargetPlatform.iOS:
26 | return _buildIos(context);
27 | default:
28 | assert(false, 'Unexpected platform $defaultTargetPlatform');
29 | return _buildAndroid(context);
30 | }
31 | }
32 |
33 | Widget _buildAndroid(BuildContext context) {
34 | return Scaffold(
35 | appBar: AppBar(
36 | elevation: 0.0,
37 | title: Text(
38 | L.of(context)!.opml_import_export_label,
39 | ),
40 | ),
41 | body: _buildBody(context),
42 | );
43 | }
44 |
45 | Widget _buildIos(BuildContext context) {
46 | return CupertinoPageScaffold(
47 | navigationBar: const CupertinoNavigationBar(),
48 | child: _buildBody(context),
49 | );
50 | }
51 |
52 | Widget _buildBody(BuildContext context) {
53 | return Row(
54 | mainAxisAlignment: MainAxisAlignment.center,
55 | children: [
56 | Column(
57 | mainAxisAlignment: MainAxisAlignment.center,
58 | crossAxisAlignment: CrossAxisAlignment.center,
59 | children: [
60 | ElevatedButton(
61 | onPressed: () async {
62 | final navigator = Navigator.of(context);
63 | var result = (await FilePicker.platform.pickFiles())!;
64 |
65 | if (result.count > 0) {
66 | var file = result.files.first;
67 |
68 | await navigator.push(
69 | MaterialPageRoute(
70 | settings: const RouteSettings(name: 'opmlimport'),
71 | builder: (context) => OPMLImport(file: file.path!),
72 | fullscreenDialog: true,
73 | ),
74 | );
75 |
76 | navigator.pop();
77 | }
78 | },
79 | child: Text(L.of(context)!.opml_import_button_label),
80 | ),
81 | ElevatedButton(
82 | onPressed: () {},
83 | child: Text(L.of(context)!.opml_export_button_label),
84 | ),
85 | ],
86 | ),
87 | ],
88 | );
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/lib/ui/library/opml_export.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/bloc/podcast/opml_bloc.dart';
6 | import 'package:anytime/l10n/L.dart';
7 | import 'package:anytime/state/opml_state.dart';
8 | import 'package:flutter/material.dart';
9 | import 'package:provider/provider.dart';
10 |
11 | class OPMLExport extends StatefulWidget {
12 | const OPMLExport({
13 | super.key,
14 | });
15 |
16 | @override
17 | State createState() => _OPMLExportState();
18 | }
19 |
20 | class _OPMLExportState extends State {
21 | @override
22 | Widget build(BuildContext context) {
23 | final bloc = Provider.of(context, listen: false);
24 | final width = MediaQuery.of(context).size.width - 60.0;
25 |
26 | return SizedBox(
27 | height: 80,
28 | width: width,
29 | child: StreamBuilder(
30 | initialData: OPMLNoneState(),
31 | stream: bloc.opmlState,
32 | builder: (context, snapshot) {
33 | if (snapshot.data is OPMLCompletedState) {
34 | WidgetsBinding.instance.addPostFrameCallback((_) {
35 | Navigator.pop(context);
36 | });
37 | }
38 |
39 | return Row(
40 | mainAxisAlignment: MainAxisAlignment.start,
41 | crossAxisAlignment: CrossAxisAlignment.center,
42 | children: [
43 | const Flexible(
44 | child: CircularProgressIndicator.adaptive(),
45 | ),
46 | Flexible(
47 | child: Padding(
48 | padding: const EdgeInsets.all(8.0),
49 | child: Text(
50 | L.of(context)!.settings_export_opml,
51 | maxLines: 1,
52 | ),
53 | ),
54 | ),
55 | ],
56 | );
57 | }),
58 | );
59 | }
60 |
61 | @override
62 | void initState() {
63 | super.initState();
64 |
65 | final bloc = Provider.of(context, listen: false);
66 |
67 | bloc.opmlEvent(OPMLExportEvent());
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/lib/ui/podcast/dot_decoration.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:flutter/widgets.dart';
6 |
7 | /// Custom [Decoration] for the chapters, episode & notes tab selector
8 | /// shown in the [NowPlaying] page.
9 | class DotDecoration extends Decoration {
10 | final Color colour;
11 |
12 | const DotDecoration({required this.colour});
13 |
14 | @override
15 | BoxPainter createBoxPainter([void Function()? onChanged]) {
16 | return _DotDecorationPainter(decoration: this);
17 | }
18 | }
19 |
20 | class _DotDecorationPainter extends BoxPainter {
21 | final DotDecoration decoration;
22 |
23 | _DotDecorationPainter({required this.decoration});
24 |
25 | @override
26 | void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
27 | const double pillWidth = 8.0;
28 | const double pillHeight = 3.0;
29 |
30 | final center = configuration.size!.center(offset);
31 | final height = configuration.size!.height;
32 |
33 | final newOffset = Offset(center.dx, height - 8);
34 |
35 | final paint = Paint();
36 | paint.color = decoration.colour;
37 | paint.style = PaintingStyle.fill;
38 |
39 | canvas.drawRRect(
40 | RRect.fromLTRBR(
41 | newOffset.dx - pillWidth,
42 | newOffset.dy - pillHeight,
43 | newOffset.dx + pillWidth,
44 | newOffset.dy + pillHeight,
45 | const Radius.circular(12.0),
46 | ),
47 | paint);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/lib/ui/podcast/person_avatar.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 | // ignore_for_file: must_be_immutable
5 |
6 | import 'dart:async';
7 |
8 | import 'package:anytime/entities/person.dart';
9 | import 'package:extended_image/extended_image.dart';
10 | import 'package:flutter/material.dart';
11 | import 'package:url_launcher/url_launcher.dart';
12 |
13 | /// This Widget handles rendering of a person avatar.
14 | ///
15 | /// The data comes from the tag in the Podcasting 2.0 namespace.
16 | ///
17 | /// https://github.com/Podcastindex-org/podcast-namespace/blob/main/docs/1.0.md#person
18 | class PersonAvatar extends StatelessWidget {
19 | final Person person;
20 | final double width;
21 | final double radius;
22 | final EdgeInsetsGeometry padding;
23 | final bool showName;
24 | final bool highlight;
25 | String initials = '';
26 | String role = '';
27 |
28 | PersonAvatar({
29 | super.key,
30 | required this.person,
31 | this.width = 96,
32 | this.radius = 32,
33 | this.padding = const EdgeInsets.all(0.0),
34 | this.highlight = false,
35 | this.showName = true,
36 | }) {
37 | if (person.name.isNotEmpty) {
38 | var parts = person.name.split(' ');
39 |
40 | for (var i in parts) {
41 | if (i.isNotEmpty) {
42 | initials += i.substring(0, 1).toUpperCase();
43 | }
44 | }
45 | }
46 |
47 | if (person.role.isNotEmpty) {
48 | role = person.role.substring(0, 1).toUpperCase() + person.role.substring(1);
49 | }
50 | }
51 |
52 | @override
53 | Widget build(BuildContext context) {
54 | return GestureDetector(
55 | behavior: HitTestBehavior.translucent,
56 | onTap: person.link != null && person.link!.isNotEmpty
57 | ? () {
58 | final uri = Uri.parse(person.link!);
59 |
60 | unawaited(
61 | canLaunchUrl(uri).then((value) => launchUrl(uri)),
62 | );
63 | }
64 | : null,
65 | child: SizedBox(
66 | width: width,
67 | child: Padding(
68 | padding: const EdgeInsets.only(left: 16.0),
69 | child: Column(
70 | mainAxisSize: MainAxisSize.min,
71 | children: [
72 | Container(
73 | padding: padding,
74 | decoration: BoxDecoration(
75 | color: highlight ? Colors.orange : Colors.transparent,
76 | shape: BoxShape.circle,
77 | ),
78 | child: CircleAvatar(
79 | radius: radius,
80 | foregroundImage: person.image == null
81 | ? null
82 | : ExtendedImage.network(
83 | person.image!,
84 | cache: true,
85 | ).image,
86 | child: Text(initials),
87 | ),
88 | ),
89 | if (showName)
90 | Text(
91 | person.name,
92 | maxLines: 3,
93 | textAlign: TextAlign.center,
94 | ),
95 | if (showName) Text(role),
96 | ],
97 | ),
98 | ),
99 | ),
100 | );
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/lib/ui/podcast/playback_error_listener.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:async';
6 |
7 | import 'package:anytime/bloc/podcast/audio_bloc.dart';
8 | import 'package:anytime/l10n/L.dart';
9 | import 'package:flutter/material.dart';
10 | import 'package:provider/provider.dart';
11 |
12 | /// Listens for errors on the audio BLoC.
13 | ///
14 | /// We receive a code which we then map to an error message. This needs to be placed
15 | /// below a [Scaffold].
16 | class PlaybackErrorListener extends StatefulWidget {
17 | final Widget child;
18 |
19 | const PlaybackErrorListener({
20 | super.key,
21 | required this.child,
22 | });
23 |
24 | @override
25 | State createState() => _PlaybackErrorListenerState();
26 | }
27 |
28 | class _PlaybackErrorListenerState extends State {
29 | StreamSubscription? errorSubscription;
30 |
31 | @override
32 | Widget build(BuildContext context) {
33 | return widget.child;
34 | }
35 |
36 | @override
37 | void initState() {
38 | super.initState();
39 | final audioBloc = Provider.of(context, listen: false);
40 |
41 | errorSubscription = audioBloc.playbackError!.listen((code) {
42 | if (mounted) {
43 | ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(_codeToMessage(context, code))));
44 | }
45 | });
46 | }
47 |
48 | @override
49 | void dispose() {
50 | errorSubscription?.cancel();
51 | super.dispose();
52 | }
53 |
54 | /// Ideally the BLoC would pass us the message to display; however, as we need a
55 | /// context to fetch the correct version of any text string we need to work it out here.
56 | String _codeToMessage(BuildContext context, int code) {
57 | var result = '';
58 |
59 | switch (code) {
60 | case 401:
61 | result = L.of(context)!.error_no_connection;
62 | break;
63 | case 501:
64 | result = L.of(context)!.error_playback_fail;
65 | break;
66 | }
67 |
68 | return result;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/ui/podcast/podcast_episode_list.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/bloc/podcast/queue_bloc.dart';
6 | import 'package:anytime/entities/episode.dart';
7 | import 'package:anytime/state/queue_event_state.dart';
8 | import 'package:anytime/ui/widgets/episode_tile.dart';
9 | import 'package:flutter/material.dart';
10 | import 'package:provider/provider.dart';
11 |
12 | class PodcastEpisodeList extends StatelessWidget {
13 | final List? episodes;
14 | final IconData icon;
15 | final String emptyMessage;
16 | final bool play;
17 | final bool download;
18 |
19 | static const _defaultIcon = Icons.add_alert;
20 |
21 | const PodcastEpisodeList({
22 | super.key,
23 | required this.episodes,
24 | required this.play,
25 | required this.download,
26 | this.icon = _defaultIcon,
27 | this.emptyMessage = '',
28 | });
29 |
30 | @override
31 | Widget build(BuildContext context) {
32 | if (episodes != null && episodes!.isNotEmpty) {
33 | var queueBloc = Provider.of(context);
34 |
35 | return StreamBuilder(
36 | stream: queueBloc.queue,
37 | builder: (context, snapshot) {
38 | return SliverList(
39 | delegate: SliverChildBuilderDelegate(
40 | (BuildContext context, int index) {
41 | var queued = false;
42 | var playing = false;
43 | var episode = episodes![index]!;
44 |
45 | if (snapshot.hasData) {
46 | var playingGuid = snapshot.data!.playing?.guid;
47 |
48 | queued = snapshot.data!.queue.any((element) => element.guid == episode.guid);
49 |
50 | playing = playingGuid == episode.guid;
51 | }
52 |
53 | return EpisodeTile(
54 | episode: episode,
55 | download: download,
56 | play: play,
57 | playing: playing,
58 | queued: queued,
59 | );
60 | },
61 | childCount: episodes!.length,
62 | addAutomaticKeepAlives: false,
63 | ));
64 | });
65 | } else {
66 | return SliverFillRemaining(
67 | hasScrollBody: false,
68 | child: Padding(
69 | padding: const EdgeInsets.all(32.0),
70 | child: Column(
71 | mainAxisAlignment: MainAxisAlignment.center,
72 | crossAxisAlignment: CrossAxisAlignment.center,
73 | children: [
74 | Icon(
75 | icon,
76 | size: 75,
77 | color: Theme.of(context).primaryColor,
78 | ),
79 | Text(
80 | emptyMessage,
81 | style: Theme.of(context).textTheme.titleLarge,
82 | textAlign: TextAlign.center,
83 | ),
84 | ],
85 | ),
86 | ),
87 | );
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/lib/ui/podcast/show_notes.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/entities/episode.dart';
6 | import 'package:anytime/ui/widgets/podcast_html.dart';
7 | import 'package:flutter/material.dart';
8 | import 'package:flutter_html/flutter_html.dart';
9 |
10 | /// This class displays the show notes for the selected podcast.
11 | ///
12 | /// We make use of [Html] to render the notes and, if in HTML format, display the
13 | /// correct formatting, links etc.
14 | class ShowNotes extends StatelessWidget {
15 | final ScrollController _sliverScrollController = ScrollController();
16 | final Episode episode;
17 |
18 | ShowNotes({
19 | super.key,
20 | required this.episode,
21 | });
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | final textTheme = Theme.of(context).textTheme;
26 |
27 | return Scaffold(
28 | backgroundColor: Theme.of(context).scaffoldBackgroundColor,
29 | body: CustomScrollView(controller: _sliverScrollController, slivers: [
30 | SliverAppBar(
31 | title: Text(episode.podcast!),
32 | floating: false,
33 | pinned: true,
34 | snap: false,
35 | ),
36 | SliverToBoxAdapter(
37 | child: Column(
38 | mainAxisAlignment: MainAxisAlignment.start,
39 | crossAxisAlignment: CrossAxisAlignment.start,
40 | children: [
41 | Padding(
42 | padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 8.0),
43 | child: Text(episode.title ?? '', style: textTheme.titleLarge),
44 | ),
45 | Padding(
46 | padding: const EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 0.0),
47 | child: PodcastHtml(content: episode.content ?? episode.description!),
48 | ),
49 | ],
50 | ),
51 | ),
52 | ]));
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/ui/search/search_bar.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 | import 'package:anytime/l10n/L.dart';
5 | import 'package:anytime/ui/widgets/search_slide_route.dart';
6 | import 'package:flutter/material.dart';
7 | import 'package:flutter/services.dart';
8 |
9 | import 'search.dart';
10 |
11 | class SearchBar extends StatefulWidget {
12 | const SearchBar({super.key});
13 |
14 | @override
15 | State createState() => _SearchBarState();
16 | }
17 |
18 | class _SearchBarState extends State {
19 | late TextEditingController _searchController;
20 | late FocusNode _searchFocusNode;
21 |
22 | @override
23 | void initState() {
24 | super.initState();
25 | _searchController = TextEditingController();
26 | _searchController.addListener(() {
27 | setState(() {});
28 | });
29 | _searchFocusNode = FocusNode();
30 | }
31 |
32 | @override
33 | void dispose() {
34 | _searchFocusNode.dispose();
35 | _searchController.dispose();
36 |
37 | super.dispose();
38 | }
39 |
40 | @override
41 | Widget build(BuildContext context) {
42 | return ListTile(
43 | contentPadding: const EdgeInsets.only(left: 16, right: 16),
44 | title: TextField(
45 | controller: _searchController,
46 | focusNode: _searchFocusNode,
47 | keyboardType: TextInputType.text,
48 | textInputAction: TextInputAction.search,
49 | decoration: InputDecoration(hintText: L.of(context)!.search_for_podcasts_hint, border: InputBorder.none),
50 | style: TextStyle(
51 | color: Theme.of(context).primaryIconTheme.color,
52 | fontSize: 18.0,
53 | decorationColor: Theme.of(context).scaffoldBackgroundColor),
54 | onSubmitted: (value) async {
55 | await Navigator.push(
56 | context,
57 | SlideRightRoute(
58 | widget: Search(searchTerm: value),
59 | settings: const RouteSettings(name: 'search'),
60 | ));
61 | _searchController.clear();
62 | },
63 | ),
64 | trailing: IconButton(
65 | padding: EdgeInsets.zero,
66 | tooltip: _searchFocusNode.hasFocus ? L.of(context)!.clear_search_button_label : null,
67 | color: _searchFocusNode.hasFocus ? Theme.of(context).iconTheme.color : null,
68 | splashColor: _searchFocusNode.hasFocus ? Theme.of(context).splashColor : Colors.transparent,
69 | highlightColor: _searchFocusNode.hasFocus ? Theme.of(context).highlightColor : Colors.transparent,
70 | icon: Icon(_searchController.text.isEmpty && !_searchFocusNode.hasFocus ? Icons.search : Icons.clear),
71 | onPressed: () {
72 | _searchController.clear();
73 | FocusScope.of(context).requestFocus(FocusNode());
74 | SystemChannels.textInput.invokeMethod('TextInput.show');
75 | }),
76 | );
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/lib/ui/search/search_results.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/l10n/L.dart';
6 | import 'package:anytime/state/bloc_state.dart';
7 | import 'package:anytime/ui/widgets/platform_progress_indicator.dart';
8 | import 'package:anytime/ui/widgets/podcast_list.dart';
9 | import 'package:flutter/material.dart';
10 | import 'package:podcast_search/podcast_search.dart' as search;
11 |
12 | class SearchResults extends StatelessWidget {
13 | final Stream data;
14 |
15 | const SearchResults({
16 | super.key,
17 | required this.data,
18 | });
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | return StreamBuilder(
23 | stream: data,
24 | builder: (BuildContext context, AsyncSnapshot snapshot) {
25 | final state = snapshot.data;
26 |
27 | if (state is BlocPopulatedState) {
28 | return PodcastList(results: state.results as search.SearchResult);
29 | } else {
30 | if (state is BlocLoadingState) {
31 | return const SliverFillRemaining(
32 | hasScrollBody: false,
33 | child: Column(
34 | mainAxisAlignment: MainAxisAlignment.center,
35 | crossAxisAlignment: CrossAxisAlignment.center,
36 | children: [
37 | PlatformProgressIndicator(),
38 | ],
39 | ),
40 | );
41 | } else if (state is BlocErrorState) {
42 | return SliverFillRemaining(
43 | hasScrollBody: false,
44 | child: Padding(
45 | padding: const EdgeInsets.all(32.0),
46 | child: Column(
47 | mainAxisAlignment: MainAxisAlignment.center,
48 | crossAxisAlignment: CrossAxisAlignment.center,
49 | children: [
50 | Icon(
51 | Icons.search,
52 | size: 75,
53 | color: Theme.of(context).primaryColor,
54 | ),
55 | Text(
56 | L.of(context)!.no_search_results_message,
57 | style: Theme.of(context).textTheme.titleLarge,
58 | textAlign: TextAlign.center,
59 | ),
60 | ],
61 | ),
62 | ),
63 | );
64 | }
65 |
66 | return SliverFillRemaining(
67 | hasScrollBody: false,
68 | child: Container(),
69 | );
70 | }
71 | },
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/ui/settings/settings_section_label.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:flutter/material.dart';
6 |
7 | class SettingsDividerLabel extends StatelessWidget {
8 | final String label;
9 | final EdgeInsetsGeometry padding;
10 |
11 | const SettingsDividerLabel({
12 | super.key,
13 | required this.label,
14 | this.padding = const EdgeInsets.fromLTRB(16.0, 24.0, 0.0, 0.0),
15 | });
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | return Padding(
20 | padding: padding,
21 | child: Semantics(
22 | header: true,
23 | child: Text(
24 | label,
25 | style: Theme.of(context).textTheme.titleSmall!.copyWith(
26 | fontSize: 12.0,
27 | color: Theme.of(context).primaryColor,
28 | ),
29 | ),
30 | ),
31 | );
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/ui/widgets/action_text.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:io';
6 |
7 | import 'package:flutter/widgets.dart';
8 |
9 | /// This is a simple wrapper for the [Text] widget that is intended to
10 | /// be used with action dialogs.
11 | ///
12 | /// It should be supplied with a text value in sentence case. If running on
13 | /// Android this will be shifted to all upper case to meet the Material Design
14 | /// guidelines; otherwise it will be displayed as is to fit in the with iOS
15 | /// developer guidelines.
16 | class ActionText extends StatelessWidget {
17 | /// The text to display which will be shifted to all upper-case on Android.
18 | final String text;
19 |
20 | const ActionText(this.text, {super.key});
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return Platform.isAndroid ? Text(text.toUpperCase()) : Text(text);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/ui/widgets/decorated_icon_button.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:flutter/material.dart';
6 |
7 | /// An [IconButton] cannot have a background or border.
8 | ///
9 | /// This class wraps an IconButton in a shape so that it can have a background.
10 | class DecoratedIconButton extends StatelessWidget {
11 | final Color decorationColour;
12 | final Color iconColour;
13 | final IconData icon;
14 | final VoidCallback onPressed;
15 |
16 | const DecoratedIconButton({
17 | super.key,
18 | required this.iconColour,
19 | required this.decorationColour,
20 | required this.icon,
21 | required this.onPressed,
22 | });
23 |
24 | @override
25 | Widget build(BuildContext context) {
26 | return Material(
27 | color: Colors.transparent,
28 | child: Center(
29 | child: Ink(
30 | width: 42.0,
31 | height: 42.0,
32 | decoration: ShapeDecoration(
33 | color: decorationColour,
34 | shape: const CircleBorder(),
35 | ),
36 | child: IconButton(
37 | icon: Icon(icon),
38 | padding: const EdgeInsets.all(0.0),
39 | color: iconColour,
40 | onPressed: onPressed,
41 | ),
42 | ),
43 | ),
44 | );
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/lib/ui/widgets/delayed_progress_indicator.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/ui/widgets/platform_progress_indicator.dart';
6 | import 'package:flutter/material.dart';
7 |
8 | /// This class returns a platform-specific spinning indicator after a time specified
9 | /// in milliseconds.
10 | ///
11 | /// Defaults to 1 second. This can be used as a place holder for cached images. By
12 | /// delaying for several milliseconds it can reduce the occurrences of placeholders
13 | /// flashing on screen as the cached image is loaded. Images that take longer to fetch
14 | /// or process from the cache will result in a [PlatformProgressIndicator] indicator
15 | /// being displayed.
16 | class DelayedCircularProgressIndicator extends StatelessWidget {
17 | final f = Future.delayed(const Duration(milliseconds: 1000), () => Container());
18 |
19 | DelayedCircularProgressIndicator({
20 | super.key,
21 | });
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | return FutureBuilder(
26 | future: f,
27 | builder: (context, snapshot) {
28 | if (snapshot.connectionState == ConnectionState.done) {
29 | return const Center(
30 | child: PlatformProgressIndicator(),
31 | );
32 | } else {
33 | return Container();
34 | }
35 | });
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/ui/widgets/download_button.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:flutter/material.dart';
6 | import 'package:percent_indicator/percent_indicator.dart';
7 |
8 | /// Displays a download button for an episode.
9 | ///
10 | /// Can be passed a percentage representing the download progress which
11 | /// the button will then animate to show progress.
12 | class DownloadButton extends StatelessWidget {
13 | final String label;
14 | final String title;
15 | final IconData icon;
16 | final int percent;
17 | final VoidCallback onPressed;
18 |
19 | const DownloadButton({
20 | super.key,
21 | required this.label,
22 | required this.title,
23 | required this.icon,
24 | required this.percent,
25 | required this.onPressed,
26 | });
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | var progress = percent.toDouble() / 100;
31 |
32 | return Semantics(
33 | label: '$label $title',
34 | child: InkWell(
35 | onTap: onPressed,
36 | child: CircularPercentIndicator(
37 | radius: 19.0,
38 | lineWidth: 1.5,
39 | backgroundColor: Theme.of(context).primaryColor,
40 | progressColor: Theme.of(context).indicatorColor,
41 | animation: true,
42 | animateFromLastPercent: true,
43 | percent: progress,
44 | center: percent > 0
45 | ? Text(
46 | '$percent%',
47 | style: const TextStyle(
48 | fontSize: 12.0,
49 | ),
50 | )
51 | : Icon(
52 | icon,
53 | size: 22.0,
54 |
55 | /// Why is this not picking up the theme like other widgets?!?!?!
56 | color: Theme.of(context).primaryColor,
57 | ),
58 | ),
59 | ),
60 | );
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/lib/ui/widgets/draggable_episode_tile.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/bloc/podcast/audio_bloc.dart';
6 | import 'package:anytime/entities/episode.dart';
7 | import 'package:anytime/ui/widgets/episode_tile.dart';
8 | import 'package:anytime/ui/widgets/tile_image.dart';
9 | import 'package:flutter/material.dart';
10 | import 'package:provider/provider.dart';
11 |
12 | /// Renders an episode within the queue which can be dragged to re-order the queue.
13 | class DraggableEpisodeTile extends StatelessWidget {
14 | final Episode episode;
15 | final int index;
16 | final bool draggable;
17 | final bool playable;
18 |
19 | const DraggableEpisodeTile({
20 | super.key,
21 | required this.episode,
22 | this.index = 0,
23 | this.draggable = true,
24 | this.playable = false,
25 | });
26 |
27 | @override
28 | Widget build(BuildContext context) {
29 | final textTheme = Theme.of(context).textTheme;
30 | final audioBloc = Provider.of(context, listen: false);
31 |
32 | return ListTile(
33 | key: Key('DT${episode.guid}'),
34 | enabled: playable,
35 | leading: TileImage(
36 | url: episode.thumbImageUrl ?? episode.imageUrl ?? '',
37 | size: 56.0,
38 | highlight: episode.highlight,
39 | ),
40 | title: Text(
41 | episode.title!,
42 | overflow: TextOverflow.ellipsis,
43 | maxLines: 2,
44 | softWrap: false,
45 | style: textTheme.bodyMedium,
46 | ),
47 | subtitle: EpisodeSubtitle(episode),
48 | trailing: draggable
49 | ? ReorderableDragStartListener(
50 | index: index,
51 | child: const Icon(Icons.drag_handle),
52 | )
53 | : const SizedBox(
54 | width: 0.0,
55 | height: 0.0,
56 | ),
57 | onTap: () {
58 | if (playable) {
59 | audioBloc.play(episode);
60 | }
61 | },
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/ui/widgets/placeholder_builder.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 | import 'package:flutter/material.dart';
5 |
6 | class PlaceholderBuilder extends InheritedWidget {
7 | final WidgetBuilder Function() builder;
8 | final WidgetBuilder Function() errorBuilder;
9 |
10 | const PlaceholderBuilder({
11 | super.key,
12 | required this.builder,
13 | required this.errorBuilder,
14 | required super.child,
15 | });
16 |
17 | static PlaceholderBuilder? of(BuildContext context) {
18 | return context.dependOnInheritedWidgetOfExactType();
19 | }
20 |
21 | @override
22 | bool updateShouldNotify(PlaceholderBuilder oldWidget) {
23 | return builder != oldWidget.builder || errorBuilder != oldWidget.errorBuilder;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/ui/widgets/platform_back_button.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:io';
6 |
7 | import 'package:anytime/l10n/L.dart';
8 | import 'package:flutter/material.dart';
9 |
10 | /// Simple widget for rendering either the standard Android close or iOS Back button.
11 | class PlatformBackButton extends StatelessWidget {
12 | final Color decorationColour;
13 | final Color iconColour;
14 | final VoidCallback onPressed;
15 |
16 | const PlatformBackButton({
17 | super.key,
18 | required this.iconColour,
19 | required this.decorationColour,
20 | required this.onPressed,
21 | });
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | return Material(
26 | color: Colors.transparent,
27 | child: Semantics(
28 | button: true,
29 | child: Center(
30 | child: SizedBox(
31 | height: 48.0,
32 | width: 48.0,
33 | child: InkWell(
34 | onTap: onPressed,
35 | child: Container(
36 | margin: const EdgeInsets.all(6.0),
37 | height: 48.0,
38 | width: 48.0,
39 | decoration: ShapeDecoration(
40 | color: decorationColour,
41 | shape: const CircleBorder(),
42 | ),
43 | child: Padding(
44 | padding: EdgeInsets.only(left: Platform.isIOS ? 8.0 : 0.0),
45 | child: Icon(
46 | Platform.isIOS ? Icons.arrow_back_ios : Icons.close,
47 | size: Platform.isIOS ? 20.0 : 26.0,
48 | semanticLabel: L.of(context)?.go_back_button_label,
49 | ),
50 | ),
51 | ),
52 | ),
53 | ),
54 | ),
55 | ),
56 | );
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/lib/ui/widgets/platform_progress_indicator.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 | import 'package:anytime/l10n/L.dart';
5 | import 'package:flutter/cupertino.dart';
6 | import 'package:flutter/material.dart';
7 |
8 | /// The class returns a circular progress indicator that is appropriate for the platform
9 | /// it is running on.
10 | ///
11 | /// This boils down to a [CupertinoActivityIndicator] when running on iOS or MacOS
12 | /// and a [CircularProgressIndicator] for everything else.
13 | class PlatformProgressIndicator extends StatelessWidget {
14 | const PlatformProgressIndicator({
15 | super.key,
16 | });
17 |
18 | @override
19 | Widget build(BuildContext context) {
20 | var theme = Theme.of(context);
21 |
22 | switch (theme.platform) {
23 | case TargetPlatform.android:
24 | case TargetPlatform.fuchsia:
25 | case TargetPlatform.linux:
26 | case TargetPlatform.windows:
27 | return CircularProgressIndicator(
28 | semanticsLabel: L.of(context)!.semantic_announce_loading,
29 | );
30 | case TargetPlatform.iOS:
31 | case TargetPlatform.macOS:
32 | return const CupertinoActivityIndicator();
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/lib/ui/widgets/play_pause_button.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:flutter/material.dart';
6 | import 'package:flutter_spinkit/flutter_spinkit.dart';
7 | import 'package:percent_indicator/percent_indicator.dart';
8 |
9 | class PlayPauseButton extends StatelessWidget {
10 | final IconData icon;
11 | final String label;
12 | final String title;
13 |
14 | const PlayPauseButton({
15 | super.key,
16 | required this.icon,
17 | required this.label,
18 | required this.title,
19 | });
20 |
21 | @override
22 | Widget build(BuildContext context) {
23 | return Semantics(
24 | label: '$label $title',
25 | child: CircularPercentIndicator(
26 | radius: 19.0,
27 | lineWidth: 1.5,
28 | backgroundColor: Theme.of(context).primaryColor,
29 | percent: 0.0,
30 | center: Icon(
31 | icon,
32 | size: 22.0,
33 |
34 | /// Why is this not picking up the theme like other widgets?!?!?!
35 | color: Theme.of(context).primaryColor,
36 | ),
37 | ),
38 | );
39 | }
40 | }
41 |
42 | class PlayPauseBusyButton extends StatelessWidget {
43 | final IconData icon;
44 | final String label;
45 | final String title;
46 |
47 | const PlayPauseBusyButton({
48 | super.key,
49 | required this.icon,
50 | required this.label,
51 | required this.title,
52 | });
53 |
54 | @override
55 | Widget build(BuildContext context) {
56 | return Semantics(
57 | label: '$label $title',
58 | child: Stack(
59 | children: [
60 | SizedBox(
61 | height: 48.0,
62 | width: 48.0,
63 | child: Icon(
64 | icon,
65 | size: 22.0,
66 | color: Theme.of(context).primaryColor,
67 | ),
68 | ),
69 | SpinKitRing(
70 | lineWidth: 1.5,
71 | color: Theme.of(context).primaryColor,
72 | size: 38.0,
73 | ),
74 | ],
75 | ));
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/lib/ui/widgets/podcast_grid_tile.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/bloc/podcast/podcast_bloc.dart';
6 | import 'package:anytime/entities/podcast.dart';
7 | import 'package:anytime/ui/podcast/podcast_details.dart';
8 | import 'package:anytime/ui/widgets/tile_image.dart';
9 | import 'package:flutter/material.dart';
10 | import 'package:provider/provider.dart';
11 |
12 | class PodcastGridTile extends StatelessWidget {
13 | final Podcast podcast;
14 |
15 | const PodcastGridTile({
16 | super.key,
17 | required this.podcast,
18 | });
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | final podcastBloc = Provider.of(context);
23 |
24 | return GestureDetector(
25 | onTap: () {
26 | Navigator.push(
27 | context,
28 | MaterialPageRoute(
29 | settings: const RouteSettings(name: 'podcastdetails'),
30 | builder: (context) => PodcastDetails(podcast, podcastBloc)),
31 | );
32 | },
33 | child: Semantics(
34 | label: podcast.title,
35 | child: GridTile(
36 | child: Hero(
37 | key: Key('tilehero${podcast.imageUrl}:${podcast.link}'),
38 | tag: '${podcast.imageUrl}:${podcast.link}',
39 | child: TileImage(
40 | url: podcast.imageUrl!,
41 | size: 18.0,
42 | ),
43 | ),
44 | ),
45 | ),
46 | );
47 | }
48 | }
49 |
50 | class PodcastTitledGridTile extends StatelessWidget {
51 | final Podcast podcast;
52 |
53 | const PodcastTitledGridTile({
54 | super.key,
55 | required this.podcast,
56 | });
57 |
58 | @override
59 | Widget build(BuildContext context) {
60 | final podcastBloc = Provider.of(context);
61 | final theme = Theme.of(context);
62 |
63 | return GestureDetector(
64 | onTap: () {
65 | Navigator.push(
66 | context,
67 | MaterialPageRoute(
68 | settings: const RouteSettings(name: 'podcastdetails'),
69 | builder: (context) => PodcastDetails(podcast, podcastBloc)),
70 | );
71 | },
72 | child: GridTile(
73 | child: Hero(
74 | key: Key('tilehero${podcast.imageUrl}:${podcast.link}'),
75 | tag: '${podcast.imageUrl}:${podcast.link}',
76 | child: Column(
77 | children: [
78 | TileImage(
79 | url: podcast.imageUrl!,
80 | size: 128.0,
81 | ),
82 | Padding(
83 | padding: const EdgeInsets.only(
84 | top: 4.0,
85 | ),
86 | child: Text(
87 | podcast.title,
88 | maxLines: 2,
89 | overflow: TextOverflow.ellipsis,
90 | textAlign: TextAlign.center,
91 | style: theme.textTheme.titleSmall,
92 | ),
93 | ),
94 | ],
95 | ),
96 | ),
97 | ),
98 | );
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/lib/ui/widgets/podcast_html.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 | import 'package:flutter/material.dart';
5 | import 'package:flutter_html/flutter_html.dart';
6 | import 'package:flutter_html_svg/flutter_html_svg.dart';
7 | import 'package:flutter_html_table/flutter_html_table.dart';
8 | import 'package:url_launcher/url_launcher.dart';
9 |
10 | /// This class is a simple, common wrapper around the flutter_html Html widget.
11 | ///
12 | /// This wrapper allows us to remove some of the HTML tags which can cause rendering
13 | /// issues when viewing podcast descriptions on a mobile device.
14 | class PodcastHtml extends StatelessWidget {
15 | final String content;
16 | final FontSize? fontSize;
17 | final bool clipboard;
18 |
19 | const PodcastHtml({
20 | super.key,
21 | required this.content,
22 | this.fontSize,
23 | this.clipboard = true,
24 | });
25 |
26 | @override
27 | Widget build(BuildContext context) {
28 | return clipboard
29 | ? SelectionArea(
30 | child: HtmlRenderer(content: content),
31 | )
32 | : HtmlRenderer(content: content);
33 | }
34 | }
35 |
36 | class HtmlRenderer extends StatelessWidget {
37 | const HtmlRenderer({
38 | super.key,
39 | required this.content,
40 | });
41 |
42 | final String content;
43 |
44 | @override
45 | Widget build(BuildContext context) {
46 | return Html(
47 | data: content,
48 | extensions: const [
49 | SvgHtmlExtension(),
50 | TableHtmlExtension(),
51 | ],
52 | style: {
53 | 'html': Style(
54 | fontSize: FontSize(16.25),
55 | lineHeight: LineHeight.percent(110),
56 | ),
57 | 'p': Style(
58 | margin: Margins.only(
59 | top: 0,
60 | bottom: 12,
61 | ),
62 | ),
63 | },
64 | onLinkTap: (url, _, __) => canLaunchUrl(Uri.parse(url!)).then((value) => launchUrl(
65 | Uri.parse(url),
66 | mode: LaunchMode.externalApplication,
67 | )),
68 | );
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/lib/ui/widgets/podcast_tile.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/bloc/podcast/podcast_bloc.dart';
6 | import 'package:anytime/entities/podcast.dart';
7 | import 'package:anytime/ui/podcast/podcast_details.dart';
8 | import 'package:anytime/ui/widgets/tile_image.dart';
9 | import 'package:flutter/material.dart';
10 | import 'package:provider/provider.dart';
11 |
12 | class PodcastTile extends StatelessWidget {
13 | final Podcast podcast;
14 |
15 | const PodcastTile({
16 | super.key,
17 | required this.podcast,
18 | });
19 |
20 | @override
21 | Widget build(BuildContext context) {
22 | final podcastBloc = Provider.of(context);
23 |
24 | return ListTile(
25 | onTap: () {
26 | Navigator.push(
27 | context,
28 | MaterialPageRoute(
29 | settings: const RouteSettings(name: 'podcastdetails'),
30 | builder: (context) => PodcastDetails(podcast, podcastBloc)),
31 | );
32 | },
33 | minVerticalPadding: 9,
34 | leading: ExcludeSemantics(
35 | child: Hero(
36 | key: Key('tilehero${podcast.imageUrl}:${podcast.link}'),
37 | tag: '${podcast.imageUrl}:${podcast.link}',
38 | child: TileImage(
39 | url: podcast.imageUrl!,
40 | size: 60,
41 | ),
42 | ),
43 | ),
44 | title: Text(
45 | podcast.title,
46 | maxLines: 1,
47 | ),
48 |
49 | /// A ListTile's density changes depending upon whether we have 2 or more lines of text. We
50 | /// manually add a newline character here to ensure the density is consistent whether the
51 | /// podcast subtitle spans 1 or more lines. Bit of a hack, but a simple solution.
52 | subtitle: Text(
53 | '${podcast.copyright ?? ''}\n',
54 | maxLines: 2,
55 | ),
56 | isThreeLine: false,
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib/ui/widgets/search_slide_route.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:flutter/material.dart';
6 |
7 | /// A transitioning route that slides the child in from the
8 | /// right.
9 | class SlideRightRoute extends PageRouteBuilder {
10 | final Widget widget;
11 |
12 | @override
13 | final RouteSettings settings;
14 |
15 | SlideRightRoute({
16 | required this.widget,
17 | required this.settings,
18 | }) : super(
19 | pageBuilder: (
20 | BuildContext context,
21 | Animation animation,
22 | Animation secondaryAnimation,
23 | ) {
24 | return widget;
25 | },
26 | settings: settings,
27 | transitionsBuilder: (
28 | BuildContext context,
29 | Animation animation,
30 | Animation secondaryAnimation,
31 | Widget child,
32 | ) {
33 | return SlideTransition(
34 | position: Tween(
35 | begin: const Offset(
36 | 1.0,
37 | 0.0,
38 | ),
39 | end: Offset.zero,
40 | ).animate(animation),
41 | child: child,
42 | );
43 | });
44 | }
45 |
--------------------------------------------------------------------------------
/lib/ui/widgets/slider_handle.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:flutter/material.dart';
6 |
7 | /// This class generates a simple 'handle' icon that can be added on widgets such as
8 | /// scrollable sheets and bottom dialogs.
9 | ///
10 | /// When running with a screen reader, the handle icon becomes selectable with an
11 | /// optional label and tap callback. This makes it easier to open/close.
12 | class SliderHandle extends StatelessWidget {
13 | final GestureTapCallback? onTap;
14 | final String label;
15 |
16 | const SliderHandle({
17 | super.key,
18 | this.onTap,
19 | this.label = '',
20 | });
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | return Semantics(
25 | liveRegion: true,
26 | label: label,
27 | child: GestureDetector(
28 | onTap: onTap,
29 | child: Padding(
30 | padding: const EdgeInsets.all(8.0),
31 | child: Container(
32 | width: 36,
33 | height: 4,
34 | decoration: BoxDecoration(
35 | color: Theme.of(context).hintColor,
36 | borderRadius: const BorderRadius.all(Radius.circular(4.0)),
37 | ),
38 | ),
39 | ),
40 | ),
41 | );
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/ui/widgets/sync_spinner.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:async';
6 |
7 | import 'package:anytime/bloc/podcast/podcast_bloc.dart';
8 | import 'package:anytime/state/bloc_state.dart';
9 | import 'package:flutter/material.dart';
10 | import 'package:provider/provider.dart';
11 |
12 | class SyncSpinner extends StatefulWidget {
13 | const SyncSpinner({super.key});
14 |
15 | @override
16 | State createState() => _SyncSpinnerState();
17 | }
18 |
19 | class _SyncSpinnerState extends State with SingleTickerProviderStateMixin {
20 | late AnimationController _controller;
21 | StreamSubscription>? subscription;
22 | Widget? _child;
23 |
24 | @override
25 | void initState() {
26 | super.initState();
27 |
28 | _controller = AnimationController(
29 | vsync: this,
30 | duration: const Duration(milliseconds: 1500),
31 | )..repeat();
32 |
33 | _child = const Icon(
34 | Icons.refresh,
35 | size: 16.0,
36 | );
37 |
38 | final podcastBloc = Provider.of(context, listen: false);
39 |
40 | subscription = podcastBloc.backgroundLoading.listen((event) {
41 | if (event is BlocSuccessfulState || event is BlocErrorState) {
42 | _controller.stop();
43 | }
44 | });
45 | }
46 |
47 | @override
48 | void dispose() {
49 | _controller.dispose();
50 | subscription?.cancel();
51 |
52 | super.dispose();
53 | }
54 |
55 | @override
56 | Widget build(BuildContext context) {
57 | final podcastBloc = Provider.of(context, listen: false);
58 |
59 | return StreamBuilder>(
60 | initialData: BlocEmptyState(),
61 | stream: podcastBloc.backgroundLoading,
62 | builder: (context, snapshot) {
63 | final state = snapshot.data;
64 |
65 | return state is BlocLoadingState
66 | ? RotationTransition(
67 | turns: _controller,
68 | child: _child,
69 | )
70 | : const SizedBox(
71 | width: 0.0,
72 | height: 0.0,
73 | );
74 | });
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/lib/ui/widgets/tile_image.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/ui/widgets/placeholder_builder.dart';
6 | import 'package:anytime/ui/widgets/podcast_image.dart';
7 | import 'package:flutter/material.dart';
8 |
9 | class TileImage extends StatelessWidget {
10 | const TileImage({
11 | super.key,
12 | required this.url,
13 | required this.size,
14 | this.highlight = false,
15 | });
16 |
17 | /// The URL of the image to display.
18 | final String url;
19 |
20 | /// The size of the image container; both height and width.
21 | final double size;
22 |
23 | final bool highlight;
24 |
25 | @override
26 | Widget build(BuildContext context) {
27 | final placeholderBuilder = PlaceholderBuilder.of(context);
28 |
29 | return PodcastImage(
30 | key: Key('tile$url'),
31 | highlight: highlight,
32 | url: url,
33 | height: size,
34 | width: size,
35 | borderRadius: 4.0,
36 | fit: BoxFit.contain,
37 | placeholder: placeholderBuilder != null
38 | ? placeholderBuilder.builder()(context)
39 | : const Image(
40 | fit: BoxFit.contain,
41 | image: AssetImage('assets/images/anytime-placeholder-logo.png'),
42 | ),
43 | errorPlaceholder: placeholderBuilder != null
44 | ? placeholderBuilder.errorBuilder()(context)
45 | : const Image(
46 | fit: BoxFit.contain,
47 | image: AssetImage('assets/images/anytime-placeholder-logo.png'),
48 | ),
49 | );
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/metadata/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Anytime Podcast Player is a free and open-source podcast player that's designed to be simple and easy to use. Anytime is Podcasting 2.0 ready and will support more features as the app is developed.
2 |
3 | Discover podcasts:
4 | - Search from over 4 million free podcasts.
5 | - Discover something new in the podcast charts.
6 | - Follow your favourite podcasts so you never miss an episode.
7 | - Stream episodes or download for offline playback later.
8 |
9 | Features:
10 | - View episode chapters and skip to the part of an episode you're interested in*
11 | - Directly support the show via funding links*
12 | - Read, search or follow along with transcripts (where available)*
13 | - Listen at faster or slower speeds.
14 | - Sort & filter episodes.
15 | - Pause a streamed or downloaded episode and pickup where you left off later on.
16 | - Playback controllable from notification shade.
17 | - Playback controllable from WearOS device.
18 | - OPML import & export.
19 |
20 | * Chapters, funding links and transcripts appear for podcasts that support Podcasting 2.0.
--------------------------------------------------------------------------------
/metadata/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/metadata/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/metadata/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/metadata/en-US/images/icon.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/metadata/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/metadata/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/metadata/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/metadata/en-US/images/phoneScreenshots/4.png
--------------------------------------------------------------------------------
/metadata/en-US/images/phoneScreenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amugofjava/anytime_podcast_player/ced082d38b228ad1dda976cfaea77119bf3b3fc7/metadata/en-US/images/phoneScreenshots/5.png
--------------------------------------------------------------------------------
/metadata/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | The easy to use open-source podcast player for mobile and tablet.
--------------------------------------------------------------------------------
/metadata/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Anytime Podcast Player
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: anytime
2 | description: Anytime Podcast Player
3 |
4 | version: 1.3.13+190
5 |
6 | environment:
7 | sdk: '>=3.2.0 <4.0.0'
8 | flutter: 3.32.1
9 |
10 | dependencies:
11 | accessibility_tools: ^2.4.1
12 | app_links: ^6.3.0
13 | auto_size_text: ^3.0.0
14 | audio_service: ^0.18.18
15 | audio_session: ^0.2.1
16 | collection: ^1.18.0
17 | connectivity_plus: ^6.0.11
18 | cupertino_icons: ^1.0.6
19 | device_info_plus: ^11.2.0
20 | extended_image: ^9.1.0
21 | file_picker: ^10.0.0
22 | flutter_dialogs: ^3.0.0
23 | flutter_downloader: ^1.12.0
24 | flutter_html: ^3.0.0
25 | flutter_html_svg: ^3.0.0
26 | flutter_html_table: ^3.0.0
27 | flutter_launcher_icons: ^0.14.2
28 | flutter_spinkit: ^5.0.0
29 | html: ^0.15.6
30 | intl: ^0.20.2
31 | intl_translation: ^0.20.0
32 | just_audio: ^0.10.2
33 | logging: ^1.3.0
34 | meta: ^1.12.0
35 | mp3_info: ^0.2.0
36 | path: ^1.8.3
37 | path_provider: ^2.1.4
38 | path_provider_platform_interface: ^2.0.4
39 | percent_indicator: 4.2.5
40 | permission_handler: ^12.0.0+1
41 | podcast_search: ^0.7.10
42 | provider: ^6.0.3
43 | rxdart: ^0.28.0
44 | scrollable_positioned_list: ^0.3.7
45 | sembast: ^3.8.3
46 | share_plus: ^11.0.0
47 | shared_preferences: ^2.3.4
48 | sliver_tools: ^0.2.12
49 | url_launcher: ^6.3.1
50 | xml: 6.5.0
51 |
52 | flutter:
53 | sdk: flutter
54 | flutter_localizations:
55 | sdk: flutter
56 |
57 | dev_dependencies:
58 | mockito: ^5.2.0
59 | flutter_lints: ^4.0.0
60 | flutter_test:
61 | sdk: flutter
62 |
63 | flutter_icons:
64 | image_path_android: "assets/images/anytime-logo.png"
65 | image_path_ios: "assets/images/anytime-logo-ios.png"
66 | remove_alpha_ios: true
67 | android: true
68 | ios: true
69 |
70 | flutter:
71 | uses-material-design: true
72 |
73 | assets:
74 | - assets/images/anytime-logo.png
75 | - assets/images/anytime-logo-s.png
76 | - assets/images/anytime-placeholder-logo.png
77 |
78 | # Certificate authorities
79 | - assets/ca/lets-encrypt-r3.pem
80 | - assets/ca/globalsign-gcc-r6-alphassl-ca-2023.pem
81 |
82 | fonts:
83 | - family: MontserratMedium
84 | fonts:
85 | - asset: assets/fonts/Montserrat-Medium.otf
86 | - family: MontserratRegular
87 | fonts:
88 | - asset: assets/fonts/Montserrat-Regular.otf
89 | - family: MontserratBold
90 | fonts:
91 | - asset: assets/fonts/Montserrat-Bold.otf
92 |
93 |
--------------------------------------------------------------------------------
/test/unit/mocks/mock_path_provider.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:io';
6 |
7 | import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
8 |
9 | class MockPathProvder extends PathProviderPlatform {
10 | Future getApplicationDocumentsDirectory() {
11 | return Future.value(Directory.systemTemp);
12 | }
13 |
14 | @override
15 | Future getApplicationDocumentsPath() {
16 | return Future.value(Directory.systemTemp.path);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/test/unit/mocks/mock_podcast_api.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'package:anytime/api/podcast/mobile_podcast_api.dart';
6 | import 'package:podcast_search/podcast_search.dart';
7 |
8 | /// This Mock version of the Podcast API replaces loading via URL
9 | /// with loading via local file. This allows use to test API
10 | /// loading without requiring an Internet connection.
11 | class MockPodcastApi extends MobilePodcastApi {
12 | @override
13 | Future loadFeed(String? url) async {
14 | return _loadFeed(url!);
15 | }
16 |
17 | Future _loadFeed(String url) {
18 | return Feed.loadFeedFile(file: url);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/test/unit/mocks/mock_settings_service.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 | import 'package:anytime/entities/app_settings.dart';
5 | import 'package:anytime/services/settings/settings_service.dart';
6 | import 'package:flutter/material.dart';
7 | import 'package:rxdart/rxdart.dart';
8 |
9 | class MockSettingsService extends SettingsService {
10 | @override
11 | bool autoOpenNowPlaying = false;
12 |
13 | @override
14 | int autoUpdateEpisodePeriod = 1;
15 |
16 | @override
17 | bool externalLinkConsent = false;
18 |
19 | @override
20 | int layoutMode = 1;
21 |
22 | @override
23 | bool markDeletedEpisodesAsPlayed = false;
24 |
25 | @override
26 | bool deleteDownloadedPlayedEpisodes = false;
27 |
28 | @override
29 | double playbackSpeed = 1;
30 |
31 | @override
32 | String searchProvider = 'itunes';
33 |
34 | @override
35 | AppSettings? settings;
36 |
37 | @override
38 | bool showFunding = true;
39 |
40 | @override
41 | bool storeDownloadsSDCard = false;
42 |
43 | @override
44 | String theme = ThemeMode.dark.name;
45 |
46 | @override
47 | bool trimSilence = false;
48 |
49 | @override
50 | bool volumeBoost = false;
51 |
52 | @override
53 | Stream get settingsListener => PublishSubject().stream;
54 |
55 | @override
56 | bool autoPlay = false;
57 | }
58 |
--------------------------------------------------------------------------------
/test/unit/opml/opml_service_test.dart:
--------------------------------------------------------------------------------
1 | // Copyright 2020 Ben Hills and the project contributors. All rights reserved.
2 | // Use of this source code is governed by a BSD-style license that can be
3 | // found in the LICENSE file.
4 |
5 | import 'dart:io';
6 |
7 | import 'package:anytime/repository/repository.dart';
8 | import 'package:anytime/repository/sembast/sembast_repository.dart';
9 | import 'package:anytime/services/podcast/mobile_opml_service.dart';
10 | import 'package:anytime/services/podcast/mobile_podcast_service.dart';
11 | import 'package:anytime/services/podcast/opml_service.dart';
12 | import 'package:anytime/services/podcast/podcast_service.dart';
13 | import 'package:anytime/state/opml_state.dart';
14 | import 'package:flutter_test/flutter_test.dart';
15 | import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
16 |
17 | import '../mocks/mock_path_provider.dart';
18 | import '../mocks/mock_podcast_api.dart';
19 | import '../mocks/mock_settings_service.dart';
20 |
21 | void main() {
22 | final api = MockPodcastApi();
23 | final mockPath = MockPathProvder();
24 | const dbName = 'anytime-opml.db';
25 | late OPMLService opmlService;
26 | late PodcastService podcastService;
27 | Repository repository;
28 |
29 | setUp(() async {
30 | TestWidgetsFlutterBinding.ensureInitialized();
31 | PathProviderPlatform.instance = mockPath;
32 | repository = SembastRepository(databaseName: dbName);
33 |
34 | podcastService = MobilePodcastService(
35 | api: api,
36 | repository: repository,
37 | settingsService: MockSettingsService(),
38 | );
39 |
40 | opmlService = MobileOPMLService(podcastService: podcastService, repository: repository);
41 | });
42 |
43 | tearDown(() async {
44 | var f = File('${Directory.systemTemp.path}/$dbName');
45 |
46 | if (f.existsSync()) {
47 | f.deleteSync();
48 | }
49 | });
50 |
51 | test('Load test OPML file. Single Podcast. Single episode.', () async {
52 | var stream = opmlService.loadOPMLFile('test_resources/opml_import_test1.opml');
53 |
54 | await expectLater(
55 | stream,
56 | emitsInOrder([
57 | emits(isInstanceOf()),
58 | emits(isInstanceOf()),
59 | emits(isInstanceOf()),
60 | ]));
61 |
62 | var subs = await podcastService.subscriptions();
63 |
64 | expect(subs.length, 1);
65 | expect(subs[0].title, 'Podcast Load Test 1');
66 | expect(subs[0].url, 'test_resources/podcast1.rss');
67 | });
68 | }
69 |
--------------------------------------------------------------------------------
/test_resources/opml_import_test1.opml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Anytime Unit Test 1
5 | 03 Jun 21 16:50:02 +0100
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/test_resources/podcast1.rss:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Podcast Load Test 1
5 | Unit test podcast test 1
6 | https://nowhere.com/podcastsearchtest1
7 | Thu, 24 Jun 2021 18:00:20 +0000
8 | en
9 |
10 | https://nowhere.com/podcastsearchtest1/image1.png
11 | Podcast Load Test 1
12 | https://nowhere.com/podcastsearchtest1
13 |
14 |
15 |
16 |
17 |
18 | no
19 | Unit test podcast test 1
20 | Podcast Search Author
21 |
22 | Ben Hills
23 | anytime@amugofjava.me.uk
24 |
25 | podcast1.rss
26 | episodic
27 |
28 | -
29 | Episode 001
30 | https://nowhere.com/podcastsearchtest1/podcast1
31 |
32 | 1200
33 | yes
34 | full
35 | Test of episode 001]]>
36 | Episode summary 001
37 | Thu, 24 Jun 2021 18:00:00 +0000
38 |
39 | Ben Hills
40 | Ben Hills
41 | Podcast, NowPlaying, Listen, Test
42 | Podcast, NowPlaying, Listen, Test
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/useful_commands.txt:
--------------------------------------------------------------------------------
1 | ### Generate ARB files from Dart code getters:
2 | dart run intl_translation:extract_to_arb --output-dir=lib/l10n lib/l10n/L.dart
3 |
4 | ### Generate l10n Dart files from ARB files:
5 | dart run intl_translation:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/l10n/L.dart lib/l10n/intl_*.arb
6 |
7 | ### Run Flutter testing with code coverage
8 | flutter test --coverage
9 |
10 | ### Generate coverage output (requires genhtml)
11 | genhtml coverage/lcov.info --output coverage
12 |
--------------------------------------------------------------------------------
/xl10n.yaml:
--------------------------------------------------------------------------------
1 | arb-dir: lib/l10n
2 | template-arb-file: intl_en.arb
3 | output-localization-file: messages_all.dart
4 |
--------------------------------------------------------------------------------