├── .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 | --------------------------------------------------------------------------------