From f3f8312c5896794bf1e6132b02ac74213b350e35 Mon Sep 17 00:00:00 2001 From: stonegate Date: Sun, 9 Feb 2020 20:29:09 +0800 Subject: [PATCH] first commit --- .gitignore | 37 ++ .metadata | 10 + .vscode/launch.json | 13 + android/.gitignore | 7 + android/.project | 17 + .../org.eclipse.buildship.core.prefs | 2 + android/app/.classpath | 6 + android/app/.project | 23 + .../org.eclipse.buildship.core.prefs | 2 + android/app/build.gradle | 80 +++ android/app/src/debug/AndroidManifest.xml | 7 + android/app/src/main/AndroidManifest.xml | 33 ++ .../com/stonegate/tsacdop/MainActivity.kt | 12 + .../res/drawable-hdpi/ic_stat_forward_30.png | Bin 0 -> 1182 bytes .../ic_stat_pause_circle_filled.png | Bin 0 -> 847 bytes .../ic_stat_play_circle_filled.png | Bin 0 -> 955 bytes .../res/drawable-hdpi/ic_stat_replay_10.png | Bin 0 -> 1165 bytes .../res/drawable-mdpi/ic_stat_forward_30.png | Bin 0 -> 666 bytes .../ic_stat_pause_circle_filled.png | Bin 0 -> 492 bytes .../ic_stat_play_circle_filled.png | Bin 0 -> 527 bytes .../res/drawable-mdpi/ic_stat_replay_10.png | Bin 0 -> 649 bytes .../res/drawable-xhdpi/ic_stat_forward_30.png | Bin 0 -> 1423 bytes .../ic_stat_pause_circle_filled.png | Bin 0 -> 989 bytes .../ic_stat_play_circle_filled.png | Bin 0 -> 1127 bytes .../res/drawable-xhdpi/ic_stat_replay_10.png | Bin 0 -> 1339 bytes .../drawable-xxhdpi/ic_stat_forward_30.png | Bin 0 -> 2591 bytes .../ic_stat_pause_circle_filled.png | Bin 0 -> 1812 bytes .../ic_stat_play_circle_filled.png | Bin 0 -> 2114 bytes .../res/drawable-xxhdpi/ic_stat_replay_10.png | Bin 0 -> 2432 bytes .../drawable-xxxhdpi/ic_stat_forward_30.png | Bin 0 -> 3051 bytes .../ic_stat_pause_circle_filled.png | Bin 0 -> 2097 bytes .../ic_stat_play_circle_filled.png | Bin 0 -> 2488 bytes .../drawable-xxxhdpi/ic_stat_replay_10.png | Bin 0 -> 2899 bytes .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3355 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2059 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4640 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7304 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 10470 bytes android/app/src/main/res/values/styles.xml | 8 + .../main/res/xml/network_security_config.xml | 7 + android/app/src/profile/AndroidManifest.xml | 7 + android/build.gradle | 31 + android/gradle.properties | 4 + .../gradle/wrapper/gradle-wrapper.properties | 6 + android/settings.gradle | 15 + android/settings_aar.gradle | 1 + assets/listennote.png | Bin 0 -> 27533 bytes ios/.gitignore | 32 ++ ios/Flutter/AppFrameworkInfo.plist | 26 + ios/Flutter/Debug.xcconfig | 1 + ios/Flutter/Release.xcconfig | 1 + ios/Runner.xcodeproj/project.pbxproj | 518 +++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/xcschemes/Runner.xcscheme | 91 +++ .../contents.xcworkspacedata | 7 + ios/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 122 ++++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 564 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 1588 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 1025 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 1716 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 1920 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 1283 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 1895 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 2665 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 3831 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 1888 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 3294 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 3612 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + ios/Runner/Base.lproj/LaunchScreen.storyboard | 37 ++ ios/Runner/Base.lproj/Main.storyboard | 26 + ios/Runner/Info.plist | 45 ++ ios/Runner/Runner-Bridging-Header.h | 1 + lib/about.dart | 29 + lib/addpodcast.dart | 265 +++++++++ lib/audio_player.dart | 504 +++++++++++++++++ lib/class/audiostate.dart | 37 ++ lib/class/downloadstate.dart | 18 + lib/class/episodebrief.dart | 27 + lib/class/importompl.dart | 18 + lib/class/podcastlocal.dart | 10 + lib/class/podcastrss.dart | 83 +++ lib/class/podcastrss.g.dart | 66 +++ lib/class/podcasts.dart | 112 ++++ lib/class/podcasts.g.dart | 98 ++++ lib/class/searchpodcast.dart | 54 ++ lib/class/searchpodcast.g.dart | 51 ++ lib/class/sqflite_localpodcast.dart | 451 +++++++++++++++ lib/episodedetail.dart | 533 ++++++++++++++++++ lib/episodedownload.dart | 282 +++++++++ lib/episodegrid.dart | 323 +++++++++++ lib/home.dart | 50 ++ lib/homescroll.dart | 323 +++++++++++ lib/hometab.dart | 157 ++++++ lib/importompl.dart | 31 + lib/main.dart | 35 ++ lib/pageroute.dart | 60 ++ lib/podcastdetail.dart | 70 +++ lib/podcastlist.dart | 193 +++++++ lib/popupmenu.dart | 112 ++++ lib/webfeed/domain/atom_category.dart | 16 + lib/webfeed/domain/atom_feed.dart | 77 +++ lib/webfeed/domain/atom_generator.dart | 19 + lib/webfeed/domain/atom_item.dart | 66 +++ lib/webfeed/domain/atom_link.dart | 32 ++ lib/webfeed/domain/atom_person.dart | 17 + lib/webfeed/domain/atom_source.dart | 21 + .../domain/dublin_core/dublin_core.dart | 61 ++ lib/webfeed/domain/media/category.dart | 24 + lib/webfeed/domain/media/community.dart | 34 ++ lib/webfeed/domain/media/content.dart | 56 ++ lib/webfeed/domain/media/copyright.dart | 21 + lib/webfeed/domain/media/credit.dart | 21 + lib/webfeed/domain/media/description.dart | 21 + lib/webfeed/domain/media/embed.dart | 30 + lib/webfeed/domain/media/group.dart | 40 ++ lib/webfeed/domain/media/hash.dart | 21 + lib/webfeed/domain/media/license.dart | 24 + lib/webfeed/domain/media/media.dart | 169 ++++++ lib/webfeed/domain/media/param.dart | 21 + lib/webfeed/domain/media/peer_link.dart | 24 + lib/webfeed/domain/media/player.dart | 27 + lib/webfeed/domain/media/price.dart | 24 + lib/webfeed/domain/media/rating.dart | 21 + lib/webfeed/domain/media/restriction.dart | 24 + lib/webfeed/domain/media/rights.dart | 18 + lib/webfeed/domain/media/scene.dart | 28 + lib/webfeed/domain/media/star_rating.dart | 24 + lib/webfeed/domain/media/statistics.dart | 18 + lib/webfeed/domain/media/status.dart | 21 + lib/webfeed/domain/media/tags.dart | 21 + lib/webfeed/domain/media/text.dart | 30 + lib/webfeed/domain/media/thumbnail.dart | 24 + lib/webfeed/domain/media/title.dart | 21 + lib/webfeed/domain/rss_category.dart | 18 + lib/webfeed/domain/rss_cloud.dart | 29 + lib/webfeed/domain/rss_content.dart | 30 + lib/webfeed/domain/rss_enclosure.dart | 19 + lib/webfeed/domain/rss_feed.dart | 108 ++++ lib/webfeed/domain/rss_image.dart | 21 + lib/webfeed/domain/rss_item.dart | 66 +++ lib/webfeed/domain/rss_item_itunes.dart | 83 +++ lib/webfeed/domain/rss_itunes.dart | 70 +++ lib/webfeed/domain/rss_itunes_category.dart | 24 + .../domain/rss_itunes_episode_type.dart | 19 + lib/webfeed/domain/rss_itunes_image.dart | 14 + lib/webfeed/domain/rss_itunes_owner.dart | 18 + lib/webfeed/domain/rss_itunes_type.dart | 17 + lib/webfeed/domain/rss_source.dart | 18 + lib/webfeed/util/helpers.dart | 28 + lib/webfeed/webfeed.dart | 13 + pubspec.lock | 446 +++++++++++++++ pubspec.yaml | 94 +++ test/widget_test.dart | 30 + 163 files changed, 7568 insertions(+) create mode 100644 .gitignore create mode 100644 .metadata create mode 100644 .vscode/launch.json create mode 100644 android/.gitignore create mode 100644 android/.project create mode 100644 android/.settings/org.eclipse.buildship.core.prefs create mode 100644 android/app/.classpath create mode 100644 android/app/.project create mode 100644 android/app/.settings/org.eclipse.buildship.core.prefs create mode 100644 android/app/build.gradle create mode 100644 android/app/src/debug/AndroidManifest.xml create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/kotlin/com/stonegate/tsacdop/MainActivity.kt create mode 100644 android/app/src/main/res/drawable-hdpi/ic_stat_forward_30.png create mode 100644 android/app/src/main/res/drawable-hdpi/ic_stat_pause_circle_filled.png create mode 100644 android/app/src/main/res/drawable-hdpi/ic_stat_play_circle_filled.png create mode 100644 android/app/src/main/res/drawable-hdpi/ic_stat_replay_10.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_stat_forward_30.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_stat_pause_circle_filled.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_stat_play_circle_filled.png create mode 100644 android/app/src/main/res/drawable-mdpi/ic_stat_replay_10.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_stat_forward_30.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_stat_pause_circle_filled.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_stat_play_circle_filled.png create mode 100644 android/app/src/main/res/drawable-xhdpi/ic_stat_replay_10.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_stat_forward_30.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_stat_pause_circle_filled.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_stat_play_circle_filled.png create mode 100644 android/app/src/main/res/drawable-xxhdpi/ic_stat_replay_10.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_stat_forward_30.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_stat_pause_circle_filled.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_stat_play_circle_filled.png create mode 100644 android/app/src/main/res/drawable-xxxhdpi/ic_stat_replay_10.png create mode 100644 android/app/src/main/res/drawable/launch_background.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/main/res/xml/network_security_config.xml create mode 100644 android/app/src/profile/AndroidManifest.xml create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/settings.gradle create mode 100644 android/settings_aar.gradle create mode 100644 assets/listennote.png create mode 100644 ios/.gitignore create mode 100644 ios/Flutter/AppFrameworkInfo.plist create mode 100644 ios/Flutter/Debug.xcconfig create mode 100644 ios/Flutter/Release.xcconfig create mode 100644 ios/Runner.xcodeproj/project.pbxproj create mode 100644 ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 ios/Runner/AppDelegate.swift create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 ios/Runner/Base.lproj/Main.storyboard create mode 100644 ios/Runner/Info.plist create mode 100644 ios/Runner/Runner-Bridging-Header.h create mode 100644 lib/about.dart create mode 100644 lib/addpodcast.dart create mode 100644 lib/audio_player.dart create mode 100644 lib/class/audiostate.dart create mode 100644 lib/class/downloadstate.dart create mode 100644 lib/class/episodebrief.dart create mode 100644 lib/class/importompl.dart create mode 100644 lib/class/podcastlocal.dart create mode 100644 lib/class/podcastrss.dart create mode 100644 lib/class/podcastrss.g.dart create mode 100644 lib/class/podcasts.dart create mode 100644 lib/class/podcasts.g.dart create mode 100644 lib/class/searchpodcast.dart create mode 100644 lib/class/searchpodcast.g.dart create mode 100644 lib/class/sqflite_localpodcast.dart create mode 100644 lib/episodedetail.dart create mode 100644 lib/episodedownload.dart create mode 100644 lib/episodegrid.dart create mode 100644 lib/home.dart create mode 100644 lib/homescroll.dart create mode 100644 lib/hometab.dart create mode 100644 lib/importompl.dart create mode 100644 lib/main.dart create mode 100644 lib/pageroute.dart create mode 100644 lib/podcastdetail.dart create mode 100644 lib/podcastlist.dart create mode 100644 lib/popupmenu.dart create mode 100644 lib/webfeed/domain/atom_category.dart create mode 100644 lib/webfeed/domain/atom_feed.dart create mode 100644 lib/webfeed/domain/atom_generator.dart create mode 100644 lib/webfeed/domain/atom_item.dart create mode 100644 lib/webfeed/domain/atom_link.dart create mode 100644 lib/webfeed/domain/atom_person.dart create mode 100644 lib/webfeed/domain/atom_source.dart create mode 100644 lib/webfeed/domain/dublin_core/dublin_core.dart create mode 100644 lib/webfeed/domain/media/category.dart create mode 100644 lib/webfeed/domain/media/community.dart create mode 100644 lib/webfeed/domain/media/content.dart create mode 100644 lib/webfeed/domain/media/copyright.dart create mode 100644 lib/webfeed/domain/media/credit.dart create mode 100644 lib/webfeed/domain/media/description.dart create mode 100644 lib/webfeed/domain/media/embed.dart create mode 100644 lib/webfeed/domain/media/group.dart create mode 100644 lib/webfeed/domain/media/hash.dart create mode 100644 lib/webfeed/domain/media/license.dart create mode 100644 lib/webfeed/domain/media/media.dart create mode 100644 lib/webfeed/domain/media/param.dart create mode 100644 lib/webfeed/domain/media/peer_link.dart create mode 100644 lib/webfeed/domain/media/player.dart create mode 100644 lib/webfeed/domain/media/price.dart create mode 100644 lib/webfeed/domain/media/rating.dart create mode 100644 lib/webfeed/domain/media/restriction.dart create mode 100644 lib/webfeed/domain/media/rights.dart create mode 100644 lib/webfeed/domain/media/scene.dart create mode 100644 lib/webfeed/domain/media/star_rating.dart create mode 100644 lib/webfeed/domain/media/statistics.dart create mode 100644 lib/webfeed/domain/media/status.dart create mode 100644 lib/webfeed/domain/media/tags.dart create mode 100644 lib/webfeed/domain/media/text.dart create mode 100644 lib/webfeed/domain/media/thumbnail.dart create mode 100644 lib/webfeed/domain/media/title.dart create mode 100644 lib/webfeed/domain/rss_category.dart create mode 100644 lib/webfeed/domain/rss_cloud.dart create mode 100644 lib/webfeed/domain/rss_content.dart create mode 100644 lib/webfeed/domain/rss_enclosure.dart create mode 100644 lib/webfeed/domain/rss_feed.dart create mode 100644 lib/webfeed/domain/rss_image.dart create mode 100644 lib/webfeed/domain/rss_item.dart create mode 100644 lib/webfeed/domain/rss_item_itunes.dart create mode 100644 lib/webfeed/domain/rss_itunes.dart create mode 100644 lib/webfeed/domain/rss_itunes_category.dart create mode 100644 lib/webfeed/domain/rss_itunes_episode_type.dart create mode 100644 lib/webfeed/domain/rss_itunes_image.dart create mode 100644 lib/webfeed/domain/rss_itunes_owner.dart create mode 100644 lib/webfeed/domain/rss_itunes_type.dart create mode 100644 lib/webfeed/domain/rss_source.dart create mode 100644 lib/webfeed/util/helpers.dart create mode 100644 lib/webfeed/webfeed.dart create mode 100644 pubspec.lock create mode 100644 pubspec.yaml create mode 100644 test/widget_test.dart diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae1f183 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..7ec1395 --- /dev/null +++ b/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 659dc8129d4edb9166e9a0d600439d135740933f + channel: beta + +project_type: app diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..3287bb6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Flutter", + "request": "launch", + "type": "dart" + } + ] +} \ No newline at end of file diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..bc2100d --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,7 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java diff --git a/android/.project b/android/.project new file mode 100644 index 0000000..3964dd3 --- /dev/null +++ b/android/.project @@ -0,0 +1,17 @@ + + + android + Project android created by Buildship. + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..e889521 --- /dev/null +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir= +eclipse.preferences.version=1 diff --git a/android/app/.classpath b/android/app/.classpath new file mode 100644 index 0000000..3589094 --- /dev/null +++ b/android/app/.classpath @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/.project b/android/app/.project new file mode 100644 index 0000000..ac485d7 --- /dev/null +++ b/android/app/.project @@ -0,0 +1,23 @@ + + + app + Project app created by Buildship. + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.buildship.core.gradleprojectbuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.buildship.core.gradleprojectnature + + diff --git a/android/app/.settings/org.eclipse.buildship.core.prefs b/android/app/.settings/org.eclipse.buildship.core.prefs new file mode 100644 index 0000000..b1886ad --- /dev/null +++ b/android/app/.settings/org.eclipse.buildship.core.prefs @@ -0,0 +1,2 @@ +connection.project.dir=.. +eclipse.preferences.version=1 diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..ab03495 --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,80 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + + def keystoreProperties = new Properties() + def keystorePropertiesFile = rootProject.file('key.properties') + if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + } + +android { + compileSdkVersion 28 + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "com.stonegate.tsacdop" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + buildTypes { + release { + signingConfig signingConfigs.release + } + } + +} + +flutter { + source '../..' +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..35c9116 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5e27fd6 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/stonegate/tsacdop/MainActivity.kt b/android/app/src/main/kotlin/com/stonegate/tsacdop/MainActivity.kt new file mode 100644 index 0000000..615f563 --- /dev/null +++ b/android/app/src/main/kotlin/com/stonegate/tsacdop/MainActivity.kt @@ -0,0 +1,12 @@ +package com.stonegate.tsacdop + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugins.GeneratedPluginRegistrant + +class MainActivity: FlutterActivity() { + override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine); + } +} diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_forward_30.png b/android/app/src/main/res/drawable-hdpi/ic_stat_forward_30.png new file mode 100644 index 0000000000000000000000000000000000000000..f9c251c5fdd0cf17002e56378859eb75a7306122 GIT binary patch literal 1182 zcmV;P1Y!G$P)LZiZpMk#2b2#U&Rpp?K5Sy9#plMqX@D9MNn8%#?x3YsJeijb^G zBZDHEh(f~%Bf|F~nx#2QcdXb4&O6+5&$-w4zzq-Xx%Zs?UwiMh*WTxeZOpWd;bF)C zMa1*Kb&`JFnjnV~&_BR)z*I@U_X}z$0sRYX2RsPOmGnzLpoRmGaV!Pyko0}8poR;O zaXby&Ea|&mKn*9LEx_ZztX_!TGoXmr0oVuF7dRdm3+xSC4;{O90dSY3jcqd36QGDV z61W%`51a~&YI6v%1ehu5ucrH30g8z2fy;quz_Gy2O^z$S-wG^{w5jR2MnDm<6EGXN z3D~XT@JrwwU;wxs7}4Rt8-c}=tW%BF6;SeJx?3376$rpwSd|ffSey_Mg$@;8{R!L) z%$MZPt*i;qB;XNXuk`i>a1Zc;q(90hbO1Cj!Sp1MwSXexC}1TpCJ+7^m<+rxX>&E) z3ZPjQ$9_ggM?ewb5ny36k!>#1*^)k~%XvyLCxK zm?sYbbJ}!MU0`*9+}lRNj|>t0etaA+fb+ zQ^BW*7$Ip(%Eab=fz<&rhzWqnyHL{1QhR$$xYN!7Jk4FsR{-mk$?VSbcyY#_kmQoL zwmLv>BPO11(hESsavg-&Y6d zO?MSwKqjf9{of+OgLOY(ByetK<>b8fayG>)M?`pQoGoc_>DfzxYZCBUNuB_MtPao$ z;G!&q~*c9u{_uu*fWx@t_!3I(4Oh=ywn*x`~bKg@N#Bg zgI1B;6wJMoQZ!G?fGh*nx5<)z>=%#$AD!dDEncMD>xjF^>&REY&w0QefC+s9a5~_C zYzF;0;NAMOZrN?KQ~Mqh)vD@&TQ<)r|8wE*{L*~!-fiJ4o8Qko^EP+@cjH|npb9Ts zx<%RYofi7??LOG6%M3}cHIcs+sXIA|2$$i4d_OxRGvc9X^J|WE%j>Q;fL9Ai7&btK wOV07*qoM6N<$f+ChGIsgCw literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_pause_circle_filled.png b/android/app/src/main/res/drawable-hdpi/ic_stat_pause_circle_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..0668dbe757a7fdb316cea2b5cd33c55c7743d6ca GIT binary patch literal 847 zcmV-V1F-ywP)C0W3dlLv1DFdO0>%OQA6n}GfxHE_1NURVLPBxQ31Bm@ z66hOy(vo@7+9lv9@W#vpn>s22d9BDayMSpC8VRuq;u)|HxM^m8(iWrwd6-qeUSL>g zrz+on0(Jvu)1WE=Ee1~30VdBbs8-;5srkx)yw9`))2lprwbNz3wPyA@xv&Vxo9Yy> zz6L%M0+dgk(_Tr_l>*HNuKh;rqt-y$PxWqAK|{{DdB8znVu%K) z2T+HZO-KT5@bzj;Hvzpjv(ZVQoxnOkkr5j8(9lOS8{W`B8@#*LIy|HY%wh zW#|KqR2&002ovPDHLkV1mz;gdG3? literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_play_circle_filled.png b/android/app/src/main/res/drawable-hdpi/ic_stat_play_circle_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..b5134122aa1fdbfa6493322856a52b2caac055e3 GIT binary patch literal 955 zcmV;s14R6ZP)#r5Wast|U@UD zexXGlE^2R~BSPpb+L)U@7J|A^ANgO;9o~EI``-7Qxp%;`o4L<9&+k0n=QZxLW%n5m zNkHx{`Po1x@B%OrcocXzT@dJ#eDleGNb%W_o~`i8oIb zm;1WR>_T>94N!2^K45hlVx|mGG4-`{WnEVf^g3`XGMoMbh5`?jX>RiS4PddEsa{o9 zKTs%o=YZ*v3XX?V-U~btxhGFdb*iIkr7X}Zz!~7d$iu${RsmyyZeSKLByw+7OzFSC z%zm!7kgD*egxIM7nb|dWA0M)&YXH_#EbTTk72kUFeBlh8lV8B9`_9JMSv@9O+jC;nLVEcS`+%!v#kJC_qzL&z(>Fe zV0grtm(6TS7U+Wzpt@b`YOqjtqq}P?5zsbZM9HClnc2j_22#c?3nNQ1(Awa#irFGp zp@rOCgPW}Wc7%1LuFuRWiBQclRp{+)3hD{K=fJ{*64DUV3c^DzroGyTvQ=sST%?t& zV!xT03Uf{C*a>V%xK*|=J`Jo3iJ{A-EEUFXGkd2VXbzzAo!UA^tI~$yUzwGtl`&GjJgCGb?th60&Oz|@B3Dq(kj1NaVjEJDFQVf=U`LbgccCa}cJ zPF4l-2DGn$l?k!b0uSG)G!zbK!Ac##e&D56G)3(FBaE z1YyorOl<_-3Lh2*Vl8Ep3aN^(+C=(OI;+*)Wi3sEJHj*=`>{ca`+)a>(?!qcra|58 z`T$)%?`Ezk(iJ=N drm(godlzwtAT$D@P=^2j002ovPDHLkV1oNXzH|Tp literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_replay_10.png b/android/app/src/main/res/drawable-hdpi/ic_stat_replay_10.png new file mode 100644 index 0000000000000000000000000000000000000000..d2ad64dff8c16dc32ed2d6d74a19987cbe762c9e GIT binary patch literal 1165 zcmV;81akX{P)x>`Kz88D>UJL&zXgwn(8Q zTO@=T$-Xl)mZLLW@AQ1{eDC)@-+YsA?!VsUxv%Hk*LALQu3M~TORE_kMhwXIy1)$J z6iNS#^nfD*WP2QNA8;Blc2oh`ULUvyxU8}=X4C-L-UPS~xS&y@(E((8Q{YzMj4r_y+h^($e8{UaKy%y*4lnxTt0DKY-VO z1;BhsAGF-pPKRud1ug_`gLZogz+&M2l7`um{^|#;RfZD8_88zC;NBiVZveIe4ghxQ zdgeFaZs2A~e+~tzHJ}2u4sdC4DWce1%e&0>Uckk`p}@wK)d0C3xJ%NKUZ92rs6a&! zBZgOZm2W`Xmx@mSt^{@|06zogOPaIF02QeC*V(|G4NxoSknKprKEP~XhXU~>aG0cY z;tFfn8R|qRPMuraNSvO+XnQx{MIfS@^$2jXq`$intu&wp(Zh@IYFsZbt1gXxT^r!Q z=fDY)-dLr88lY|rSB5fI+bP{FU}qo%pDHPxH(-r4Lu;0WEK9v47nzqx`nUNe+w0Zb zbbRq{x}?a+$N|~j8aNsl56qM_&vx27rh6ceLG3Ewv4CvwE)x3}Z`1!z%uS0;g2#D~C%lHO|UjTa`10raw@WV3(CY|pQ9B=yrIMcf7~+ac#W z4M4vGbL;z3+n>~&9g|(QY-gxTtYm+Iq$qhSKo8cOocibUnN2QK@P2?k0#1_jW&_al zDruVOVM(X91r)uAHKaTVII(+#7$idzfg@`wh*e}VI7U)Rne6OXiXE<{17oy#N2?4a zl{0}YfH?J(>Zaw_9NXIgj{-MIdffJAz#YJku z{Q%|vT}xe$Xr%ln;8h@^_h}J1@x!6*fjxo!Yg)|j0sSm+1@PWb+3jntmHH@i#)qhI zb4AI21v(>X)O&*xzUFfe)S{w9xR=6x}&&^JSa zkTtwIyhJcsl#IlivT`YmZhZzk1w1Y3hqhqy+^_+4f>$ZSuicBp`76#Ie;bk~LjhcA fK*KrpaGw1SqV%RTpMf-$00000NkvXXu0mjf=*J}U literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_forward_30.png b/android/app/src/main/res/drawable-mdpi/ic_stat_forward_30.png new file mode 100644 index 0000000000000000000000000000000000000000..8f07c28d680d1e212d7d36f89dfcf8ad19c81fc4 GIT binary patch literal 666 zcmV;L0%iS)P)&35XvPoOehq}Z9!H_Bs-S|DU(7JYIdyH zAqyr9#?e#%w_fjj@Atjm$k~10|NnoUbIx;~^NRl*Y8F2t+9Y*WeK!z-K46ukdsRpV zN-zj$mGrO?$-fX7d!eK!^++laM1=1x0;U1yf$ZHcV40+@GLpIk5iu3G2+RUTmVo>K z#!LE8>%5d8BK83XfT8K(8=xClo2$S_`z0N&TtRBbPT*()x&yRJdTx-2@Dy2O$014W zO=%DjGk}-r(K%qNq~94xj^LQ2JxxtfL<|Am0pozjz+y>}2T24cB<*Sh?(@Gih=?h` zYrq+|NcvU;iHIYTtWTvV5^Mx61D_;~&b4JY(3`a?B8E8$Nk1F06bX(47WlrT6-iVR znqX}5Knt+WNdTuMJ*zBoM4SY+1GgltNd%*Sxxhi-tY;p$4NPz{l9r@s^^~o>(kH1q8PjHS3%^?{ zNndIbq{r)l8|gwT&{gz8u65ZGe3bs%An9se^nSZ2BD@A{vU%PNKu2D*g6UkL_mFYy z{yBNq_cK~9GxyS8ELY(8n6xpmV}9DOW72O|u(Xyh>TOe>Z3aAk)}&T^1-1c~ikX*c zP(+Cc`=Bjl?b&yDUp)mnCE0MLQR$0T51`T6A5g&G85hB~;{X5v07*qoM6N<$f=SpX A2mk;8 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_pause_circle_filled.png b/android/app/src/main/res/drawable-mdpi/ic_stat_pause_circle_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..c20d52ecd894f7e85f111ff4649fe3a0f51ca578 GIT binary patch literal 492 zcmVS%$1-5__NtV({0?e!p z_zt{#B*=YWT+&{pefI!n_5zpzJ_8R%SSC2{XCz%`-01Q+1V#%6tVkM;fK_0q zV8EKB!RG|5OB#rPWniS>2ue~k9{NrtM&4UADQO`_&;%S696`IJ{jAa4dEPBOouRkIb8#m#?P`{bmmvcniD=bvw@t i{exUXzsmF^xBmlcg47vS_G1$O0000s!CQ?&6$9dG^iBzh4EaKre79=pO>VfL&l;lBMh< z0cKVKmVw~{C-;CUN#C03odB3w2k-)T4&-{+U~t~gNcx+7|DS2BZ(|A1gQqt4-a101B|Jq_$QvWg{$!#6QI8@7*oP_=cc7Qu2y{iEgudWE_1AgWl zJ^)>fQjifeU3qG!^Df zA;8Qoha~$oPaaEJh#vF$rw+ZoiHkusQAynR(?#YuDAxnT) zPI}>F4Vsy+%2iKR0mu{TE3+Q+Mk%xOuesa66`(8R?N4Uue{XB)w-}zJ`aicJq8VIU Rkg)&&002ovPDHLkV1gStv=u$N=OWZfl{tQ2GWfvnaIRYQDQ(wTyov(Ia9>!PNk$|CW%BckVm)V zQN#qvgyhxzw6u=S`TL(&U3+pmXYaLsd#!JM`-uO%)J}fe>wp`QhFXnl8DM)2a1-bl zc7W|wz}>v{@B(ai0S~HLGJF8rD}md@$<4H=tAXt)z&l`cog`m@hrmur zgY`fSMYg>kI9Uk30xSj=*YS4)6|oB$3KeUOk_z-r(lFk4cz?ayi%kdUK5Z_#MB#{pk} z5x^oz&)Wvr?vxah6D=DH{HO-meh2kX-5Wj0CRvrNfT{<24(^AHcgOpG0$BBwgBfPg&F_VAa8dLcq3_E z<&f=}z>5TY2c{)plB5d>cnQo0_DPD#Dg`V79s|Gfc9AT4UJVFSJq6YReUe_;9+M)E z1ZqaLW{PZ&2R;Cuz$u_7jFX~nkQ6Qs;CkS-q-Qy%htrJTLTE|R#_}<>y&1Ti`_bpn zqx4bB$@5z)nlu8$EZ+l`=MxZZ=&J-&f7FNg^Zlx z(9kBpu~q;}f$8}@D(J3ua8n;-`7yF*Q~^v+(a3Z68mS00000NkvXXu0mjf@C+gt literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xhdpi/ic_stat_forward_30.png b/android/app/src/main/res/drawable-xhdpi/ic_stat_forward_30.png new file mode 100644 index 0000000000000000000000000000000000000000..ef4d6b058f141e1a72e73ca689186ce4cd8275b9 GIT binary patch literal 1423 zcmV;A1#tR_P)AZN>Yuk9>Z8=X}FC|1Yk)|9pGzwVpk!{jU9so!qpO$4|cl zM8q&jKlax|4Ftd@;8;mt_6Nj30qhHm>JNy41n>eJDrrM6S7M+6HUr~(xe@~n;K~0M zI7QOuy;zAI0pPg52F{T5X}1tN3czuH15A44g1Bi(Ify06G zfYX5^fdhacCBgmSeoeDeR{#;={toa;#Oc@funzbM}Ws9tGmdpxB65Dh={9zCkveU99${s zlcGR60Nf+#=5B(oyaEw%4Ddd%YsR6Md@*pDq`%t*(gt9GByYv8t#kzKa$nRw<@O=_%t(ay>T2L^{djlr|V^YQCL%!WBL_|yi zjsl#1-|DswXvI(l@Efp4Cg7=(mSh2vB7M)g9{3&bov;sZmZaC*HX@$)eUU0L%ZmMg z>5_^r###$l2Cy}EfXC)<+=w^~_!!tEX?R3fnOa(`k#t-FFqrFtAwCNc;c-_=nphNV znv`vyI-Q>~0IS1no6-5|DFwWjCtA|xbn!XBwUWjs0B@TcB|VV-w(fTw(Fc@U(cOUc zIWQMIo`nBd8Ngq;ifXc?ciYU`up}*9g^dR8lH~O1E#x4#>HUasJ$$h1hZkO9~t$w{Kn$^gFdYg(q~!;-G+ zbjl)P3h+9xT9T`^PAf2|U<=&~%*^=MC}~7ffOi0=zm~0&l$8|@&Z+Sg;69gx5#emK z!rP$LHX^(gmr0tI4ya!vHnh%2?hIJ<_JJo`CFz8w05gC`Go8JcCrNUEeHIZubbdZ} zy0t!X3biej zu4#dD6U1^!&imUeA|?ZK0AG&JNP0CNR76;|pPKpInP+Gx=~6|Fz6fOHgIB?`;()ia z^ir|4iS9`i-xImCy+qQ~cC+u_s{lCN`5`uYBl|IGT(2HzBjOOi@1$8DJ~v~^Ub0I8 z(jLAOxFa_cmKnYmyk{F*L|8wq%xzPTSE=l*3Sb$q6gV>%sU^eJlAdlt5D}LH?whT( zJ(Ta0vnBmmHf|+=vD01||s6IUg@Q#wO68aVg~l6=Wk zw%Py*!jjoDeQjk=og2C?m?6n3ww5&lXnBYTt4<5ALS0`W5TnRM;{- dZ?e+^{0ELK6HkGrM5O=#002ovPDHLkV1g=7lJ)=q literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xhdpi/ic_stat_pause_circle_filled.png b/android/app/src/main/res/drawable-xhdpi/ic_stat_pause_circle_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..b498a334d8440653699b344eda5fec602218507e GIT binary patch literal 989 zcmV<310wv1P)PlArQ@OaU%(1UH)8Zg z0hrmF!27`57$XsVZv$hJ_GXpI1YlxLaHr5GV;Dtl=XR-;%Va zmF|`RN!(v8aJPx&^xdgX()Cbq20(I1T`6Fnk99ii?*saZi$z*5~v(1o*fj0HcyV zxbFZeA40>x;fjRNYmyFzA@me*rXm2(Njen*EC&9r2*9I~yvo;HS~Rl@IWp0*53=9A zd?~3pm2fZ2Y&5C+#7;z+C-FOy-cHL#p8%#}y)QYH7?5->4Zv%jzxbsg62dPu2Lc%{ zgf+I!!M9&%yeJ&g{z$^7eywbErR*lDi*Y7T-$6+y8wJxY^`5Hs<*`tpj8`PQGT{}9 zdP^&-EJ(K0IR!ctquN7eNvnsEc7?57zjoqV_1m$qP|7jY{AGh66yy@0<7_pzz@JaNk5mGxGVrOI|^6;+zi|cT+%VIJ-{o#`@lDn z{_SWt<*_6HGdmi%6nF`^C^dMn+Z*6_q=57WFtd{Y4|-;1;<5RU!1fac`mx;BDZp!ZX{~-8M;!n~WL(!h(IeZ?p+Lzedudb-rr> zLf!u|!QG}Wr|(X)CH)o~P5}sq)PV`?^L>+!pZ5u!Bk7;&4|MepPbB02cx-v{J`@Nv=jQvqu15c*mzY zYp}aa(pQ7uVt|K%Co=*L0RqIez>B~+89rmvksT=paI&tFxGtv=fB6E(fI-G)_7Lzma7+P;d6Evs09OK^XF6{L2ybH+@EveMW}NFJeHsJY z4ZM}*8wQRp9KNaaT%u;{O0}c>tJS0$@5z$h#T$0na81#@Z5E0{G&}8LcH0l3U})XIwAolNjI(;HMm~;{eReOXm5+l8MtgOOki_ zMq7(!wmUbm$Yv1|FM|6 z7}%8&tSYDdpxRuZ-s5x@O8T-1z)!`m{nC&K;TI0?nfi&oG||U6v+qOs46l`SSIQhw z=W5o~=BX1%-d!$|^l{a9)1}^}>g)+6k?Phv65g5c4#jXZwN*|DTk5f?(F)sn$;_?$ zFfy*OwF=l+IFVF$8zil+Gj3}wz6acr8nuw!N0M%A^3?|59fc17m%WE$d;#1b>5n7; zH!?F{5APlZ2v2^oByW~Aj@Iu30m9#eUpJ?#oIV8Z7&r5^2U%N0n@-i&_}E;)RkKd{i(sAvFl$Ohkt4S t931HRKFJnY1Gxc9+ZTHSr4#Gw?>`QJRX}@gfl2@X002ovPDHLkV1mqF7nuM6 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xhdpi/ic_stat_replay_10.png b/android/app/src/main/res/drawable-xhdpi/ic_stat_replay_10.png new file mode 100644 index 0000000000000000000000000000000000000000..ff8dfc5ef7920cdd939c33047f8f2f29490269f2 GIT binary patch literal 1339 zcmV-B1;qM^P)D01>eT@J->w`v)K*wg#30oAez(L~IAl z2dq9t8TmI55jy~{0{$(_$OcH8e+}5a!qbdgfQZ-@SOjd@#!Ze)fQZ-%_ypLngO6VW ze4C8>CEyugnWV3K5gQr>vWW1B4|W4D0;d5hB}G%1rT}LG_cvX%Fzzp432=a<-#FeIxrg83pftgv(5Xnf!W=tRAYeF zCL-c;;QG2=C?X7UHgFNJUS`~Nz*Ti21`UvI;~Fmt+4MZ?Mk8V;-~r%}48#mcch^y; zo&Y2WU;mXEh{`w+5$ggE0jHM6{Q*ppej>&;Q+a!lpH=O>BRp9ppys5J&%Y-l0ml~ zFPF6QumD7aGt)hQagv^HEt>wH4D1U00K8C@jYh;w;MT&1S4nbvX6RHXB5Zh$0p|iX z#(9#a769G{c2DWIGg+(3-jbG;cNGx_11}f;Un9w>NcR9K-x(NK72YFaI&ddopY6p4 z03u=(;F{nDNtZ>$DZrz^^OBCr0Bj0;U;2NvB+s#yv^C<0_@Wdc9auikENa#9sm^jT zRs#4em77OPdMkCc&=SLrs=ED7H(-M+_IS>cR9VRM2;gMku>yd5s~b190jrDqJuR6d z$!Ty_x&&A#X;S6}Zc3`ydbC1Px!3fsT~=)-UG*DIs0!~akurjBjRnKs9K98?cekU>|O3D40dR@Auq^Hn7e^=5N`C^8#$ud(2sG2Ch5cCJP~32 ze>XXV)}vXHDyzOuHekmq-18*`F$mQ*Uh9|10)Fdb9~bF`MnV5SJNf^+3jSF09uQr?wS*? zwQ`!88*wl@VKDA^PyhvxeVpsqBLV(u(@tEpJ75`naorwh)r36(D1dlA8~XTE1B^+L xb#?k6;=t!3z3~T)GDuGWYN}07U+a5-zX5b%^g#Tt5EK9a002ovPDHLkV1g7$Yw-X8 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_stat_forward_30.png b/android/app/src/main/res/drawable-xxhdpi/ic_stat_forward_30.png new file mode 100644 index 0000000000000000000000000000000000000000..425c29d7b01d5f02c96e6a5110cb495816117914 GIT binary patch literal 2591 zcmV+)3gGpLP)BU1a?u7vq5j<-nNt;Nzm!vyMy0xU6 zN!maXKwULKs=#OIQZxxNc~0$()2<&Jpe$cJ7!D)pzLIV(>4q}`G3h*{B{=SLryaWH z_k#iy5opZ8k{&Gyvl?2^q@8X5uCE964p2nkM{XzS8N5+R*Y9hyQ`WgCU&ev9`Fk(Y zD?kx(HA#=aA4qywFFPz-gzDpdlHOwbPi421UDX>v>4bNY^a@G0nZd_gD(PZL*UG3? zc7p$2#WN4H{kNK%O0Mk%pakdDl3r8cYhb}2lk_D?KaliCNoc}*ByCr+`Md^N@MhbW zSKL#4>D&NC#P#4IlJ=Ye&SjE*D(MtS@3;M}wq0h)m5SmtxkS=iB^}VG&uKn%E`TBe zf`?h5JvW+gBI-CFm2^xN&9Zqcy`fWU+fko0BS z|D3kjLV)&2pY!)=>&^T7nE{H3TT1#~b|AX;K_&1xk}fFia}@wYgnXOrf6aUNlKW=@ zDE-byCgeAY|?fWv-@H7FwPCh5!;a7LvD{12M+e1eu7S?o1AMNhzQaLCu;VaPd%Cd)MA;0K#d$q-CL>NP2o#c0Rg3%dbzAbjR#w zQ1!z!Pm*+Q^WYJ2ZGe|_?=0>E5Hx~>_sm&oM9E8>)d!t`BW`)XHzhr5Qb!>o@LOM# zbd$064yH-UKpkiY+YBcxOGHpBdU$p_m|H~PLcf>vTHEidJ?t#k*J#i;Bt5X@A)lEv zE|;kZ(T&1Ap!`-6=7$aOxvhqQ#xgTr{=`jW?5FHRfKFXLDkz0I-1!@>b=LrJfb#Mmzp4W~Q;h;chiiM8o-Eoo;&>>%kKlDL*3&&|8E zAR;LKABul$+1Jl_*(Fi51|TYf4{3QAsmG4CdFvA;BNGSYpd(=(`f%HT)h9r7a4~JF zo=ss68~-;|Y5@9RX6nsFsVZ)78y__dAl~Z!(1P?MIA#j(`;Ggsi0DIua0l?VwzABL zw$a?FQUlOF+16<;%CPq~w!b>`<8tjY@4p{ zRH*?7k_R_w3S|iZYWrR11|kAq^v*OlzRehM9ehoR2Ayhq^R5R(1R6qFxUKR4Z#!Zt zI+eNDtt7GWHc(Mz9Q757S`mRSI3%6&S0ud<$C@FS*3A+-x4pb>M9@@ZJcjEbkuYe0 zV?u-|hLhgS_F-*@%yfpz1GJziXzbl>pWQ1!xxsHs+PGz59PP%oVY&Y!$%7b|yPqVg z0e}JkM5#CCI7~ClQ&Qo82&Q+GBpQ?y<93roU3#Oo=jA>b&N?kYZf@y3I9X{`6KGfr*UL4qDK! zv`vw0St8=LlI}0*$r*{@LO;qh<%F&Renc>__FYNz?V3XFw}tKR35==UuM=h%JIRZB+ z1looZ=XXNy(qlSI^!wQ~Bo$bgL}3(5G; zuh~?U1|>k;vsFeGCR3Y2LQ1sbW4Nl#)NFkG+Vnf*ZLsDx$4u38XFjc}(Vm$!C_!Qq zOm*xu;ZW3l9y-KmZUY?VAqgDNhOQURH)oVhh@MqatwCcpPaZ|N6iVN<4keP8N;V|7RO^{rP#pe@_xXbpK? zm(YcwTr#sioEIRffaE+Rop+e9Q7VaW4;V3tX;JN{xn4xTkl|8vSa8M^;ktyh2zIf192~PY)Tnu>fHZ( zL&-skeP={$k$D##h_>;V;DR!C#Ro(6gtAk7EZ=z@QOHgOPR;Q4Z1vK<5onCRwNtDg+BI!w;Z9I0@q;jV3&k zH!*KSbfQhXB6Es^n^*~0tNEIBfTp~ib%3S-^dEj#igr6+y6FG_002ovPDHLkV1gjH B&TIex literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_stat_pause_circle_filled.png b/android/app/src/main/res/drawable-xxhdpi/ic_stat_pause_circle_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..163ea36ea349106d38d3b1a39bf5177faf552e19 GIT binary patch literal 1812 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 bGk|{pm!DCNWx3s#00000NkvXXu0mjf-s-^D literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_stat_play_circle_filled.png b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_play_circle_filled.png new file mode 100644 index 0000000000000000000000000000000000000000..e60a66e58aee040e5b9e2835d4361b817db39061 GIT binary patch literal 2488 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4b5a9b235930eeb5870bb38828b84dc60a7eecf1 GIT binary patch literal 3355 zcmdT{=Q|q?*N&AYv13z}_$9S}Mp`3AjUcgV6V#@P+O=0&rBT{YRn%y;T3d}0YS(TM zrBy|(s=aE(Y9H_W`S}N)b6w}YznpV_y03FzhiGb|&k7NQ0001192R4K&cy#~5bzxN z5kmdv#2BoNvjm-2G|2q{0KoDYhtaYOcihOk9U%0DCvm1WUHXD*VD2|I_xLVrgImjE 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..f2a66febc72828363428f8a7508fead3ac866cdf GIT binary patch literal 2059 zcmb`I`8N~_8^_1kX5p1wjIpakBAQ0pY*}lPB~6)ft#J*bkdcg?YczJ*Da(*VlYP%v zD!tY$Gs2K%OpLW*kmb7P{TJTnobU5F-*cXyp6?G|qNRnYFa!<(006?~W=7UW#QZCe zz|oe`-pM=ypRb|$9njGPfm~t$0Ks;1BZE5u_RBfXJ!MA3KYc`qWKZjy`_?gAGVQqFDwfKRFf- zUA-JY1DC42Nya;WKW3L2-Fb2y>u-UyK&203w^ni+@@gs$tNR+J@WH-8%%`9E_6tb> zRhw^y0~B%Xu;_9}wUe(cZ{TaA%M(KlRVT;bwT3_l+0U&XO&8|nRdc+o-sz_fQj2-= z-IYuDZ|&dFcH;-dl$A?Kkmf4s%6;iu^>sWPaX>Hmt;_V)HdBy}X6$!K3zNbUW~5nY zK^PSWM_okxk^_2)Z}qb^e~2p=zDSRgmBD$+i`k!u1Rr$tJz6%&YMyf7iiUcSv5!ca zjvuiKmzotBYAp;IjcGMDHi1)K2{n-vtrfHpTgV`}P|r6qyv72q!s4dyZi;9lkgh|M z3XPX^nvB&D-%zQ39yx@=dv%-FPh}-5wNm3`pGQ1+)2-qn^=sgp`JQ9qN>7f3@2476rq5dL~Wh(=3Kk-A9sSd7b%9BcHkIQ~hi z4Rs;~i~a2eYc}t|ec^Z`h|k+Cdr)#P2Vh`kl1wR{`)ap6ErI#9>;k(OwTnE4%P9jI z`DH#Mg{DhALLW@96szm5DD6~!@S$X?CNJw5N4i#;+*v?u^M-C6m+GDn&K9s`-K64l zvOJJbzdKQbp-6_K!q3K0-wrRYL&%Mr5PxUoec6b$@z*`QU5!Gzoso0=*dI3!aerM} zEfG|uCwcA~0D~7x@+t4S+BCz0bs@jUQU+Q4dO_XPz9N6I!GwLy^W*RujmL4&v(D|u zB{lV7S}^(Jvqd~4SlH!&D4AL5>)G0IU*4KgaXm$H+A+aOSf|>(7+z05q2Y}lE5`e& zN|@Go=Hqy484wwnhr(8 zH+gosTD+dOwGKD?mCcJCd%}PK{j09it(94S?JK4XqHShwx5=lhk_wnNU$)W{!{hSg z)G8@ke4-UwC25D{K120k^A+^B{D5=zR&4?Lvz@*jB)5$Z+uY8ahorO!VTELKy<3s1 zca`kVxFtc^@b~O@G;hQKj-NK!^T5G!FW{`Mf?d_Wxr6{a2?gy{U4NnhmzB>%?ALa| zC=n6YMkd=)ytqlOD&aqoIV`8tc(bz&^4DrR^N6%{D+s;p-`P4(MkG+h4vtD3{1CZEM( zb5^Gvq<{-2>Ejoi*4)XL4WM3g));mM(vgso;Vr0WuXS>DR#mKNLwe@@-DVRj zCzW&iMxVaSAG=C_9x|}L&P~I{w+y;l=->krARu=YD14SrEZCP(@w~l%Klqp2QIwyk z9ZP^#BE2PPyRu(^*ssabbl}VvW(N_%t}Qz=tqdNFL>(R(I47R6gpz*5U@(>x7`0V<>E&xgZso$ Th{pM&b_bXnTNqUsI)wiN%HH>; literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..c81f42fca6de13f0da20e7d3c5ac07e2cadca4d4 GIT binary patch literal 4640 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ca10e9142bc2ed408de6bdee77abffddeeef1cbe GIT binary patch literal 7304 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..3ec65e055c4edfa961e5eb9d94cb70517df66a1d GIT binary patch literal 10470 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..00fa441 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..caf1945 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,7 @@ + + + + lizhi.fm + xmcdn.com + + \ No newline at end of file diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..35c9116 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..232bc0d --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,31 @@ +buildscript { + ext.kotlin_version = '1.3.50' + repositories { + google() + jcenter() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..b92fcf3 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.enableR8=false +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..296b146 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..5a2f14f --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,15 @@ +include ':app' + +def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + +def plugins = new Properties() +def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') +if (pluginsFile.exists()) { + pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } +} + +plugins.each { name, path -> + def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() + include ":$name" + project(":$name").projectDir = pluginDirectory +} diff --git a/android/settings_aar.gradle b/android/settings_aar.gradle new file mode 100644 index 0000000..e7b4def --- /dev/null +++ b/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/assets/listennote.png b/assets/listennote.png new file mode 100644 index 0000000000000000000000000000000000000000..cfad713a7c19e61f1001845d24e9d4984f59674b GIT binary patch literal 27533 zcmeGE^;er+*EWnokU)W8#R)FOwUj`Cpv9#)6f0ibr3HdRaVYLiaW7EZ9g1tQ;#%DK z!gYV|^IZ44_dl@5*kh~__~9gLt?9=x=Sirt;ybJtq%RN<5U^yVCEg<-pq9daPent8 ze-hm_MZ$j|I=z1A* zcj8!DT5=p+N`|NkkL_O^q@Si;3Y(j6rJ0YC;J!eQM)?1)|DTco6b;#tx_jW23=CQN zU(1iS5|XokB~Y2wWU$2kNEl`F)k$wWew-k1W&QGQ&L=;MV?$H@%D}G`HUEEg4qZo= z7t!L=Z4nD{zUUuaAHaDj?ZWA7JQjy_QX+uhpL4sozUi5noM7w&xl~USUGWuWfq3v8 z5AZD&rDFtEm<7C&>v{DKR$d>)b|}>sq`V4ZTu~4m}%x=Ieg%Li5TACVMJ|RO{z;WDncpnr$*Ljp?J0p$}+IcNaP2H6e&hF#*5;`C~-}3y}?H5 z`x17Z=)bj5;y2qmoJpvS=RQAjeeb1k&xz?y&tBmb z$M(MFf6Qgxe(%wct6pG)!&dE@WEWcznMri)%5NG#J~??<3EujXa)~lv8+;WWyFBAO z)M23;Y^bc^Buugw3v_*T zV>~|IUvHb{l;`n?iPMWP7+9@$ZkS@>B-D+&9~4nhzc`jfhFohAE_=YM)Releig+l`6HF?s)c^`t$f z-xclM%A=th4XTSsi)Vzjo_@LVrI4jqGi`Li-fRX5+Ko||0-npOtpJfGSNCMFFj5wGgEBfa_WJTr&tcu<+aB-9P(+1EwSaVPncZw zYAjx|bx`+0w0>$q(QfN?Ug;5Wkg%a5!&-in%*M9tA1|mtX)WI?Iw;_O7Y-XD_++mpb^^M|$-*7nFj?&+fA>f{M=wjBBpQ||2^B*Wjzy#>M5rhg;5ToLtbI6?6 ze8^`8bD%C%u*yC7u5)dkzR+->WX#QNXgnmgEEzLlaRH16K%}@sZ(?b0t{{Ho- z;zSC{x(ptb?>L%!pUmVpNk`0bvXvcu`do)7okd`GkU-c+8n*r+EfWiCx5WFGiZ`0k z10W8L9}07lkMTd5?u8A78K{!5@Td^~cl84hA2d3X#~HKVh+PHu{?ZP#E@NC=>t`Al z%o-DK!=5G~d-?oyk<_XcKUejF35Aix zFzRRHwNhp_Tcgz*9L!Vw!xri>7tF+C=lX%5e7+?HM7(|mUkI%!5`XPj+I`;J`Hg{y z2K@tr2D5GowDKQ|)MJJ2%Z`12Bv0@E7@c}~c~A|9PZ|+0==Cc6OZHHJ6}ojGrB~M- z%8u!HdbUi$VT{K^#;DV{j~Juae`Kx*HILhKOKE75gOD7(dxtR8V3c)8%8xw<%FU&a zmfb5#nPQ$bj96#nvhXCWRieTT28S~0x(l7=PRspi94I^06DaRQ9le&J%0LzXeJfin zy6sq8h6fz`_w-HJAyy}sLOV#FC7X~BsZS6~34X8kFK3!F%5-Pz>r3k6DTx8kUW@-;( zpDM2IlnoTabBjx(`>&wG@(s?@A8+26(SIpE;B|us?Dw*(M={kHX4?9p6^xgch!;B1 z)j3;SsprzHAa^2i&Gfin;36=ui`ld+u;+Qh|Fc4{d?#b#-euidUH#phd)dKEYg6fs zcH1^C(IymfuL_U9e)RU#IjTV6K;$&scHK3iJ*u1F;jsqh!6WTKer-6_WC%QT^8)M@Gk;IBMHwb+?lLt6sc&z}kPY4{_i^5=e7{4T zgdeF`z&7LGC!#_WLFRjxrJF5KQ{0w`hDXj2+)Ro6@a8;t+=O-6Km0xXOk(s|E5|=+ zrd|?5(5Glu2T+M6Oax2&27Y<4rp{v56tf>6C4Bz7^+*v}jcZ$-@#Ve7(8jB6%Qc4X zIk>QL7&vzk=EU18lP(`M+t0PVdyNYo=G3EZt)b{Z# zx{kywW)i(D5&h4yu%T}L&xesVe2eqNml&=&L`<5>=5r==#wwUDX~uDk28c zzGE*KzC!{|Z9*LN16|$4`;bYZQ~au^=%S4Psyg`!2}5G{cIK_vXsFvQ)AZO&JcB-z z(V!B{C!VUE|BW+HfG_SicB8?I7P2OB!zu=m9Nbr(Z(ps@DSGQwBh%p^`x;QiviXOE zI7CB{ki8vN?CZNwuTe82(Y?o>4D4&J5T*ianDRFMw-gePT>~C4cighSwCYo4u%oyVoV)HY z>}PJ_!oLXn zENJ(w$-sf@O2M{?HkhnJ=q4HM=Awolx0rBXssLh%yMsJj>)>+cj-S$}?q~W2W{?$i ziuKrXc3iZ=<-;V)%bSKjp*PPlO(DxE&w8yaf$93ml(LeK*;o9yD1$v&*rUPYSrMMf z9vT0e6X7an#veZh-BW&Bu_zucocPt^U{CmMXhkij%a|@24A9Ifx_6ax-4o29Ka63y z#>(C)*{a&eGd(>#b>qkKVk02%itJLtWliBad0-rtxc0pM&V>ROo)p@Sm$A#6I~X6P zgV!S2Y@>F8T-Jnu;5JzkYTWDN>4hdd+JXWAu*%HuVWhmLFr~pZPJ0jQZ@UcCY?wP+ocL>+P4XY{PKVycLJ84# z-jvv{G@#FNULvxQAtYgS;uT#PyV@*2TR?)nrQ*NDAMF8v;52|dCAMU)ft3(HSLlyR zN)+)}1a!Kz_WAy&qJTu-c_n^Ce)Q2^Zb|E4+$maA7AHB)e#)4sHC(#HKXjw-QtxGB zG1{Bsprq(KudSy)HiFs?kZwy2mz>@N8@C$o?JPVe+Ajbz&i{>6RKCM_*XXj|AI;<_ z~~1?X~Fe@C!K}v(TZBkqrpVC8U$lhApX)^a8>5<2GTEI4DQ;*s}yGE zL9xxsT6HEdqYk&DdVxxhh=-PhKt6DQ#5Y>Yn3(uPab)=`%2(9;54q#|DreC!dIe7jKesl7;q()lKhx;rHpy550+Z8jKm-2 z+dYc$`TgcYy}T1dn-4EU(3gq!?;(AZS3Bzdz?^pe?zqu3l?^K6|H%Mw`|rm@bkVpN zlehHec$Kc+!DAXu7mW(=)NA3!LNOwVl|vZyeY9$t2laOb-DTwiR z?K6kChP&fk4sD(t;ouN=ER-DG*=;E5f&#a%nVwEbW0aW~MtebBvID(m{dbw!86=j$N7`lk}#=$=kF4#v2_@S_?$V`AdLJxlA;4n2kr8S-*a zBX|Ra?_MOTvWj*p;Dv-EB=88~D7sxDKH&dw)v$c;taSq%js7n|Qbmh%JsI$QC6#6R zKZ2aXy@trvs_)760O-FWnAP4lZ$Rt2sF-a|?IDPx3I2W|Ec*`b5XjZhTD9s7r1G8) zhaT}mn_~lGCH4+w6Nh;`!@J;kpB+q@fBOX%fH5cO&MddcH!*g!xIKnj3{RR3F+H%u zH+xFMj{8MHjc@a&b#TPYOj=B}s8ZB@?Jsv?Z#Kl*p+~)pBGpVW038mlZzQfGjhSW@ z_D<9fyZ`JXvV$r4-}@jb4^x`{*FHjsdam0cQ;D%C;cpOGpQgCd4l841J%7F%6~6|P zhWZ=L(jyd3SHY8w&2sJ%E!_6~q3|pO+f|4Ao&REVjcii{~1gK8ptqEJQ-%n~!;|QZN z_zvR}V*(OQG60AT^s61ar>KkBnX+1Jr>bAB@-s6lF;#=Bii&Ki&L-NApC=gNU@_?8r*DJsT2Ojv406XJ!C2*Lw87od%sU}Hth_rR)cp7ly z=#QvRi!XdnVw8jXFNy)$i)I~u>dhTw+B=s#9Ox=`94L(KRhXp}SeSF(_Y0iuFYC-` zeGFViA`KYVCP0?#h3i0BHj6;;7Mwt(R*)RfKiGSnf#&%fyi?qEFF(T_tve*r(x`j7jP2kb9W z1uUaOFS}d~z1p+V)pd-VwX|RBlE(duN-_y(&wgdTLLBeHb$TkeW0z&8vt@;HkjZM{ zq3mK(Cmt2FcS_{_`zvTt`vT$A88Q_gF53C?r!4vIZxpW?quUAr`9-CPSsLXY#h;n| zABneq5pOXaufS~-b6Yunfw+Ne8S)1$kIT>Qfs072Zm;YD>f-fs4DI=P0>Ztmx9eKeCq8QA`V z_%;pVv3vAU)s7M?5Ls$Xq|UlVH^ z%aQlw11ccsuh04$5zJYB#PDDJ@#78=?|R_%EhQpm^Hrj4eV6C?2{daAy9$q=opAFl z#HkaIY@JfIB$+eDmtJiDBK@yx)R&(97`dvp7)x^8Ih%9WALPN#k6KJ+e=$5W!D&Jw z@}CF_$CR!gN7PYJ<4;bsNPMw?{ck|6nPW;{d-fBw&JmsZU!)zh5pzPlaio@O$Bxo1 z7Z!n2cG#}j*gKo{SXaY$Fr(p8W$0YK2aIF3B%*W6;Tu!=)4#Dzp9+tJL|+`7z0|nmgo=3)F4BQ$KjAN3@$rf;`F3 zN4Z0VSi(8nADLUpa8P<4nI}B$3UQ5!axo-#*-moZb3Sl?$*u*<YX6-qowhQ?(m>9_wd9ykIujZ>pMMVO}?Fz$Tmvx{KglBc&Q1~w6yv~?R z4=tyDa@a8*b*$tw^7?sXU9`2?QWxy`G@-2y@K1$58iO-#*MBb%`z;9#U5$GDjxE>J zIynC1{CxFJoeRfBV>9*7&fl{<+VBHI&MQDS8FnE-E9C6x6XaF6Bex=6KaKAZ`#jdH zN1+#V_~iC_F3|Q4hHpL~nB?HX*DOT|jBvSysqmN79{wfM(7wkuNrr>KSP3W?viGex z56PkVdfm5N=>d<|HmByL>{+>8FX7c2)`0`J%9di2%;y8rt;WJeSsj_$MAv!r%(7Sy zW#(oPV|>IeB>m&E-$4K28F1)*H)4|XKR*(f=E#R3%c2`{o?Q==IU==&cDrf;Q2k4T zKL*8r9vDhCf|Ps>RE6K+S1pU#$28@)TYE&b`+KjNhtzN76Okt*5N4~g*(;2tpWbMp za`Jh#hn|_d?|TL5-xs4Cj|1mAYFMV`bTKPDX|rO$O*jSZ;A`g>lqLr;j0=xHpM5Ok z#LmX7;W5)Bwf!AA;S*aPIO>htY~aZ&*LOtckuFLOQO`M%hSnbPegWgw+k}am8t)Y? z%(6Gpt$sjXOE~lp$J$xDiN3_dour`Ojce}nQ-Ft~-pe!E+shccQhi#=uGRY1tX=u$ zq+x3(R2X_36o%}W@ufR?MTpVPX5x*vSNkR8bZ6=pcawQtcm}5d%{~(0$}o1NCkeqn z`3j1F>};m~9v-`&U zG~kWXYw8V@mE)N;J=;Ju4#1oy^NyabV%W?RJq=<>yqK+FXQVAK``T;dy75%{Te8Jd z$OqqQ5nR)hP0J3;nZjKKNY*|772Mno&{v-Zv?*~$T`v*kio-!Ac*IL||0=<$5OL4>BatLFY$laMGHV5Y)Nb-3+nP5-9#L?# z@mJ9TjxEzIpUmi)gE?0bmNPy-A`*rEih9jqE>E9&- z16spgsk849Su)kI7#zbzLyeJxsIMC%Bs<3G#)%P|BSfq(@an}`bL-@c>BunCS46Tohlypls{_%%y1K_%}=pQ*n8p_`}> z-FQmR=bJ@X{!^5-hqT{Rl72Fgp~XXsYQKrotyu(73W8SHE=~GQPG+J=hEA7tSD48$ zzRYeCVDfRiV07)$(i6nQs*&M=LmR{|2xFmLFE~X0Fpil}R@Qor8>!3sFFtGQe-_oR zFO@$57G8WSg`Ex*|EX`aGGPMO6(62BqoSeC$eiPIlqvJB*L!E&#I@;>Y@J3I%FeI# zx$=Q56cKw_jDqjtUrti{^5VDGAq%rG#`gW#nFF$l9-kMykHxlq7TW(T=m)gs4NlrJ z(4^VKKQ_Y)FpgQ{U|^dZwrfH&3~;jUJF%|sI(i+Mh$NvpxJ@ot>d3w55cb>X7w*KY z14YtX^gLf!jj-Vlv^y9*Tgd-6JF%b97F}ShM*UA+TZ(p%Iisr^n8k&TtdRB2o6p7h zav^(mW5y_-hsJe5Dg)Fcz_#JkL`Owl&}jZUnmWOw00LJt1aVA{5{JiS9?1F~TcA|d z(Ei^54TcPD&+pnp$a|8R0sM167?JB z^tD2>7lssBeWN5w=_qg|NU|S z9lV=&E)NIu@O&V%>S*%0%fu|bQ4zzJB4$W%v3e4;_G-T0VOeOK-wR@&Pzt5^ZJ&(I z!ASN~KGQM|%8Yi-@1|w*)#{UZAKE42Zl!FG&r6Jd;~E?)-yX!?%{Rq|xb_0) zhWj#nH*N_-oIy{^puwS>-e`p1(Ui5G4h0L@R_{Vp->FogcN(30IxG%UY%e+NvL_c{5yWTk%|dIJsV-tE>h}xjixXOFZn|7ncbw1D zNKU&l$xcjscM9>aKAAZk9{)Mcq3yfC~R98nM2Wu zSxN-wyVZxLbA``N3{5~g@bApPcrZ$MF;d+d4o8^Hlj)DHAsuRkJf-XFm16r&hx|@^ z{NeLHlr`djRy6CBM2Owz>KwPAxC-J=U{A5=z9aNrSw94o(SW+JS?(WldSFL1EI*ELydVk326*{YphC{JdB1)GM2f- z{6;}5`uCGn`uv8Y8E&twp5Rx{MLsj3=6s}qa%7j)DTkfnCHXhRfT4te5qodC%Nz9B z>b1H`S_A#K z0>8&68}(C;e}zE=^lx|#*n&3sK)j|FtkW`2`@nmvcV_yc+eF@(T^xbPv9g9@?|Jn5 zuL9A>eA22{u#+2Kc=Atex_hQ2adP0|L%a5Ef{E4MzD<-f7nC5mlL&WN{*$uV;)2~U z-}PSaSm0dKh;_k1sG(z24T%P3rIwSK$yxKNa+~p7KZMOx_n6m)O7A8~64Oc{5pCfdml~IV$-rP0JyUe}>1mU&)5{5JC_O z2cuki6P?h7_H!4Tls*>})Bw^tbBfB=s}ciEl99@(r*n zkF+Uvu4Wrias&`gFx3*1v*}V{^^-Q)sbY1bE-i)+huG7evvp!C!Ji&PG_QN2 z@H^@Xu6(Sl?rl-L5OnsR9P!g;s|lW>E}nE2j}`+B(j6;IF;!!`e-Hvq3upOFi*53X zZlx;@bJYwwy>n)hBpV!Hx4N$3_UlKVHCENc8>Bx}LwEbOoCi;BEO5{1MkMQv zSRr(`8G!s;-+hEhl5ru(Q|garW)@!IYEyfNJ9sDg!HR#1puC?7-EB(8NJLai9a8rtC zlttuZ8dsIPpZ!U_P~p3#58@>YR`|Z*bt}rW=(%SOQlnG$ZT&XOMK~>g1*pCy>odH8 zvmoy~qxk5P6-CG!a^VW3Voz=QfPSYwo)>BZaf!Cnh6PTY*Nt8U9igiO&l5AUyR;!d? zdL6001@C8mxVS`g&=W)-Eulx*o>mx1mF-KiT)RTFNh4oCPzsK;&Z6Ht@KX}St`0W_ z&XrK0c;Ve@76~BaBYoSq)Y!j`aPc_e812 z0o%K`R1=Ou0|oHig-dgY83Y}f5$jV1oC{>#GM{tr=qk72xwRH-PB`v7t#ewi1sjxI zPeF1$?b&j})0{lo#`zw-idwUbnD;_vI-;{o-$}T)n&$by9^{8OdEpSeNPbm{e6*>n z_}g&7+>MxMX74e|c$=uddxj|QKOJwhz;<(-eJtg4OG#?PH^^^q z3jU39_-<&$ktlE$I3)@pQ!x*t{6#kBzC*993nvXc_gE)J|5hnKz)W9qqlKnnJrJr* zV!xkq$lp{6_Kn~v;KjyHJR-IRss!b9jZih}$9bLna88J_Y{*F;ZLQig9?DPB_^Y|n zD~NvUwS!hvzdT*aIuG*st0 z$}RbG-H)Sq=ACrW+#XOaFQY4^WsFCPU(^RhEE%o$SJmuB;_EwA9m_R;7QK= z3K+Nb8K4Fh(uH>6DQpw(TxB(NYbI4w(rEUDWF`|RQGH01+(PZtn69X$$}000H)lTB zdm|b^W=*2u@rtO8n-G!1_mfw^CTxzvp`^ybt_b7TCRQ7kdS8rz2O{FbTe^b4W5ica z`Qv;R6tb4#(6l3>Nw%;YkcW#kkWC=&my}tTd%Oa|0z4{B(Vb9?>Z?rpF3xwH-5hkJ75>Ei~X4{sS~lza)Fu?`$99~DwO6BE(N4^fv7R_fPAq8OO@!CUd;6Ep`% zDbTAwFk2;ozY!${F7+sIx9Hg5+_Vb$63mGYT@nzKNY_|IO^O8JWB^@WpP2$CX0$E& zMg!g%+R;Yvm)?)^S#=w|o-44gtlP}~RdTk&YC*;;`wJL`pG6t*O68&LiF<16?(KAS zL8Pdv42A(X;Ev{eqX4Ba9oLutH-~k)h`7pxlo@M+f3+UyL{j49iwE6vL12;8ZWXeo zgw+?Dj@BBP;|jglB>rc1tUDBT>0cfs_4nh)w84?)?l5R;;d2?weREj(G#Y8H<)OiX z6}a$%Q+jpEBWV7!r&lh#yhN5T9Jcnr;$fTz^8N|68lTnVNQnG?hcXGoyz?{Lu;-Fc z{$-6%KHZ@R#<5f5lSCF3Y`_0wNetMd;zer{sjQs-DT%e_`y57s;p_vv10r&Ef>w*QstfZ3MzL*ydV4#q6>f?VB^S&!80= zJ$%j7)=2psM2OysxH!vpFnx}p+&0QPlvkW$MdBjUU%3f`;klFiW#tKHVf!Kyae8E{ zWtP&D2-|)5NbvO}%arS~uC~I=_9uCk>x}i84x0@HT$^yv&+c0?B&Ix!jv2HozvHka zYVl74F9zqD;=Sb`lu>E!0g3mD?QzFz3g;3(x*`&qT(mqIubbt+?I+kKvJoQnSdy-| zgvMINwF8lRh3J$zNrr`2ffAT}Z%M+BeMZuj7PKfiZHQ|9VT$Q*y(Qc9c)RUZ#TwY} zTd}ZIp6*EA&hq|!sbbzgI!+e?`kgGTb6rgPF-^vIe{RP`v;tdhZ|<*i1G4-v zQT-4ABv$_eooXNcZCkVosz67CdE=LK(K3KrsmE~nge9MBmz%XVG3>Md^z5UdtH|eM zwVI+3T59{+@bPa5E6Uu`tjs&n_>%#eA(B zA1i!NA^uUus{zlMQ)h3tNX-htEZW~p`sha)-VO>0`p;J-zDd6awrpL|=>Z3OBTA8%I3ZmR>ym#q zAUlTe9f4hf@)0F-yFy)3W^=@o32>sD2(jhK2elaVG2KCti#BVl^;RrOe^#FpLyYAa z{-UKr$=;8*rU=`?r?;>Wl@XJ$r85h_ zbQNw_yX|+Xl))!I;uUb9U?o*Hz-92^FTzX7$;2uLr1Ru;-xqBH{6asn7Alnj_NZDT ziYBdS&h1$1I&NFQO=MA?FCdO zTV8_>rjyTK)Ml{yc2s1&gNmTP_JS!FuGT9*WQOXT`>5Vn-tc;Deg_dugR}gzCBOGU z6uu~lX4gF|4vHaP8x+;2%H}j*2;>xVlQfB_r6(X|YZKUfc4#t?f9H*=tJd3EY13^~ zN84#<8j~?PcNY~XqyVoP?(xU>Cw)XOdg2+Lewnc63BOxHohIVZFeyK0<%GkB7}zD- zEuq`TlRMQvYm8j|z#1GjU#+?>3MITnkY;HusT_K9LKPiVulNQvTS?Dk(5A9=ZymWA zoK1zEqLB+|>i1Y@Kl$Nr*?w@D1M-%MDb*hw#V(prPys3Vhsv>udr%Yv^Jn3sj_F9# zhzmM7cKkxf)7tNOtVm|~XM>y?Nw!D=iWwiZ(%BrCY7=UTEp+d_z+`PO*O%B@ z_Y{D|i2b?!Mf#|jDkh;aq#QoQj%7p2fH)^DONKpbD^LHuxo;UWB&-E?1BXCeFqDhH zMc|(J(|Rd$?{}9|pYhLOKujOEH?inP-ANLmH36vCHqchmPo)Of+yuU zWaqsFn+(Q^mvVplCxY6+#)X@~93`;413`5q{&{^yUgm)g4zRnd6-}vI-5F-TWE4LmKf$f;h;?vNY zu^6uVCECIEnh{~O$}0F;>g%LyNV2ST@MEf)U4I5KWQ>&ch5vIYNX2Q?x)g?yKWXI> zXidL19+J;;0B_F;mSzAO!?J6#1nli-S&Z)A?O>)tQFL9p4=jnxMGksN76?bCn4H;a zOWfPnHGu?nzOFg}DwD$Fe^HGnxGI@m-$?H&rXRNKrM(d~wB3+p8a5L8%?=Q*L>wup)w?sQXUDn;vx1TW8`}Ejk@K>T{nJwa- z22X!8Z_9&9HujkCyI#(B-*{(V(Yol&hX`0d48973dD>mvIR;B|^TK8EiuQ!(VD=|b z0?HaY+82-~NCuw4O%mGeR2;n^uN8AxYL&SqvgjGx0{vfU9OBp=GFF2xkIjWfi7e#8 z;&RJMq88`P5Ud2ZNGCQvs#c{(WkBOG}Mh(IL_CZkg~ zue)ohPy|(j<6Ad>Mo2DQbzM4O+&ND6@Y~znbdcWZa2P%Ld^)Xw`hdK|-B|N}cPYa& z$HwPM6r$JZsCD11u*7jg?;bujnbIF(+@+Zkzt&`IX>~SYz6Astylb)~t%cL3Me@*< zNb|5{xWb6JWb_xr7y*$3$Z^Mh}@>@d`#3o?dJb zlV+~*L8&UFNzWaI82oFV_tyRktpP7Rq>R61wKkj|5^n};!-IQBLVK6=QQlQT22PBt~wbtems zXJ51QT&@`b9>T-+F!~M#lR$}Mt4H*(E(H?6pGV0TL7M6(GP~5Thtix_y^pLOtH`&u z%tXcs1dB#UCW*FxW@P;65si5Y=9droI3Pdc6G)3(o(J-j0UO%`NM3hU*cOwdCprCG z>1AeK?Q{<%5!a zM1P^~e-(JWmtRs7?rfI$Jt^VyE1Fu%JEbJEtjKhM7L6&m!GH_8WWg+U@eG8kt~%F8 zFBlXEKRCN?e8Bje67-HL{4Q|oQnX0mvw|ryrULm{&$VlNuSO5pe;?a}(j&yX70H+`nLom5?<|mX&kjXykib;?Yt1w+-+jT%%eew-}J$U;u0v8J_~-H|WoQa?>%vLI|t9`D)V z(WSGZEo2^uoWuOV=m(b9?~Y~|ZjPPh_xA!O(g^5r}t(qV2Ho#Us!@R=(B&kvVR z+*_nRL!0hkm_F?zu$aU418}n)k!mWMV{M7y_2}74c;;?GU-Dmn5MGuv*A$fBrZ5qTSz!s@+nbQ)_g4wB0HmHx}vz$lV$|4aNB5lOja&^$z_G z<`Uh}_?XW`yMKik_os4#MntD@X|vFcSwy+sj$8LTu>=?z7AA=JxTheQSXqq=M*Ggl zHw2h*lG^o%d!tGClKXX={??1_eMo5#ac{0!l&FH;IN!x%o@8@Ml;_9EAZxR$DY+l# z_bZjg+!*D&;Umw6VWes?_lYd8I~Y2C74~tUfandT&pWb(@AJ|ax-EPyMn+1i@WI_3 zwC3=y9Is@Z^Xrv#J|%a6v_D@`27!u6+^O8Yn7NFRp7} zN!eKQmpD9XZXYPxk{qr&9rX$UgFpPkHvKSH1_Bz_3wVI_?IIc+I>gwNYZT_joJG1s zEYZA@5?a2vgOzg>P1hLQgpVA&jL4qBB=EWu6&x(Un>n>iX|-9pFw?S3YkI4nYD?T- zU#UVUS2A~`zUjWZW;*vPS&(_JQlQ?gOA7exRRnyC;O5b5g;7Qij8nPH;D zv;r;(?bMdE)j_R6zE^1DPgk~Poq_pJ`R|^B$&+C#}UJ;||79uQ! z6MtmFHiAhei5iSM!jh?cYw;{HCk5xX`enM{ofi0fP!)EAzTRg?4fE9dPs6o)UhR31 zQfsiF=h=idv-_o%65oeo=|iM?|I9jL-K$cYM)}k76+IoS*I?pb+PY^mBQwCypS>#8 zy~M#LJ!E<84%c{UK*N0*7EJx3kGu_oe^0yiSUFv^uwXX7eyLF?#h7v2lE~2@$Yen`X~{06tgYyJ$tZ1-F}^@ zeG1%zB;>R3cfZ@B3K4n&&N+)($zxpRaS=eU3G~d1g!WVOx>buSCOufa~@4UPxB&`e=dFTvGx(n=N(|LSl@a?cu6c6FyA6*q*$jzIVx*8)30VlvWDmbyj@}rugof)9Il5P9pFXjbJTZ^*2AcS8QKw zDzF7M0`WKiqQ5x?HMEdQ8 ziN1q!z<#4oYyZ@aO-YYu^)=L%q3S}`ht9X9uASEGa8Yr3vuk({)PQe`zOKs(3j6U< z{K?l`9<^ppXdjwBI3^eoy9=zDwiF7`;FOXFFwtbKG^TJGc%yGm7qyP;An^_0+8gS@ zo>l~_2#Mh%1}^FF-ogc-G?F%zexXK zsh-FKT=DyTt*$r456tOB_VUMdv{^jNX_Ml#CFB*(T0m$0QfI$Dc#8~A4w}iu2=BO| zd9g44$huo~(<~Au+vU!+#1|-?(8y)QM!%*#5k$Tt|2B*4b5G2S0KkuoT~TON5Drcj zv%cYtH9QpwHOycd5`8(btt?gj{g17?5M0#cv(*vo!C7OZ+Wl;$A0^=o?uYjJ!SgL11mmUQjdayqlbub$i(w zy5m+#_>Fk)s-k$LnVBmtFkg^ljv|Ycr3RmgA6dVNuCJ zcj@*ZC}_nbML%FAYDuSUPufI7@f+=BcpnB$Xbyc8;MZry)_0mRkfB;CGg`9R2 z;Ja%f9_r=&$yW9c#?yWsOS*5Me=+4bVoMmE$c5rO;%uVcUxQW_cAlT@LM<$2CWkKy zXV~YIOek?~e*bNasXokhV!1+0oz2C6LaHZFvB6sZ)K`Lw<^rLSQPpemA2N&n5n>sJWEu3BvqJSO;xA@BrRl6 zJN?j?E%UARq|U|JE~a~M5SroNWS}ON|5PFH>T>tFkeS@yWl(mhP8__Pn=d$rM%bi> z3jXZ%$bgUeJi`->@kD2}+?@j1=}e{9-_>DfJ`oUKq*ySkGCe&59cM?V;qPIDOx-c%SMx z?W7$G7hH?5b#lI}qO$h0;6j{7P7n}S1fTyG0Lv+B8b9fR+DO(PchF*PmRlnEq=Jwc z^O>R6VTKtg@DT2h6LPDsUn;mrY2&(F)(8ZE0x3|15I#A11$gX;1*`9bB*S0%32LYB z-*p&2ZMFRPqCK1;6FOAjI8fYU9W3>F_X6Y?4AIx%Axk2tmQ?T$QT!+a{b3A%2$0bf ze_{{^RMBvnx!OJ1wFn!Ur=IJc9W4kODq+KGUbyikT_Db~V`%*{%8&G;3vFdv`53dN z8F+T_v9#mT<6y!cZ|!l`N1`o?K(vOw&?(TDu1N1~ad&TWWTj z7gvNQ1}-0pa~ol0p5tGkqTcTyPGjN)vRDwzC5t@J53HE*@u1O|A z)mswyDm+}U_=Ueb7@NyJ4E|mb2>D!?4^F6Hw79Q}k^GR1v;LYd*M`ODV|@#2@YXy; zVi+;1t1-==ZE{EvaQG?%Vz&)A5gg}MOl{r|&wPiqF-wBb^fq&FMmdhmwxvVoAu>L+ z$f>l^5blMZFD~n!Ecgz11qXdUDpJ9-6j(#n0>eqZHXH_xz6#TsuXCW;tt&fk`cy-{ zx{vm=NJGl~pNS<*^qsKRB7u9{xSKV?Sh;9R@`*fo@VW&~%t0CX%Gr)i5|KaPv?CZW z{+2cuECsIYdxieUPW8C7QhLr$<)z7$Q17qDUOk1_ zNhOaS;qWR=9!%2X`K6#iWlht(a}HB>u|SV{pg7nX*yNHB6|tkUTJZjZrzn}dkOw}h zz*$kk+t4_Mw7bru+`tfdZ4n)oHRkQ9j{Fs0uK13EpxS-rdLK#kig9O^j#c z@C3h0a_3q;ut6^os&l=Uy3LmZicPl$jPvmVjFvb)nQElV6mXze0~f(ri{c9DXHo4M zZHe#}J^VVHXr&T)+X1*$l4-Zg9m0f{&x{`Yb*5D1VZj;|wFK^Rw%rrE^Z8^nAoU9r5M2)y8*`wc*3PB+8_9Aomn$E~TDh8Fh?YYX$R(w;qVHl_S~5sh8L2;+0?{Qci=^6S(gTa)cz$@C_sPkLB=37%_jO(Ob-!M(9D?$XEKA&TFC%it4(G7V zlg4?E@3RHt+kA|x)u=LLWMrE-K+pWCHw;atkC*j~BQGz@6c{{@Z+R|7Y2EvII;FhD z()fc%P`k3~%ZO*DhnSfmpSJNq;?Mcy0K6hC7 z%aj_ak^K{I25$eqM#>Ji#h7Y`717iut4pyl=(ECEvanlMMaD-0hR9!!Gq?5$!|(^m zr7@wovm3NOEgls62dFKhmj|6riy)j8gZ?VuyX?ApK-ZYyMe>^@4ajB@Ck9Sy(VEH} zk)H-ofg{$0-eFeBc~ryniqpo1brfo?wfUmmeb&?YBF_D3#j)>fWJ~+4wYJ)^dn#Qh z)br^NVYaAAaZD@BKzn{JxH{OoYy6suQ`1W+&qL`Z-)RAgi=2a@r_9}fWo3<@ulptL zmG(>IT$EF4%qUdRggk=6fpX$ONQG(j$AeVLHOSITm;$PxX;UIK-{bG;IXP%*wUWc2 z()zX{(r02(fnh)vpxQln5lH08p=Q(w{-Smz|6L5DO1eze@Z(L36F4(TJnh?tYDsCd zz9_$zr-@@>n6L`TdQ*rU$hZhV*ztItQIwk-CTnazibl25rXPW*i=(=7uQ0)^MWv{w zK<@&y<}2oGUP;unFE6uO_B^IUO)iYTJChGgkFy2)c4M|5}%ouM(sxc@|F{W# zEK#Q(DMdqHEUl^)7P8+@cY2h_tXbc46|DtC-0{KS`hc=}DtnfU(PEeB; z>1q>m;?3CvW61rqqkjx4JvHMg8gpp@oK=9EwWYV6#uF`1&98UZYkR{TXNxXDU7dV= zniZ%r5DvsDL>*9RYM@FM<=^E0)9xXEnWyHP7=Je@)`y^zD_)OHP?u+~F2>xXZBayq zB{`c{U_~AP7*&B7 zTjy|Iy=0Jd9wMCf-EW>TdU2}2$yT#%eeJ6fb#X5Mp8=?k^xL~jt+Y=YJF9$WrI6_) zZyuL-q;eI7>&D%;-$I?!Zofm^OopZX>i;zfqs4rCcZs<*J{50%Yb{Z+R5q?KId4s~ zX16I<$)CisX+{_4x!t8KJikixcro6N*<9(mM)K!VpW)$J^8M%&((Hvx!NayZZ-%kP1|fD zI!4(x6PtG0^tn0Q2pG?4jgoaPE|s((a~8jOx+kXN81xsv8!H3<;n7xFHgIHlxcIyH z+>OMEC)F$tZ{eAtiA}(BEPr%_)?mSP;rBHVTGve>eLGRRB2>SSo|cQ9KUTSLi%eq} zr7&`;u-IcYbJ$8JD4L}%ofqcjJr@qgr#==fi_xC>8S~W))hyltF6pmK<;X%rlQ|>@ zmF}^%vKn(I#1#f_b1j{)?BiTkn0wy!S|41gY4weE!fYOu583L>ACjS|%jdNYcs#F#-ufoZ{PTCn zQcO>cjoFlQ*-^8uI~yPQ&>+^!ft9d=IKcb1AFBZtOSbCNRk#}+I)Bg<;e)k5HNMP{ zTm+moH-{M37kytIRKdM&fmV<$h+7~A!?SGq5fm|1u2rc4rPpa=B83V?8}F!cn;2ql z?Z^Yl9GnDYjv50en9Wus%;cH4+2P7J1>~bu3r~|f!GZ~nn=1|!qKcRNa@t8j(^*aR z{_3-9TI%ZJWm%>om3fnmdhP)fpla-+Ua?PwTg0T&dFq4 zHxiwUdrXO+C8VGNh*(wHRt*@BOiIPLmRKUq_56LnmIM$eX}1GAT77!{)E3)q%5HX24Eq)E$u#de9;aQd6G4vE?JOgObz5M;{G>hJi}NWRXxp$SD~xE_ zIox>#b7DYIk?k;Qn=_w88mt3wTqr=%&p)rFyeps4fF;2uwLMARU5@_sS7|Gj0v_0h zlVQ~QlBs?hEIVsHOLHk`ujJsUm!y$7Y}fP`D=$AuM!Rl8eg44AVvX-g;s*OGj#9k$ zdKO#%XK8?bM4qbN*n$s#%OxecRq|DMW|Xay-Cf*=*$);^F$&3vaY+-p*d%32pTz8zjY(|k;+t=yVr!ag03+-dcC!(*H z)C@>=PN+N5u(1-<`DEQu#mD@X!Fz+vBI=QzK!}j0WcuEtbX!KDDv4>gXk`h(a6bU*DnQYDS`JPIa-O+{a zil23-yYq|&myJYh@I)b(3vjbG;o2!T9`U`j^L7fy^tm$sb|9^_o>m5u0`qtjWK6j6 zNJ>A>-No!){T+{pR6|qlrWWc_!5z*c5XABqiEapQ}@mWQycV= zQMyi7E$xC@V$x#KM6)vT;e+IupFf$n7Y*Zaa@}V%AO3JS!>-XYYwD zBDdLq#L@qv2d=yNdh&wFFbMp<-O+NmxRt5P;W&3ahD4IUL|p+76pr55m`dlQufpMav_?p_jMJ^GeYeEd8cs-u~Y=^0Sq^(aPbXD|$ z-nHbdh8cH~Wc zrNMF?oK6yR#~-POv1P!P#moV_VfDhQ>L*veC-JFw*lll#DO3dg z(2lRytZBW0{_>W&p*W!mW;tB$_#&tE+)q&OaGh@Khp}65v^r_gMy-(@o#C(}Q`TY=1DezCXGG z+FYao!I>KioJt)_gUPi0&Ik4IMY0ojSVu{zC$Ab(uOob`;cPi~K?ntV5aTq;*& zzL)0DEnZ7=J^Ip~9(s*(lQ)$cdUr-TE@X0gxYD$=oA%^V4NXJRwHrJkYTn?F?;HE- zCbj|##4)z4J%~Tlb`!(C3G6YYW~Br_;fHg(E~=lxSy?_&kU5=(-4Ed>b5*0)_5JC!X-~b* z;rzj$Ly${I#XfyPmXmN&dq!WghO`s3HZ#5br@J%Nx4%@7Hoq^}|B(r|`Cfbmufi6I zi|x0%X!V)mDHwjzNzYUHoVm`lxQ%*)LuNvw532zSI+@euucvo_YOrX*c=#y=Qxre> zd0rRQVlB33Na#$^|Nc5vzu0ffF3J1D85j8TYVdPS%pgOe7s9C6T`h%A;*+;P@s`v= z1Ze*ckwP86aq&tEajMktO`Yf-Ace=yTV#U(=}R940daU*NkkR@aSzxgS>v>C`M;J8}Pmm{wlhc@#ye8atMI-2EN& zaMVc9L(ZF9h-NZwMlJB8`VK7%ls7tl08LPM7*P1*vxt6^4x>zC4(&ixw9O=z0N`bw-END4QzVD~mqUMu`ich2URxCZ{AQ zH=B%pI0s4QY;L2Ecdv3PUO@8nWQL@Yw`(BKE#<9so0X@v{43MJduOBClN$K+`BkGx z_0xjLW&^r=mxO7a?9gg_{1^$`pbkKHp;qQsqt3>QEx*-`v6+$e(FgH*Xy1)>*V*db zsOT}wQJ!utSC)7_6j6l#;Pv1HkW5;w)(P3IyMiO(D?T8t|lhG>_tbQJf1+hf&BC^H%xp*YD{; zBw?Skuiv{&;pmdpPE!ZD7Qrj|3>1|gnAVBt{7&3O-N9)2^DfM$X0%VJ!K6!S*r9i8 z&)grP7>d047CAxAiO5jGMnz8dmR%UlYwD@%X-33;&bE@TosorE;x8&3_J&p^IMHJf zb}P`!D>AsBq@n>ogU_*OIc#_mjcCnxxiF_UB_9rA%8$7mZtS@O_=U|-xnuAcAQX9_ z;$JbYd27mAD1H|VNava8mZs3&sNn)s9?ZsblRJDWVr|fwq%V10H zyroriBBIU<=OM2;=(yunIpff!YySC4*~}QX*0|%5_>n%3BJpPE%1tUwj}E8{(czIp zgfge~6`7tpHf>)K9R-Mq0ge!k0mpgIA&x6+*8{#w86R#84W~fc4#qOE01ZhZ$BINY zyXS+5*@&qog~*+&o~}Pk*iT<}$;6q?OjBGsQ&A%YAZ*?&?RmgCN*C8Y$B>3XPE8#16xz3M58YN~L|J!~wbc3t4>K=)abQDV^CPP}Q@iRAFtf^biD?PoQZyJmi|kzFRA_)lVd1 zk@}C#j^a`)y^EZQ4|rEbGjDiYoY7B~xGVYQa(-7)XDar}?Lr@4gnxu6%u)^|`H*8tv6XQQU=uAOfsa)LEMHTo zor6%#J6qA{Km@QNYEQS%16CX%u2$FP_{vDu6j>V$DN59{kL&n#WQHXhd*A=;>cDLT ziVF+&!oqWDc%D5P>&75x2rz*&a_vQAowd)tfm@J9{hYvgv~=%Q3S=wx$Xk>hG|?;R*aMg2_# zC(FbAwupK0bor+;&a!tncYBpH0kQ+(PW?19m7HRR@jFVl{S6z61-IaPLkjY9cR&135pH|nE zkV1wob^g^wV<#FZBb1DQ%N!mn#&|kG`KcZ{xytvp(@aKD*_8MfIUw(&dg_>13cdvZ zlm}Qr#dGDufw%NgY@i4H3-dvC?#X2SJ-G)q67YKZ)R9;#A+WO?6Ey5qi_nh!j7)YXy^_91|mb#z5^JG z98&a@pJy?7dZ&k0A7Sc+X0rX7_>$1`FfecF{ zHn{-sPSrEP0UB-*U^d=mKF}Rv#lI%A032mmw!nHiulBb<5y=U&;pyRop+@`j3V{=s8G3@ACp zpYjUy!I56KrI;87gyB-=+ts$B%EM4v;^+|6BeL$c!Pz*C>8%etHrdM6hpV!sMB#AT=cg zwcc8qOLrDfQn=-!jD&tiG}}eyDRF`Om4$9N$BN}_(qWsUl=Aa0E#G9kHb9zpdqi$< z#=@L|84l!>v14@ub*@&bHw4!LB!7}jIy2dJiSz#J;S)xAuGRVJiqz{)^(|EY;B3H6 z?V)#LtNuXkLyS{^9`}J8wC6nC+s{OzsK|}w?HWEz#uoS?M4t^X37xu1)Fer}Ee`F3 zu4YMJPH^Xex6_ri2zL|?tgXcD>;d9DVYhW`>Ozna&{EuQwb-f{`v2~{430AYbE~{wY7G7{LZ2rhYyWlRoFBsVxq(f~ zz%_+MxqNoNxQkv3u8t3UK=#7J1I!I6c#?)gXBjT*HnK{p-Eg@QdZq%ttz+nP_@8`J;d_FPmjZ4+_;Qh-&`NYU%1O&*JHTlX~ zlwvq7zNY4Z0lT*s%2Jl2`d9o0pjJmpxUo= zzWTo1po|&LJ3mNUdQV4{q|xNZmqh|TNUxqMaTuR%ml_$MLR|lIw=y(GAhunNmm*g{h){^L7AEs!UJ}t-E`&j1oh~m%Em~ z(>j;04`WjwYaoz%<*$|no~W+N)7l!ukWrotBQNKz0%e6{C@A_+5n4AB4ZMX{d|yn2 zv?PD3MRBSo8h-cDoaVz2Vzt_oAFI=c$03duE#nVz0NKC=#Sf}ZVN6ma*x-E(fg9gp z=wkTK%?T7Wt6fNa2F z=p!GS6e)WEPuewxco06nNTaoCNyR(`a^2n{GR-$FGJ}+YgbOocWk9g9wNM<%k6Fm; zBu{o8!IqOBD~u;ly(ymR@UYvtlM6sel5ocPnZl2X=zapZGyPJx(udz{GJq3Nf&0?0 z+^&gnO<@p$->3T)DU`2Hop;?8z)WxJEhv#cH&P;aTsX5?cf7`=aciGSU^d1?G=$2d zKn0H^X2@(B(|#uI7?gGCjYhg6+VslEFpU#%s(vjVp!oSA~E#TEap zTy4f#mbahIWiC&wdi^0fI7l)OQgEpPMoEXb&9xGG60k$`kmG8Qpo=_12XgT#D~Qm1 zu$Fr9nkN|}lPfRiAvlbv@mlG(aQR=n_wL81t(fn(hmpxvrxGbvmm+S)h9|8r_xUXMRXq(rvR0IR=Rd3*$eTeN(zbuU8ghU zw@ft8Mq2x{JO-9T3*9)$Qno0mLx}mu)>h*V6GLQ+ttgPn9ty9(VYSws+Cf5`qJU)Q z?Z-~PkKm)b>k?D!TYz)JIJt|<|C=fwa=)}}Dw=ZXlbdGtAsesgmOP#St?XrpWSw}U z8AvYs#rWgl{U?VlXqf_GnM;7UZKeRR;xNAJzioAQw8XG*J}^K&4I`z-o2x0thdYk@P zuQy+lBYEOP?P^cb7&UKzE^shRM+1U>P^O4deu8z7^h6F9|6z?1eqvNw7+QG+_~L!A zgJw21Mb-AXJ~1}~0SgTFGPUGid(7-5xKBjIWcgx(oa=`g2x5+@&gDcta@?r!sGB{_wAU(!hrZXDFvyxj-JMcEmWU)?scuHj{w!G zoE{?pSrocK^0{94Zr=}=aGclOzVW{BGJ-+jC)V!$vvMRIdd~bv9hQWOC_hOfnKzxX zNSKiChG5)@um}P>G1~|zgq#QA=FDT_1J0wS=e#_D>L|YGz-*74UsrxA05}{n33+F?@lfJ2TU0SIM$W(!GvA+!_@boL6_?m&BczC(;=Luo8UFBAwuBUVA#0V5>81E||lYXDUO0FN=DS0@e zc{{d46&l}3J9xI+Lg1<Wy%`h2+De?_|ZG>J~~mA%RR6 zVnx?o2-?1^C!rz16I!+z))vpLC6Rj&l1j(^ zkh_rh$6{|w^m~c6*qj;P2;II!+<=f7)0s0ryzO`XyF+ETH!xJ zLns}PvzLt8bUzc~)bd4;i0XeqC6+D zghc$^Eo5FL#%J~j|F;YHs$4+Rcx~R<7PH>=5R5E|rvD z70bMJ@J#;IsE8CGhvsDLG!nnd&|t9k=AmKud*op7Z?y}4&fB$kk_zL-np-sKbr~pw z2wVc`E+qQZdsYjgq*2g^UZH;j_}?Q>JveM|?klpRKDGr7< zFz=?IA}7q!G~;g?!nwc9xSqLZ_PUs-|b z2Dd;1Ex90u{jSZH-_RgzIcZ2>rbT1DiL!c5(&DdEAcS_HEmREX!0+d8%k0j+mE+re zuAc?OW95fh2A;&2Eym!mWr6=h|1+-;Z&fsTY6Xs2l-queq>qhX{$|iOL8H>z=OZH( z&^_}0t!=38(8C^#sNR~g0)704g)mhPbCPne-T7bh*w!65^wQ!tsrxBR%eIG-;yh0`ZXypbmVh=tT3s z2Yb?B0V|Y;&rMvS?rn@A@u9_)kXpT9J|WK(otr!2%J-H}v-xgRvt0}42FHj2OR8G znx)9RfL}L>^t>EKPNp6ar)5xDOQtS|+EHC4a%y4tl(*dLb8m}i>kAwCIN#Tndu9MH zAk8cd5bRkhQoaB8mtg>A?iSx5HuMD*pn2Wf&I}YBBwWKdMp(Zw+vNKPjZUa8_P3+= z0qXng7UUfu0@?0xPyP>=AG52k#NqwZxa8${|GV`izC*)Wh;b8ZZaVk!e+~VgM}g#6 zvHO+j*I_Q}u_Gjo<5$ZXSkc^y&d!r%d^v2o$mqYvChVuWP|Gzl`K+bUisoI|POS|jx QiwO8L&@t6+&~%FXKTRLHq5uE@ literal 0 HcmV?d00001 diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..e96ef60 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,32 @@ +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..6b4c0f7 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8fac11b --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,518 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, + 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, + 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B80C3931E831B6300D905FE /* App.framework */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEBA1CF902C7004384FC /* Flutter.framework */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.tsacdopPlayer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.tsacdopPlayer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.tsacdopPlayer; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..a28140c --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..f091b6b0bca859a3f474b03065bef75ba58a9e4c GIT binary patch literal 1588 zcmV-42Fv-0P)C1SqPt}wig>|5Crh^=oyX$BK<}M8eLU3e2hGT;=G|!_SP)7zNI6fqUMB=)y zRAZ>eDe#*r`yDAVgB_R*LB*MAc)8(b{g{9McCXW!lq7r(btRoB9!8B-#AI6JMb~YFBEvdsV)`mEQO^&#eRKx@b&x- z5lZm*!WfD8oCLzfHGz#u7sT0^VLMI1MqGxF^v+`4YYnVYgk*=kU?HsSz{v({E3lb9 z>+xILjBN)t6`=g~IBOelGQ(O990@BfXf(DRI5I$qN$0Gkz-FSc$3a+2fX$AedL4u{ z4V+5Ong(9LiGcIKW?_352sR;LtDPmPJXI{YtT=O8=76o9;*n%_m|xo!i>7$IrZ-{l z-x3`7M}qzHsPV@$v#>H-TpjDh2UE$9g6sysUREDy_R(a)>=eHw-WAyfIN z*qb!_hW>G)Tu8nSw9yn#3wFMiLcfc4pY0ek1}8(NqkBR@t4{~oC>ryc-h_ByH(Cg5 z>ao-}771+xE3um9lWAY1FeQFxowa1(!J(;Jg*wrg!=6FdRX+t_<%z&d&?|Bn){>zm zZQj(aA_HeBY&OC^jj*)N`8fa^ePOU72VpInJoI1?`ty#lvlNzs(&MZX+R%2xS~5Kh zX*|AU4QE#~SgPzOXe9>tRj>hjU@c1k5Y_mW*Jp3fI;)1&g3j|zDgC+}2Q_v%YfDax z!?umcN^n}KYQ|a$Lr+51Nf9dkkYFSjZZjkma$0KOj+;aQ&721~t7QUKx61J3(P4P1 zstI~7-wOACnWP4=8oGOwz%vNDqD8w&Q`qcNGGrbbf&0s9L0De{4{mRS?o0MU+nR_! zrvshUau0G^DeMhM_v{5BuLjb#Hh@r23lDAk8oF(C+P0rsBpv85EP>4CVMx#04MOfG z;P%vktHcXwTj~+IE(~px)3*MY77e}p#|c>TD?sMatC0Tu4iKKJ0(X8jxQY*gYtxsC z(zYC$g|@+I+kY;dg_dE>scBf&bP1Nc@Hz<3R)V`=AGkc;8CXqdi=B4l2k|g;2%#m& z*jfX^%b!A8#bI!j9-0Fi0bOXl(-c^AB9|nQaE`*)Hw+o&jS9@7&Gov#HbD~#d{twV zXd^Tr^mWLfFh$@Dr$e;PBEz4(-2q1FF0}c;~B5sA}+Q>TOoP+t>wf)V9Iy=5ruQa;z)y zI9C9*oUga6=hxw6QasLPnee@3^Rr*M{CdaL5=R41nLs(AHk_=Y+A9$2&H(B7!_pURs&8aNw7?`&Z&xY_Ye z)~D5Bog^td-^QbUtkTirdyK^mTHAOuptDflut!#^lnKqU md>ggs(5nOWAqO?umG&QVYK#ibz}*4>0000U6E9hRK9^#O7(mu>ETqrXGsduA8$)?`v2seloOCza43C{NQ$$gAOH**MCn0Q?+L7dl7qnbRdqZ8LSVp1ItDxhxD?t@5_yHg6A8yI zC*%Wgg22K|8E#!~cTNYR~@Y9KepMPrrB8cABapAFa=`H+UGhkXUZV1GnwR1*lPyZ;*K(i~2gp|@bzp8}og7e*#% zEnr|^CWdVV!-4*Y_7rFvlww2Ze+>j*!Z!pQ?2l->4q#nqRu9`ELo6RMS5=br47g_X zRw}P9a7RRYQ%2Vsd0Me{_(EggTnuN6j=-?uFS6j^u69elMypu?t>op*wBx<=Wx8?( ztpe^(fwM6jJX7M-l*k3kEpWOl_Vk3@(_w4oc}4YF4|Rt=2V^XU?#Yz`8(e?aZ@#li0n*=g^qOcVpd-Wbok=@b#Yw zqn8u9a)z>l(1kEaPYZ6hwubN6i<8QHgsu0oE) ziJ(p;Wxm>sf!K+cw>R-(^Y2_bahB+&KI9y^);#0qt}t-$C|Bo71lHi{_+lg#f%RFy z0um=e3$K3i6K{U_4K!EX?F&rExl^W|G8Z8;`5z-k}OGNZ0#WVb$WCpQu-_YsiqKP?BB# vzVHS-CTUF4Ozn5G+mq_~Qqto~ahA+K`|lyv3(-e}00000NkvXXu0mjfd`9t{ literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0ef06e7edb86cdfe0d15b4b0d98334a86163658 GIT binary patch literal 1716 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8f9ed8f5cee1c98386d13b17e89f719e83555b2 GIT binary patch literal 1895 zcmV-t2blPYP)FQtfgmafE#=YDCq`qUBt#QpG%*H6QHY765~R=q zZ6iudfM}q!Pz#~9JgOi8QJ|DSu?1-*(kSi1K4#~5?#|rh?sS)(-JQqX*}ciXJ56_H zdw=^s_srbAdqxlvGyrgGet#6T7_|j;95sL%MtM;q86vOxKM$f#puR)Bjv9Zvz9-di zXOTSsZkM83)E9PYBXC<$6(|>lNLVBb&&6y{NByFCp%6+^ALR@NCTse_wqvNmSWI-m z!$%KlHFH2omF!>#%1l3LTZg(s7eof$7*xB)ZQ0h?ejh?Ta9fDv59+u#MokW+1t8Zb zgHv%K(u9G^Lv`lh#f3<6!JVTL3(dCpxHbnbA;kKqQyd1~^Xe0VIaYBSWm6nsr;dFj z4;G-RyL?cYgsN1{L4ZFFNa;8)Rv0fM0C(~Tkit94 zz#~A)59?QjD&pAPSEQ)p8gP|DS{ng)j=2ux)_EzzJ773GmQ_Cic%3JJhC0t2cx>|v zJcVusIB!%F90{+}8hG3QU4KNeKmK%T>mN57NnCZ^56=0?&3@!j>a>B43pi{!u z7JyDj7`6d)qVp^R=%j>UIY6f+3`+qzIc!Y_=+uN^3BYV|o+$vGo-j-Wm<10%A=(Yk^beI{t%ld@yhKjq0iNjqN4XMGgQtbKubPM$JWBz}YA65k%dm*awtC^+f;a-x4+ddbH^7iDWGg&N0n#MW{kA|=8iMUiFYvMoDY@sPC#t$55gn6ykUTPAr`a@!(;np824>2xJthS z*ZdmT`g5-`BuJs`0LVhz+D9NNa3<=6m;cQLaF?tCv8)zcRSh66*Z|vXhG@$I%U~2l z?`Q zykI#*+rQ=z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRe zt3L_uNyQ*cE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=ky zx=~RKa4{iTm{_>_vSCm?$Ej=i6@=m%@VvAITnigVg{&@!7CDgs908761meDK5azA} z4?=NOH|PdvabgJ&fW2{Mo$Q0CcD8Qc84%{JPYt5EiG{MdLIAeX%T=D7NIP4%Hw}p9 zg)==!2Lbp#j{u_}hMiao9=!VSyx0gHbeCS`;q&vzeq|fs`y&^X-lso(Ls@-706qmA z7u*T5PMo_w3{se1t2`zWeO^hOvTsohG_;>J0wVqVe+n)AbQCx)yh9;w+J6?NF5Lmo zecS@ieAKL8%bVd@+-KT{yI|S}O>pYckUFs;ry9Ow$CD@ztz5K-*D$^{i(_1llhSh^ zEkL$}tsQt5>QA^;QgjgIfBDmcOgi5YDyu?t6vSnbp=1+@6D& z5MJ}B8q;bRlVoxasyhcUF1+)o`&3r0colr}QJ3hcSdLu;9;td>kf@Tcn<@9sIx&=m z;AD;SCh95=&p;$r{Xz3iWCO^MX83AGJ(yH&eTXgv|0=34#-&WAmw{)U7OU9!Wz^!7 zZ%jZFi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i h0WYBP*#0Ks^FNSabJA*5${_#%002ovPDHLkV1oKhTl@e3 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a6d6b8609df07bf62e5100a53a01510388bd2b22 GIT binary patch literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..75b2d164a5a98e212cca15ea7bf2ab5de5108680 GIT binary patch literal 3831 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 literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..c4df70d39da7941ef3f6dcb7f06a192d8dcb308d GIT binary patch literal 1888 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 iW8Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +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. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..ec0fd94 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + tsacdop_player + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/lib/about.dart b/lib/about.dart new file mode 100644 index 0000000..a730a37 --- /dev/null +++ b/lib/about.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class AboutApp extends StatelessWidget { + TextSpan buildTextSpan() { + return TextSpan(children: [ + TextSpan(text: 'About Dopcast Player\n',style: TextStyle(fontSize: 20)), + TextSpan( + text: + 'Dopcast Player is a podcast client developed by flutter, is a simple, easy-use player.\n'), + TextSpan( + text: + 'Github https://github.com/stonga .\n'), + ]); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.grey[100], + title: Text('About'), + ), + body: Container( + padding: EdgeInsets.all(20), + alignment: Alignment.topLeft, + child: Text.rich(buildTextSpan()), + )); + } +} diff --git a/lib/addpodcast.dart b/lib/addpodcast.dart new file mode 100644 index 0000000..f2edbba --- /dev/null +++ b/lib/addpodcast.dart @@ -0,0 +1,265 @@ +import 'package:flutter/material.dart'; +import 'package:color_thief_flutter/color_thief_flutter.dart'; +import 'class/importompl.dart'; +import 'package:dio/dio.dart'; +import 'package:provider/provider.dart'; +import 'dart:convert'; +import 'dart:async'; +import 'class/searchpodcast.dart'; +import 'class/podcastlocal.dart'; +import 'class/sqflite_localpodcast.dart'; +import 'home.dart'; +import 'popupmenu.dart'; + +class MyHomePage extends StatefulWidget { + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final _MyHomePageDelegate _delegate = _MyHomePageDelegate(); + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => ImportOmpl(), + child: Scaffold( + key: _scaffoldKey, + appBar: AppBar( + elevation: 0, + centerTitle: true, + backgroundColor: Colors.grey[100], + leading: IconButton( + tooltip: 'Add', + icon: const Icon(Icons.add_circle_outline), + onPressed: () async { + await showSearch( + context: context, + delegate: _delegate, + ); + }, + ), + title: Text('🎙TsacDop', style: TextStyle(color: Colors.blue[600])), + actions: [ + PopupMenu(), + ], + ), + body: Home(), + ), + ); + } +} + +class _MyHomePageDelegate extends SearchDelegate { + static Future getList(String searchText) async { + String url = + "https://listennotes.p.mashape.com/api/v1/search?only_in=title&q=" + + searchText + + "&sort_by_date=0&type=podcast"; + Response response = await Dio().get(url, + options: Options(headers: { + 'X-Mashape-Key': "UtSwKG4afSmshZfglwsXylLKJZHgp1aZHi2jsnSYK5mZi0A32T", + 'Accept': "application/json" + })); + Map searchResultMap = jsonDecode(response.toString()); + var searchResult = SearchPodcast.fromJson(searchResultMap); + return searchResult.results; + } + + @override + Widget buildLeading(BuildContext context) { + return IconButton( + tooltip: 'Back', + icon: AnimatedIcon( + icon: AnimatedIcons.menu_arrow, + progress: transitionAnimation, + ), + onPressed: () { + close(context, 1); + }, + ); + } + + @override + Widget buildSuggestions(BuildContext context) { + if (query.isEmpty) + return Center( + child: Container( + padding: EdgeInsets.only(top: 400), + child: Image( + image: AssetImage('assets/listennote.png'), + width: 300, + ), + )); + return FutureBuilder( + future: getList(query), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (!snapshot.hasData && query != null) + return Container( + padding: EdgeInsets.only(top: 200), + alignment: Alignment.topCenter, + child: CircularProgressIndicator(), + ); + List content = snapshot.data; + return ListView.builder( + scrollDirection: Axis.vertical, + itemCount: content.length, + itemBuilder: (BuildContext context, int index) { + return SearchResult( + onlinePodcast: content[index], + ); + }, + ); + }, + ); + } + + @override + List buildActions(BuildContext context) { + return [ + if (query.isEmpty) + IconButton( + tooltip: 'Voice Search', + icon: const Icon(Icons.mic), + onPressed: () { + query = 'TODO: implement voice input'; + }, + ) + else + IconButton( + tooltip: 'Clear', + icon: const Icon(Icons.clear), + onPressed: () { + query = ''; + }, + ), + ]; + } + + @override + Widget buildResults(BuildContext context) { + if (query.isEmpty) + return Container( + height: 10, + width: 10, + margin: EdgeInsets.only(top: 400), + child: Image.asset( + 'assets/listennote.png', + fit: BoxFit.fill, + ), + ); + return FutureBuilder( + future: getList(query), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (!snapshot.hasData && query != null) + return Container( + padding: EdgeInsets.only(top: 200), + alignment: Alignment.topCenter, + child: CircularProgressIndicator(), + ); + List content = snapshot.data; + return ListView.builder( + scrollDirection: Axis.vertical, + itemCount: content.length, + itemBuilder: (BuildContext context, int index) { + return SearchResult( + onlinePodcast: content[index], + ); + }, + ); + }, + ); + } +} + +class SearchResult extends StatefulWidget { + final OnlinePodcast onlinePodcast; + SearchResult({this.onlinePodcast, Key key}) : super(key: key); + @override + _SearchResultState createState() => _SearchResultState(); +} + +class _SearchResultState extends State { + bool _issubscribe; + bool _adding; + Future _subscribe(OnlinePodcast t) async { + if (mounted) + setState(() { + _adding = true; + }); + String _primaryColor; + await getColorFromUrl(t.image).then((color) { + print(color.toString()); + _primaryColor = color.toString(); + }); + var dbHelper = DBHelper(); + final PodcastLocal _pdt = + PodcastLocal(t.title, t.image, t.rss, _primaryColor, t.publisher); + _pdt.description = t.description; + print(t.title + t.rss); + await dbHelper.savePodcastLocal(_pdt); + final response = await Dio().get(t.rss); + int result = await dbHelper.savePodcastRss(response.data); + if (result == 0 && mounted) setState(() => _issubscribe = true); + } + + bool isXimalaya(String input) { + RegExp ximalaya = RegExp(r"ximalaya"); + return ximalaya.hasMatch(input); + } + + @override + void initState() { + super.initState(); + _issubscribe = false; + _adding = false; + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.symmetric(horizontal: 12.0), + child: ListTile( + leading: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(20.0)), + child: Image.network( + widget.onlinePodcast.image, + height: 40.0, + width: 40.0, + fit: BoxFit.fitWidth, + alignment: Alignment.center, + ), + ), + title: Text(widget.onlinePodcast.title), + subtitle: Text(widget.onlinePodcast.publisher), + trailing: isXimalaya(widget.onlinePodcast.rss) + ? OutlineButton(child: Text('Not Support'), onPressed: null) + : !_issubscribe + ? !_adding + ? OutlineButton( + child: Text('Subscribe', + style: TextStyle(color: Colors.blue)), + onPressed: () { + _subscribe(widget.onlinePodcast); + }) + : OutlineButton( + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.blue), + )), + onPressed: () {}, + ) + : OutlineButton(child: Text('Subscribe'), onPressed: null), + ), + ); + } +} diff --git a/lib/audio_player.dart b/lib/audio_player.dart new file mode 100644 index 0000000..22d0718 --- /dev/null +++ b/lib/audio_player.dart @@ -0,0 +1,504 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; +import 'package:provider/provider.dart'; +import 'package:network_image_to_byte/network_image_to_byte.dart'; + +import 'package:audiofileplayer/audiofileplayer.dart'; +import 'package:audiofileplayer/audio_system.dart'; +import 'package:logging/logging.dart'; +import 'package:flutter/material.dart'; +import 'package:marquee/marquee.dart'; +import 'package:flutter_downloader/flutter_downloader.dart'; +import 'class/audiostate.dart'; + +final Logger _logger = Logger('audiofileplayer'); + +class PlayerWidget extends StatefulWidget { + @override + _PlayerWidgetState createState() => _PlayerWidgetState(); +} + +class _PlayerWidgetState extends State { + static const String replay10ButtonId = 'replay10ButtonId'; + static const String newReleasesButtonId = 'newReleasesButtonId'; + static const String likeButtonId = 'likeButtonId'; + static const String pausenowButtonId = 'pausenowButtonId'; + static const String forwardButtonId = 'forwardButtonId'; + + Audio _backgroundAudio; + bool _backgroundAudioPlaying; + double _backgroundAudioDurationSeconds; + double _backgroundAudioPositionSeconds = 0; + bool _remoteAudioLoading; + String _remoteErrorMessage; + double _seekSliderValue = 0.0; + String url; + String _title; + String _feedtitle; + String _imgurl; + bool _isLoading; + + @override + void initState() { + super.initState(); + AudioSystem.instance.addMediaEventListener(_mediaEventListener); + _isLoading = false; + } + + void _initbackgroundAudioPlayer(String url) { + _remoteErrorMessage = null; + _remoteAudioLoading = true; + Provider.of(context, listen: false).audioState = AudioState.load; + + if (_backgroundAudioPlaying == true) _backgroundAudio?.pause(); + _backgroundAudio?.dispose(); + _backgroundAudio = Audio.loadFromRemoteUrl(url, + onDuration: (double durationSeconds) { + setState(() { + _backgroundAudioDurationSeconds = durationSeconds; + _remoteAudioLoading = false; + Provider.of(context, listen: false).audioState = + AudioState.play; + }); + _setNotification(); + }, + onPosition: (double positionSeconds) { + setState(() { + if (_backgroundAudioPositionSeconds < + _backgroundAudioDurationSeconds) { + _seekSliderValue = _backgroundAudioPositionSeconds / + _backgroundAudioDurationSeconds; + _backgroundAudioPositionSeconds = positionSeconds; + } else { + _seekSliderValue = 1; + _backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds; + } + }); + }, + onError: (String message) => setState(() { + _remoteErrorMessage = message; + _backgroundAudio.dispose(); + _backgroundAudio = null; + _backgroundAudioPlaying = false; + _remoteAudioLoading = false; + Provider.of(context, listen: false).audioState = + AudioState.error; + }), + onComplete: () => setState(() { + _backgroundAudioPlaying = false; + _remoteAudioLoading = false; + Provider.of(context, listen: false).audioState = + AudioState.complete; + }), + looping: false, + playInBackground: true) + ..play(); + } + + void _initbackgroundAudioPlayerLocal(String path) { + _remoteErrorMessage = null; + _remoteAudioLoading = true; + ByteData audio = getAudio(path); + Provider.of(context, listen: false).audioState = AudioState.load; + if (_backgroundAudioPlaying == true) _backgroundAudio?.pause(); + _backgroundAudio?.dispose(); + _backgroundAudio = Audio.loadFromByteData(audio, + onDuration: (double durationSeconds) { + setState(() { + _backgroundAudioDurationSeconds = durationSeconds; + _remoteAudioLoading = false; + }); + _setNotification(); + Provider.of(context, listen: false).audioState = + AudioState.play; + }, + onPosition: (double positionSeconds) { + setState(() { + if (_backgroundAudioPositionSeconds < + _backgroundAudioDurationSeconds) { + _seekSliderValue = _backgroundAudioPositionSeconds / + _backgroundAudioDurationSeconds; + _backgroundAudioPositionSeconds = positionSeconds; + } else { + _seekSliderValue = 1; + _backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds; + } + }); + }, + onError: (String message) => setState(() { + _remoteErrorMessage = message; + _backgroundAudio.dispose(); + _backgroundAudio = null; + _backgroundAudioPlaying = false; + _remoteAudioLoading = false; + Provider.of(context, listen: false).audioState = + AudioState.error; + }), + onComplete: () => setState(() { + _backgroundAudioPlaying = false; + _remoteAudioLoading = false; + Provider.of(context, listen: false).audioState = + AudioState.complete; + }), + looping: false, + playInBackground: true) + ..play(); + } + + Future _getFile(String url) async { + final task = await FlutterDownloader.loadTasksWithRawQuery( + query: "SELECT * FROM task WHERE url = '$url' AND status = 3"); + if (task.length != 0) { + String _filePath = task.first.savedDir + '/' + task.first.filename; + return _filePath; + } + return 'NotDownload'; + } + + ByteData getAudio(String path) { + File audioFile = File(path); + Uint8List audio = audioFile.readAsBytesSync(); + return ByteData.view(audio.buffer); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final url = Provider.of(context).audiourl; + if (url != this.url) { + setState(() { + this.url = url; + _title = Provider.of(context).title; + _feedtitle = Provider.of(context).feedtitle; + _imgurl = Provider.of(context).imageurl; + _backgroundAudioPlaying = true; + _isLoading = true; + _getFile(url).then((result) { + result == 'NotDownload' + ? _initbackgroundAudioPlayer(url) + : _initbackgroundAudioPlayerLocal(result); + }); + }); + } + } + + @override + void dispose() { + AudioSystem.instance.removeMediaEventListener(_mediaEventListener); + _backgroundAudio?.dispose(); + super.dispose(); + } + + static String _stringForSeconds(double seconds) { + if (seconds == null) return null; + return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; + } + + void _mediaEventListener(MediaEvent mediaEvent) { + _logger.info('App received media event of type: ${mediaEvent.type}'); + final MediaActionType type = mediaEvent.type; + if (type == MediaActionType.play) { + _resumeBackgroundAudio(); + } else if (type == MediaActionType.pause) { + _pauseBackgroundAudio(); + } else if (type == MediaActionType.playPause) { + _backgroundAudioPlaying + ? _pauseBackgroundAudio() + : _resumeBackgroundAudio(); + } else if (type == MediaActionType.stop) { + _stopBackgroundAudio(); + } else if (type == MediaActionType.seekTo) { + _backgroundAudio.seek(mediaEvent.seekToPositionSeconds); + AudioSystem.instance + .setPlaybackState(true, mediaEvent.seekToPositionSeconds); + } else if (type == MediaActionType.skipForward) { + final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds; + _forwardBackgroundAudio(skipIntervalSeconds); + _logger.info( + 'Skip-forward event had skipIntervalSeconds $skipIntervalSeconds.'); + } else if (type == MediaActionType.skipBackward) { + final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds; + _forwardBackgroundAudio(skipIntervalSeconds); + _logger.info( + 'Skip-backward event had skipIntervalSeconds $skipIntervalSeconds.'); + } else if (type == MediaActionType.custom) { + if (mediaEvent.customEventId == replay10ButtonId) { + _forwardBackgroundAudio(-10.0); + } else if (mediaEvent.customEventId == likeButtonId) { + _resumeBackgroundAudio(); + } else if (mediaEvent.customEventId == forwardButtonId) { + _forwardBackgroundAudio(30.0); + } else if (mediaEvent.customEventId == pausenowButtonId) { + _pauseBackgroundAudio(); + } + } + } + + Future _networkImageToByte(String url) async { + Uint8List byteImage = await networkImageToByte(url); + return byteImage; + } + + final _pauseButton = AndroidCustomMediaButton( + 'pausenow', pausenowButtonId, 'ic_stat_pause_circle_filled'); + final _replay10Button = AndroidCustomMediaButton( + 'replay10', replay10ButtonId, 'ic_stat_replay_10'); + final _forwardButton = AndroidCustomMediaButton( + 'forward', forwardButtonId, 'ic_stat_forward_30'); + final _playnowButton = AndroidCustomMediaButton( + 'playnow', likeButtonId, 'ic_stat_play_circle_filled'); + + Future _setNotification() async { + final Uint8List imageBytes = await _networkImageToByte(_imgurl); + AudioSystem.instance.setMetadata(AudioMetadata( + title: _title, + artist: _feedtitle, + album: _feedtitle, + genre: "Podcast", + durationSeconds: _backgroundAudioDurationSeconds, + artBytes: imageBytes)); + AudioSystem.instance + .setPlaybackState(true, _backgroundAudioPositionSeconds); + AudioSystem.instance.setAndroidNotificationButtons([ + AndroidMediaButtonType.pause, + _forwardButton, + AndroidMediaButtonType.stop, + ], androidCompactIndices: [ + 0, + 1 + ]); + + AudioSystem.instance.setSupportedMediaActions({ + MediaActionType.playPause, + MediaActionType.pause, + MediaActionType.next, + MediaActionType.previous, + MediaActionType.skipForward, + MediaActionType.skipBackward, + MediaActionType.seekTo, + MediaActionType.custom, + }, skipIntervalSeconds: 30); + } + + Future _resumeBackgroundAudio() async { + _backgroundAudio.resume(); + setState(() { + _backgroundAudioPlaying = true; + Provider.of(context, listen: false).audioState = + AudioState.play; + }); + + final Uint8List imageBytes = await _networkImageToByte(_imgurl); + AudioSystem.instance.setMetadata(AudioMetadata( + title: _title, + artist: _feedtitle, + album: _feedtitle, + genre: "Podcast", + durationSeconds: _backgroundAudioDurationSeconds, + artBytes: imageBytes)); + + AudioSystem.instance + .setPlaybackState(true, _backgroundAudioPositionSeconds); + + AudioSystem.instance.setAndroidNotificationButtons([ + AndroidMediaButtonType.pause, + _forwardButton, + AndroidMediaButtonType.stop, + ], androidCompactIndices: [ + 0, + 1 + ]); + + AudioSystem.instance.setSupportedMediaActions({ + MediaActionType.playPause, + MediaActionType.pause, + MediaActionType.next, + MediaActionType.previous, + MediaActionType.skipForward, + MediaActionType.skipBackward, + MediaActionType.seekTo, + MediaActionType.custom, + }, skipIntervalSeconds: 30); + } + + void _pauseBackgroundAudio() { + _backgroundAudio.pause(); + setState(() { + _backgroundAudioPlaying = false; + Provider.of(context, listen: false).audioState = + AudioState.pause; + }); + + AudioSystem.instance + .setPlaybackState(false, _backgroundAudioPositionSeconds); + + AudioSystem.instance.setAndroidNotificationButtons([ + AndroidMediaButtonType.play, + _forwardButton, + AndroidMediaButtonType.stop, + ], androidCompactIndices: [ + 0, + 1, + ]); + + AudioSystem.instance.setSupportedMediaActions({ + MediaActionType.playPause, + MediaActionType.play, + MediaActionType.next, + MediaActionType.previous, + }); + } + + void _stopBackgroundAudio() { + _backgroundAudio.pause(); + setState(() => _backgroundAudioPlaying = false); + AudioSystem.instance.stopBackgroundDisplay(); + } + + void _forwardBackgroundAudio(double seconds) { + final double forwardposition = _backgroundAudioPositionSeconds + seconds; + _backgroundAudio.seek(forwardposition); + AudioSystem.instance.setPlaybackState(true, forwardposition); + } + + @override + Widget build(BuildContext context) { + return !_isLoading + ? Center() + : Container( + padding: EdgeInsets.symmetric(horizontal: 10.0), + color: Colors.grey[100], + height: 120.0, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + alignment: Alignment.centerLeft, + child: _remoteErrorMessage != null + ? Text(_remoteErrorMessage, + style: + const TextStyle(color: const Color(0xFFFF0000))) + : Text( + _remoteAudioLoading ? 'Buffring...' : '', + style: TextStyle(color: Colors.blue), + ), + ), + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: Colors.blue[100], + inactiveTrackColor: Colors.grey[300], + trackHeight: 2.0, + thumbColor: Colors.blue[400], + thumbShape: + RoundSliderThumbShape(enabledThumbRadius: 5.0), + overlayColor: Colors.blue.withAlpha(32), + overlayShape: + RoundSliderOverlayShape(overlayRadius: 14.0), + ), + child: Slider( + value: _seekSliderValue, + onChanged: (double val) { + setState(() => _seekSliderValue = val); + final double positionSeconds = + val * _backgroundAudioDurationSeconds; + _backgroundAudio.seek(positionSeconds); + AudioSystem.instance + .setPlaybackState(true, positionSeconds); + }), + ), + Container( + height: 20.0, + padding: EdgeInsets.symmetric(horizontal: 10.0), + child: Row( + children: [ + Text( + _stringForSeconds(_backgroundAudioPositionSeconds) ?? + '', + style: TextStyle(fontSize: 10), + ), + Expanded( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 30), + alignment: Alignment.center, + child: (_title.length > 50) + ? Marquee( + text: _title, + style: TextStyle(fontWeight: FontWeight.bold), + scrollAxis: Axis.horizontal, + crossAxisAlignment: CrossAxisAlignment.start, + blankSpace: 30.0, + velocity: 50.0, + pauseAfterRound: Duration(seconds: 1), + startPadding: 30.0, + accelerationDuration: Duration(seconds: 1), + accelerationCurve: Curves.linear, + decelerationDuration: + Duration(milliseconds: 500), + decelerationCurve: Curves.easeOut, + ) + : Text( + _title, + style: TextStyle(fontWeight: FontWeight.bold), + ), + )), + Text( + _stringForSeconds(_backgroundAudioDurationSeconds) ?? + '', + style: TextStyle(fontSize: 10), + ), + ], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + padding: EdgeInsets.symmetric(horizontal: 30.0), + onPressed: _backgroundAudioPlaying + ? () => _forwardBackgroundAudio(-10) + : null, + iconSize: 25.0, + icon: Icon(Icons.replay_10), + color: Colors.black), + _backgroundAudioPlaying + ? IconButton( + padding: EdgeInsets.symmetric(horizontal: 30.0), + onPressed: _backgroundAudioPlaying + ? () { + _pauseBackgroundAudio(); + } + : null, + iconSize: 32.0, + icon: Icon(Icons.pause_circle_filled), + color: Colors.black) + : IconButton( + padding: EdgeInsets.symmetric(horizontal: 30.0), + onPressed: _backgroundAudioPlaying + ? null + : () { + _resumeBackgroundAudio(); + }, + iconSize: 32.0, + icon: Icon(Icons.play_circle_filled), + color: Colors.black), + IconButton( + padding: EdgeInsets.symmetric(horizontal: 30.0), + onPressed: _backgroundAudioPlaying + ? () => _forwardBackgroundAudio(30) + : null, + iconSize: 25.0, + icon: Icon(Icons.forward_30), + color: Colors.black), + /*IconButton( + onPressed: _isPlaying || _isPaused ? () => _stop() : null, + iconSize: 32.0, + icon: Icon(Icons.stop), + color: Colors.black), */ + ], + ), + ]), + ); + } +} diff --git a/lib/class/audiostate.dart b/lib/class/audiostate.dart new file mode 100644 index 0000000..b488742 --- /dev/null +++ b/lib/class/audiostate.dart @@ -0,0 +1,37 @@ +import 'package:flutter/foundation.dart'; + +enum AudioState {load, play, pause, complete, error} + +class Urlchange with ChangeNotifier { + String _audiourl; + String get audiourl => _audiourl; + set audioUrl(String playing) { + _audiourl = playing; + notifyListeners(); + } + + String _title; + String get title => _title; + set rssTitle(String title){ + _title = title; + } + + String _feedTitle; + String get feedtitle => _feedTitle; + set feedTitle(String feed){ + _feedTitle = feed; + } + + String _imageurl; + String get imageurl => _imageurl; + set imageUrl(String image){ + _imageurl = image; + } + + AudioState _audioState; + AudioState get audioState => _audioState; + set audioState(AudioState state){ + _audioState = state; + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/class/downloadstate.dart b/lib/class/downloadstate.dart new file mode 100644 index 0000000..8fc79db --- /dev/null +++ b/lib/class/downloadstate.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; + +enum DownloadState { stop, load, donwload, complete, error } + +class EpisodeDownload with ChangeNotifier { + String _title; + String get title => _title; + set title(String t) { + _title = t; + notifyListeners(); + } + DownloadState _downloadState = DownloadState.stop; + DownloadState get downloadState => _downloadState; + set downloadState(DownloadState state){ + _downloadState = state; + notifyListeners(); + } +} diff --git a/lib/class/episodebrief.dart b/lib/class/episodebrief.dart new file mode 100644 index 0000000..ec1856a --- /dev/null +++ b/lib/class/episodebrief.dart @@ -0,0 +1,27 @@ +class EpisodeBrief { + final String title; + String description; + final String pubDate; + final int enclosureLength; + final String enclosureUrl; + final String feedTitle; + final String imageUrl; + final String primaryColor; + final int liked; + final String downloaded; + final int duration; + final int explicit; + EpisodeBrief( + this.title, + this.enclosureUrl, + this.enclosureLength, + this.pubDate, + this.feedTitle, + this.imageUrl, + this.primaryColor, + this.liked, + this.downloaded, + this.duration, + this.explicit + ); +} diff --git a/lib/class/importompl.dart b/lib/class/importompl.dart new file mode 100644 index 0000000..35d31a8 --- /dev/null +++ b/lib/class/importompl.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; + +enum ImportState{start, import, complete, stop, error} + +class ImportOmpl extends ChangeNotifier{ + ImportState _importState = ImportState.stop; + String _rssTitle; + String get rsstitle => _rssTitle; + set rssTitle(String title){ + _rssTitle = title; + notifyListeners(); + } + ImportState get importState => _importState; + set importState(ImportState state){ + _importState = state; + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/class/podcastlocal.dart b/lib/class/podcastlocal.dart new file mode 100644 index 0000000..47dd50c --- /dev/null +++ b/lib/class/podcastlocal.dart @@ -0,0 +1,10 @@ + +class PodcastLocal { + final String title; + final String imageUrl; + final String rssUrl; + final String author; + String description; + final String primaryColor; + PodcastLocal(this.title, this.imageUrl, this.rssUrl, this.primaryColor, this.author); +} \ No newline at end of file diff --git a/lib/class/podcastrss.dart b/lib/class/podcastrss.dart new file mode 100644 index 0000000..7c83737 --- /dev/null +++ b/lib/class/podcastrss.dart @@ -0,0 +1,83 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'podcastrss.g.dart'; + +@JsonSerializable() +class Podcastrss{ + @_ConvertR() + final R rss; + Podcastrss({this.rss}); + factory Podcastrss.fromJson(Map json) => + _$PodcastrssFromJson(json); + Map toJson() => _$PodcastrssToJson(this); +} + +class _ConvertR implements JsonConverter{ + const _ConvertR(); + @override + R fromJson(Object json){ + return Rss.fromJson(json) as R; + } + @override + Object toJson(R object){ + return object; + } +} +@JsonSerializable() +class Rss{ + @_ConvertC() + final C channel; + Rss({this.channel}); + factory Rss.fromJson(Map json) => + _$RssFromJson(json); + Map toJson() => _$RssToJson(this); +} + +class _ConvertC implements JsonConverter{ + const _ConvertC(); + @override + C fromJson(Object json){ + return Channel.fromJson(json) as C; + } + @override + Object toJson(C object){ + return object; + } +} + +@JsonSerializable() +class Channel { + final String title; + final String link; + final String description; + @_ConvertE() + final List item; + Channel({this.title, this.link, this.description, this.item}); + factory Channel.fromJson(Map json) => + _$ChannelFromJson(json); + Map toJson() => _$ChannelToJson(this); +} + +class _ConvertE implements JsonConverter{ + const _ConvertE(); + @override + E fromJson(Object json){ + return EpisodeItem.fromJson(json) as E; + } + @override + Object toJson(E object){ + return object; + } +} + +@JsonSerializable() +class EpisodeItem{ + final String title; + final String link; + final String pubDate; + final String description; + EpisodeItem({this.title, this.link, this.pubDate, this.description} + ); + factory EpisodeItem.fromJson(Map json) => + _$EpisodeItemFromJson(json); + Map toJson() => _$EpisodeItemToJson(this); +} \ No newline at end of file diff --git a/lib/class/podcastrss.g.dart b/lib/class/podcastrss.g.dart new file mode 100644 index 0000000..aed659a --- /dev/null +++ b/lib/class/podcastrss.g.dart @@ -0,0 +1,66 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'podcastrss.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Podcastrss _$PodcastrssFromJson(Map json) { + return Podcastrss( + rss: json['rss'] == null ? null : _ConvertR().fromJson(json['rss'])); +} + +Map _$PodcastrssToJson(Podcastrss instance) => + { + 'rss': instance.rss == null ? null : _ConvertR().toJson(instance.rss) + }; + +Rss _$RssFromJson(Map json) { + return Rss( + channel: json['channel'] == null + ? null + : _ConvertC().fromJson(json['channel'])); +} + +Map _$RssToJson(Rss instance) => { + 'channel': instance.channel == null + ? null + : _ConvertC().toJson(instance.channel) + }; + +Channel _$ChannelFromJson(Map json) { + return Channel( + title: json['title'] as String, + link: json['link'] as String, + description: json['description'] as String, + item: (json['item'] as List) + ?.map((e) => e == null ? null : _ConvertE().fromJson(e)) + ?.toList()); +} + +Map _$ChannelToJson(Channel instance) => + { + 'title': instance.title, + 'link': instance.link, + 'description': instance.description, + 'item': instance.item + ?.map((e) => e == null ? null : _ConvertE().toJson(e)) + ?.toList() + }; + +EpisodeItem _$EpisodeItemFromJson(Map json) { + return EpisodeItem( + title: json['title'] as String, + link: json['link'] as String, + pubDate: json['pubDate'] as String, + description: json['description'] as String); +} + +Map _$EpisodeItemToJson(EpisodeItem instance) => + { + 'title': instance.title, + 'link': instance.link, + 'pubDate': instance.pubDate, + 'description': instance.description + }; diff --git a/lib/class/podcasts.dart b/lib/class/podcasts.dart new file mode 100644 index 0000000..088694b --- /dev/null +++ b/lib/class/podcasts.dart @@ -0,0 +1,112 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'podcasts.g.dart'; + +@JsonSerializable() +class Podcast{ + final String version; + final String title; + @JsonKey(name: 'homepage_url') + final String homepageUrl; + @JsonKey(name: 'feed_url') + final String feedUrl; + final String description; + @JsonKey(name: '_fireside') + @_ConvertF() + final F fireSide; + @JsonKey(name: 'items') + @_ConvertE() + final List items; + Podcast( + {this.version, this.title, this.homepageUrl, this.feedUrl, this.description, this.fireSide, this.items} + ); + factory Podcast.fromJson(Map json) => + _$PodcastFromJson(json); + Map toJson() => _$PodcastToJson(this); +} + +class _ConvertE implements JsonConverter{ + const _ConvertE(); + @override + E fromJson(Object json){ + return EpisodeItem.fromJson(json) as E; + } + @override + Object toJson(E object){ + return object; + } +} +class _ConvertF implements JsonConverter{ + const _ConvertF(); + @override + F fromJson(Object json){ + return FireSide.fromJson(json) as F; + } + @override + Object toJson(F object){ + return object; + } +} + +@JsonSerializable() + +class FireSide{ + final String pubdate; + final bool explicit; + final String copyright; + final String owner; + final String image; + FireSide({this.pubdate, this.explicit, this.copyright, this.owner, this.image}); + factory FireSide.fromJson(Map json) => + _$FireSideFromJson(json); + Map toJson() => _$FireSideToJson(this); +} + +@JsonSerializable() +class EpisodeItem{ + final String id; + final String title; + final String url; + @JsonKey(name: 'content_text') + final String contentText; + @JsonKey(name: 'content_html') + final String contentHtml; + final String summary; + @JsonKey(name: 'date_published') + final String datePublished; + @_ConvertA() + final List attachments; + EpisodeItem({this.id, this.title, this.url, this.contentText, this.contentHtml, this.summary, this.datePublished, this.attachments} + ); + factory EpisodeItem.fromJson(Map json) => + _$EpisodeItemFromJson(json); + Map toJson() => _$EpisodeItemToJson(this); +} + +class _ConvertA implements JsonConverter { + const _ConvertA(); + @override + A fromJson(Object json){ + return Attachment.fromJson(json) as A; + } + @override + Object toJson(A object){ + return object; + } +} + +@JsonSerializable() +class Attachment{ + final String url; + @JsonKey(name: 'mime_type') + final String mimeType; + @JsonKey(name: 'size_in_bytes') + final int sizeInBytes; + @JsonKey(name: 'duration_in_seconds') + final int durationInSeconds; + Attachment( + {this.url, this.mimeType, this.sizeInBytes, this.durationInSeconds} + ); + factory Attachment.fromJson(Map json) => + _$AttachmentFromJson(json); + Map toJson() => _$AttachmentToJson(this); +} \ No newline at end of file diff --git a/lib/class/podcasts.g.dart b/lib/class/podcasts.g.dart new file mode 100644 index 0000000..12f8aa7 --- /dev/null +++ b/lib/class/podcasts.g.dart @@ -0,0 +1,98 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'podcasts.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Podcast _$PodcastFromJson(Map json) { + return Podcast( + version: json['version'] as String, + title: json['title'] as String, + homepageUrl: json['homepage_url'] as String, + feedUrl: json['feed_url'] as String, + description: json['description'] as String, + fireSide: json['_fireside'] == null + ? null + : _ConvertF().fromJson(json['_fireside']), + items: (json['items'] as List) + ?.map((e) => e == null ? null : _ConvertE().fromJson(e)) + ?.toList()); +} + +Map _$PodcastToJson(Podcast instance) => + { + 'version': instance.version, + 'title': instance.title, + 'homepage_url': instance.homepageUrl, + 'feed_url': instance.feedUrl, + 'description': instance.description, + '_fireside': instance.fireSide == null + ? null + : _ConvertF().toJson(instance.fireSide), + 'items': instance.items + ?.map((e) => e == null ? null : _ConvertE().toJson(e)) + ?.toList() + }; + +FireSide _$FireSideFromJson(Map json) { + return FireSide( + pubdate: json['pubdate'] as String, + explicit: json['explicit'] as bool, + copyright: json['copyright'] as String, + owner: json['owner'] as String, + image: json['image'] as String); +} + +Map _$FireSideToJson(FireSide instance) => { + 'pubdate': instance.pubdate, + 'explicit': instance.explicit, + 'copyright': instance.copyright, + 'owner': instance.owner, + 'image': instance.image + }; + +EpisodeItem _$EpisodeItemFromJson(Map json) { + return EpisodeItem( + id: json['id'] as String, + title: json['title'] as String, + url: json['url'] as String, + contentText: json['content_text'] as String, + contentHtml: json['content_html'] as String, + summary: json['summary'] as String, + datePublished: json['date_published'] as String, + attachments: (json['attachments'] as List) + ?.map((e) => e == null ? null : _ConvertA().fromJson(e)) + ?.toList()); +} + +Map _$EpisodeItemToJson(EpisodeItem instance) => + { + 'id': instance.id, + 'title': instance.title, + 'url': instance.url, + 'content_text': instance.contentText, + 'content_html': instance.contentHtml, + 'summary': instance.summary, + 'date_published': instance.datePublished, + 'attachments': instance.attachments + ?.map((e) => e == null ? null : _ConvertA().toJson(e)) + ?.toList() + }; + +Attachment _$AttachmentFromJson(Map json) { + return Attachment( + url: json['url'] as String, + mimeType: json['mime_type'] as String, + sizeInBytes: json['size_in_bytes'] as int, + durationInSeconds: json['duration_in_seconds'] as int); +} + +Map _$AttachmentToJson(Attachment instance) => + { + 'url': instance.url, + 'mime_type': instance.mimeType, + 'size_in_bytes': instance.sizeInBytes, + 'duration_in_seconds': instance.durationInSeconds + }; diff --git a/lib/class/searchpodcast.dart b/lib/class/searchpodcast.dart new file mode 100644 index 0000000..7f0358c --- /dev/null +++ b/lib/class/searchpodcast.dart @@ -0,0 +1,54 @@ +import 'package:json_annotation/json_annotation.dart'; +part 'searchpodcast.g.dart'; + +@JsonSerializable() +class SearchPodcast

{ + @_ConvertP() + final List

results; + @JsonKey(name: 'next_offset') + final int nextOffset; + final int total; + final int count; + SearchPodcast( + {this.results, this.nextOffset, this.total, this.count} + ); + factory SearchPodcast.fromJson(Map json) => + _$SearchPodcastFromJson

(json); + Map toJson() => _$SearchPodcastToJson(this); +} + +class _ConvertP

implements JsonConverter{ + const _ConvertP(); + @override + P fromJson(Object json){ + return OnlinePodcast.fromJson(json) as P; + } + @override + Object toJson(P object){ + return object; + } +} + +@JsonSerializable() +class OnlinePodcast{ + @JsonKey(name: 'earliest_pub_date_ms') + final int earliestPubDate; + @JsonKey(name: 'title_original') + final String title; + final String rss; + @JsonKey(name: 'lastest_pub_date_ms') + final int lastestPubDate; + @JsonKey(name: 'description_original') + final String description; + @JsonKey(name: 'total_episodes') + final int count; + final String image; + @JsonKey(name: 'publisher_original') + final String publisher; + OnlinePodcast( + {this.earliestPubDate, this.title, this.count, this.description, this.image, this.lastestPubDate, this.rss, this.publisher} + ); + factory OnlinePodcast.fromJson(Map json) => + _$OnlinePodcastFromJson(json); + Map toJson() => _$OnlinePodcastToJson(this); +} \ No newline at end of file diff --git a/lib/class/searchpodcast.g.dart b/lib/class/searchpodcast.g.dart new file mode 100644 index 0000000..b9a0dda --- /dev/null +++ b/lib/class/searchpodcast.g.dart @@ -0,0 +1,51 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'searchpodcast.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SearchPodcast

_$SearchPodcastFromJson

(Map json) { + return SearchPodcast

( + results: (json['results'] as List) + ?.map((e) => e == null ? null : _ConvertP

().fromJson(e)) + ?.toList(), + nextOffset: json['next_offset'] as int, + total: json['total'] as int, + count: json['count'] as int); +} + +Map _$SearchPodcastToJson

(SearchPodcast

instance) => + { + 'results': instance.results + ?.map((e) => e == null ? null : _ConvertP

().toJson(e)) + ?.toList(), + 'next_offset': instance.nextOffset, + 'total': instance.total, + 'count': instance.count + }; + +OnlinePodcast _$OnlinePodcastFromJson(Map json) { + return OnlinePodcast( + earliestPubDate: json['earliest_pub_date_ms'] as int, + title: json['title_original'] as String, + count: json['total_episodes'] as int, + description: json['description_original'] as String, + image: json['image'] as String, + lastestPubDate: json['lastest_pub_date_ms'] as int, + rss: json['rss'] as String, + publisher: json['publisher_original'] as String); +} + +Map _$OnlinePodcastToJson(OnlinePodcast instance) => + { + 'earliest_pub_date_ms': instance.earliestPubDate, + 'title_original': instance.title, + 'rss': instance.rss, + 'lastest_pub_date_ms': instance.lastestPubDate, + 'description_original': instance.description, + 'total_episodes': instance.count, + 'image': instance.image, + 'publisher_original': instance.publisher + }; diff --git a/lib/class/sqflite_localpodcast.dart b/lib/class/sqflite_localpodcast.dart new file mode 100644 index 0000000..fd522b9 --- /dev/null +++ b/lib/class/sqflite_localpodcast.dart @@ -0,0 +1,451 @@ +import 'package:sqflite/sqflite.dart'; +import 'dart:async'; +import 'dart:io' as io; +import 'package:path/path.dart'; +import 'package:intl/intl.dart'; +import 'package:flutter_downloader/flutter_downloader.dart'; +import 'package:path_provider/path_provider.dart'; +import 'podcastlocal.dart'; +import 'episodebrief.dart'; +import '../webfeed/webfeed.dart'; + +class DBHelper { + static Database _db; + Future get database async { + if (_db != null) return _db; + _db = await initDb(); + return _db; + } + + initDb() async { + io.Directory documentsDirectory = await getApplicationDocumentsDirectory(); + String path = join(documentsDirectory.path, "podcasts.db"); + Database theDb = await openDatabase(path, version: 1, onCreate: _onCreate); + return theDb; + } + + void _onCreate(Database db, int version) async { + await db.execute( + """CREATE TABLE PodcastLocal(id INTEGER PRIMARY KEY,title TEXT, + imageUrl TEXT,rssUrl TEXT UNIQUE,primaryColor TEXT,author TEXT, description TEXT, add_date INTEGER)"""); + await db + .execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT, + enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT, + description TEXT, feed_title TEXT, feed_link TEXT, milliseconds INTEGER, + duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0, + downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0)"""); + } + + Future> getPodcastLocal() async { + var dbClient = await database; + List list = await dbClient.rawQuery( + 'SELECT title, imageUrl, rssUrl, primaryColor, author FROM PodcastLocal ORDER BY add_date DESC'); + List podcastLocal = List(); + for (int i = 0; i < list.length; i++) { + podcastLocal.add(PodcastLocal( + list[i]['title'], + list[i]['imageUrl'], + list[i]['rssUrl'], + list[i]['primaryColor'], + list[i]['author'], + )); + } + print(podcastLocal.length); + return podcastLocal; + } + + Future savePodcastLocal(PodcastLocal podcastLocal) async { + print('save'); + int _milliseconds = DateTime.now().millisecondsSinceEpoch; + var dbClient = await database; + await dbClient.transaction((txn) async { + return await txn.rawInsert( + """INSERT OR IGNORE INTO PodcastLocal (title, imageUrl, rssUrl, + primaryColor, author, description, add_date) VALUES(?, ?, ?, ?, ?, ?, ?)""", + [ + podcastLocal.title, + podcastLocal.imageUrl, + podcastLocal.rssUrl, + podcastLocal.primaryColor, + podcastLocal.author, + podcastLocal.description, + _milliseconds + ]); + }); + } + + Future delPodcastLocal(String title) async { + print('deleted'); + var dbClient = await database; + await dbClient + .rawDelete('DELETE FROM PodcastLocal WHERE title =?', [title]); + List list = await dbClient.rawQuery( + """SELECT downloaded FROM Episodes WHERE downloaded != 'ND' AND feed_title = ?""", + [title]); + for(int i=0; i < list.length; i++){ + if(list[i] != null) + FlutterDownloader.remove(taskId: list[i]['downloaded'], shouldDeleteContent: true); + print('Removed all download task'); + } + await dbClient + .rawDelete('DELETE FROM Episodes WHERE feed_title=?', [title]); + } + + Future getImageUrl(String title) async { + var dbClient = await database; + List list = await dbClient + .rawQuery('SELECT imageUrl FROM PodcastLocal WHERE title = ?', [title]); + String url = list[0]['imageUrl']; + return url; + } + + int stringToDate(String s) { + var months = { + 'Jan': 1, + 'Feb': 2, + 'Mar': 3, + 'Apr': 4, + 'May': 5, + 'Jun': 6, + 'Jul': 7, + 'Aug': 8, + 'Sep': 9, + 'Oct': 10, + 'Nov': 11, + 'Dec': 12 + }; + int y; + int m; + int d; + int h; + int min; + int sec; + int result; + try { + y = int.parse(s.substring(12, 16)); + } catch (e) { + y = 0; + } + + try { + m = months[s.substring(8, 11)]; + } catch (e) { + m = 0; + } + try { + d = int.parse(s.substring(5, 7)); + } catch (e) { + d = 0; + } + try { + h = int.parse(s.substring(17, 19)); + } catch (e) { + h = 0; + } + try { + min = int.parse(s.substring(20, 22)); + } catch (e) { + min = 0; + } + try { + sec = int.parse(s.substring(23, 25)); + } catch (e) { + sec = 0; + } + try { + result = DateTime(y, m, d, h, min, sec).millisecondsSinceEpoch; + } catch (e) { + result = 0; + } + return result; + } + + static _parsePubDate(String pubDate) { + if (pubDate == null) return null; + return DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate); + } + + int getExplicit(bool b) { + int result; + if (b == true) { + result = 1; + return result; + } else { + result = 0; + return result; + } + } + + bool isXimalaya(String input) { + RegExp ximalaya = RegExp(r"ximalaya.com"); + return ximalaya.hasMatch(input); + } + + Future savePodcastRss(String rss) async { + String _title; + String _url; + String _description; + var _p = RssFeed.parse(rss); + int _result = _p.items.length; + var dbClient = await database; + int _count = Sqflite.firstIntValue(await dbClient.rawQuery( + 'SELECT COUNT(*) FROM Episodes WHERE feed_title = ?', [_p.title])); + print(_count); + if (_count == _result) { + _result = 0; + return _result; + } else { + for (int i = 0; i < (_result - _count); i++) { + print(_p.items[i].title); + _p.items[i].itunes.title != null + ? _title = _p.items[i].itunes.title + : _title = _p.items[i].title; + _p.items[i].itunes.summary != null + ? _description = _p.items[i].itunes.summary + : _description = _p.items[i].description; + isXimalaya(_p.items[i].enclosure.url) + ? _url = _p.items[i].enclosure.url.split('=').last + : _url = _p.items[i].enclosure.url; + final _length = _p.items[i].enclosure.length; + final _pubDate = _p.items[i].pubDate; + final DateTime _date = _parsePubDate(_pubDate); + final _milliseconds = _date.millisecondsSinceEpoch; + final _duration = _p.items[i].itunes.duration.inMinutes; + final _explicit = getExplicit(_p.items[i].itunes.explicit); + if (_p.items[i].enclosure.url != null) { + await dbClient.transaction((txn) { + return txn.rawInsert( + """INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate, + description, feed_title, milliseconds, duration, explicit) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)""", + [ + _title, + _url, + _length, + _pubDate, + _description, + _p.title, + _milliseconds, + _duration, + _explicit, + ]); + }); + } + } + _result = 0; + return _result; + } + } + + Future> getRssItem(String title) async { + var dbClient = await database; + List episodes = List(); + List list = await dbClient + .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, + E.pubDate, E.feed_title, E.duration, E.explicit, E.liked, + E.downloaded, P.imageUrl, P.primaryColor + FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_title = P.title + where E.feed_title = ? ORDER BY E.milliseconds DESC""", [title]); + for (int x = 0; x < list.length; x++) { + episodes.add(EpisodeBrief( + list[x]['title'], + list[x]['enclosure_url'], + list[x]['enclosure_length'], + list[x]['pubDate'], + list[x]['feed_title'], + list[x]['imageUrl'], + list[x]['primaryColor'], + list[x]['liked'], + list[x]['downloaded'], + list[x]['duration'], + list[x]['explicit'])); + } + print(episodes.length); + print(title); + return episodes; + } + + Future> getRssItemTop(String title) async { + var dbClient = await database; + List episodes = List(); + List list = await dbClient.rawQuery( + """SELECT E.title, E.enclosure_url, E.enclosure_length, + E.pubDate, E.feed_title, E.duration, E.explicit, E.liked, + E.downloaded, P.imageUrl, P.primaryColor + FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_title = P.title + where E.feed_title = ? ORDER BY E.milliseconds DESC LIMIT 3""", + [title]); + for (int x = 0; x < list.length; x++) { + episodes.add(EpisodeBrief( + list[x]['title'], + list[x]['enclosure_url'], + list[x]['enclosure_length'], + list[x]['pubDate'], + list[x]['feed_title'], + list[x]['imageUrl'], + list[x]['primaryColor'], + list[x]['liked'], + list[x]['downloaded'], + list[x]['duration'], + list[x]['explicit'])); + } + print(episodes.length); + print(title); + return episodes; + } + + Future getRssItemDownload(String url) async { + var dbClient = await database; + EpisodeBrief episode; + List list = await dbClient.rawQuery( + """SELECT E.title, E.enclosure_url, E.enclosure_length, + E.pubDate, E.feed_title, E.duration, E.explicit, E.liked, + E.downloaded, P.imageUrl, P.primaryColor + FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_title = P.title + where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""", + [url]); + + if (list != null) + episode = EpisodeBrief( + list.first['title'], + list.first['enclosure_url'], + list.first['enclosure_length'], + list.first['pubDate'], + list.first['feed_title'], + list.first['imageUrl'], + list.first['primaryColor'], + list.first['liked'], + list.first['downloaded'], + list.first['duration'], + list.first['explicit']); + return episode; + } + + Future> getRecentRssItem() async { + var dbClient = await database; + List episodes = List(); + List list = await dbClient + .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, + E.pubDate, E.feed_title, E.duration, E.explicit, E.liked, + E.downloaded, P.imageUrl, P.primaryColor + FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_title = P.title + ORDER BY E.milliseconds DESC LIMIT 99"""); + for (int x = 0; x < list.length; x++) { + episodes.add(EpisodeBrief( + list[x]['title'], + list[x]['enclosure_url'], + list[x]['enclosure_length'], + list[x]['pubDate'], + list[x]['feed_title'], + list[x]['imageUrl'], + list[x]['primaryColor'], + list[x]['liked'], + list[x]['doanloaded'], + list[x]['duration'], + list[x]['explicit'])); + } + print(episodes.length); + return episodes; + } + + Future> getLikedRssItem() async { + var dbClient = await database; + List episodes = List(); + List list = await dbClient.rawQuery( + """SELECT E.title, E.enclosure_url, E.enclosure_length, E.pubDate, + E.feed_title, E.duration, E.explicit, E.liked, E.downloaded, P.imageUrl, + P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_title = P.title + WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT 99"""); + for (int x = 0; x < list.length; x++) { + episodes.add(EpisodeBrief( + list[x]['title'], + list[x]['enclosure_url'], + list[x]['enclosure_length'], + list[x]['pubDate'], + list[x]['feed_title'], + list[x]['imageUrl'], + list[x]['primaryColor'], + list[x]['liked'], + list[x]['downloaded'], + list[x]['duration'], + list[x]['explicit'])); + } + print(episodes.length); + return episodes; + } + + Future setLiked(String title) async { + var dbClient = await database; + int count = await dbClient + .rawUpdate("UPDATE Episodes SET liked = 1 WHERE title = ?", [title]); + print('liked'); + return count; + } + + Future setUniked(String title) async { + var dbClient = await database; + int count = await dbClient + .rawUpdate("UPDATE Episodes SET liked = 0 WHERE title = ?", [title]); + print('unliked'); + return count; + } + + Future saveDownloaded(String url, String id) async { + var dbClient = await database; + int _milliseconds = DateTime.now().millisecondsSinceEpoch; + int count = await dbClient.rawUpdate( + "UPDATE Episodes SET downloaded = ?, download_date = ? WHERE enclosure_url = ?", + [id, _milliseconds, url]); + print('Downloaded ' + url); + return count; + } + + Future delDownloaded(String url) async { + var dbClient = await database; + int count = await dbClient.rawUpdate( + "UPDATE Episodes SET downloaded = 'ND' WHERE enclosure_url = ?", [url]); + print('Deleted ' + url); + return count; + } + + Future> getDownloadedRssItem() async { + var dbClient = await database; + List episodes = List(); + List list = await dbClient.rawQuery( + """SELECT E.title, E.enclosure_url, E.enclosure_length, E.pubDate, + E.feed_title, E.duration, E.explicit, E.liked, E.downloaded, P.imageUrl, + P.primaryColor FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_title = P.title + WHERE E.downloaded != 'ND' ORDER BY E.download_date DESC LIMIT 99"""); + for (int x = 0; x < list.length; x++) { + episodes.add(EpisodeBrief( + list[x]['title'], + list[x]['enclosure_url'], + list[x]['enclosure_length'], + list[x]['pubDate'], + list[x]['feed_title'], + list[x]['imageUrl'], + list[x]['primaryColor'], + list[x]['liked'], + list[x]['downloaded'], + list[x]['duration'], + list[x]['explicit'])); + } + print(episodes.length); + return episodes; + } + + Future getDescription(String title) async { + var dbClient = await database; + List list = await dbClient + .rawQuery('SELECT description FROM Episodes WHERE title = ?', [title]); + String description = list[0]['description']; + return description; + } + + Future getFeedDescription(String title) async { + var dbClient = await database; + List list = await dbClient.rawQuery( + 'SELECT description FROM PodcastLocal WHERE title = ?', [title]); + String description = list[0]['description']; + return description; + } +} diff --git a/lib/episodedetail.dart b/lib/episodedetail.dart new file mode 100644 index 0000000..e508b90 --- /dev/null +++ b/lib/episodedetail.dart @@ -0,0 +1,533 @@ +import 'dart:convert'; +import 'dart:math' as math; + +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'class/audiostate.dart'; +import 'class/episodebrief.dart'; +import 'class/sqflite_localpodcast.dart'; +import 'episodedownload.dart'; + +enum DownloadState { stop, load, donwload, complete, error } + +class EpisodeDetail extends StatefulWidget { + final EpisodeBrief episodeItem; + final String heroTag; + EpisodeDetail({this.episodeItem, this.heroTag, Key key}) : super(key: key); + + @override + _EpisodeDetailState createState() => _EpisodeDetailState(); +} + +class _EpisodeDetailState extends State { + final textstyle = TextStyle(fontSize: 15.0, color: Colors.black); + double downloadProgress; + Color _c; + bool _loaddes; + + Future getSDescription(String title) async { + var dbHelper = DBHelper(); + widget.episodeItem.description = await dbHelper.getDescription(title); + if (mounted) + setState(() { + _loaddes = true; + }); + } + + _launchUrl(String url) async { + if (await canLaunch(url)) { + await launch(url); + } else { + throw 'Could not launch $url'; + } +} + @override + void initState() { + super.initState(); + _loaddes = false; + getSDescription(widget.episodeItem.title); + } + + @override + Widget build(BuildContext context) { + var color = json.decode(widget.episodeItem.primaryColor); + (color[0] > 200 && color[1] > 200 && color[2] > 200) + ? _c = Color.fromRGBO( + (255 - color[0]), 255 - color[1], 255 - color[2], 1.0) + : _c = Color.fromRGBO(color[0], color[1], color[2], 0.8); + + return Scaffold( + backgroundColor: Colors.grey[100], + appBar: AppBar( + title: Text(widget.episodeItem.feedTitle), + elevation: 0.0, + centerTitle: true, + backgroundColor: Colors.grey[100], + ), + body: Container( + color: Colors.grey[100], + padding: EdgeInsets.all(12.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 12.0), + margin: EdgeInsets.only(bottom: 10.0), + alignment: Alignment.topLeft, + child: Text( + widget.episodeItem.title, + style: Theme.of(context).textTheme.title, + ), + ), + Container( + padding: EdgeInsets.all(12.0), + height: 50, + child: Row( + children: [ + (widget.episodeItem.explicit == 1) + ? Container( + decoration: BoxDecoration( + color: Colors.red[800], + shape: BoxShape.circle), + height: 25.0, + margin: EdgeInsets.only(right: 10.0), + padding: EdgeInsets.symmetric(horizontal: 10.0), + alignment: Alignment.center, + child: Text('E', + style: TextStyle(color: Colors.white))) + : Center(), + Container( + decoration: BoxDecoration( + color: Colors.cyan[300], + borderRadius: + BorderRadius.all(Radius.circular(15.0))), + height: 30.0, + margin: EdgeInsets.only(right: 10.0), + padding: EdgeInsets.symmetric(horizontal: 10.0), + alignment: Alignment.center, + child: Text( + (widget.episodeItem.duration).toString() + 'mins', + style: textstyle), + ), + Container( + decoration: BoxDecoration( + color: Colors.lightBlue[300], + borderRadius: + BorderRadius.all(Radius.circular(15.0))), + height: 30.0, + margin: EdgeInsets.only(right: 10.0), + padding: EdgeInsets.symmetric(horizontal: 10.0), + alignment: Alignment.center, + child: Text( + ((widget.episodeItem.enclosureLength) ~/ 1000000) + .toString() + + 'MB', + style: textstyle), + ), + Container( + decoration: BoxDecoration( + color: Colors.lightGreen[300], + borderRadius: + BorderRadius.all(Radius.circular(15.0))), + height: 30.0, + alignment: Alignment.center, + margin: EdgeInsets.only(right: 10.0), + padding: EdgeInsets.symmetric(horizontal: 10.0), + child: Text( + widget.episodeItem.pubDate.substring(0, 16), + style: textstyle), + ), + ], + ), + ), + ], + ), + ), + Expanded( + child: Container( + padding: EdgeInsets.only(left: 12.0, right: 12.0, top: 5.0), + child: SingleChildScrollView( + child: (widget.episodeItem.description != null && _loaddes) + ? Html(data: widget.episodeItem.description, + onLinkTap: (url){ + _launchUrl(url); + }, + useRichText: true, + ) + : Center(), + ), + ), + ), + MenuBar( + episodeItem: widget.episodeItem, + heroTag: widget.heroTag, + ), + ], + ), + ), + ); + } +} + +class MenuBar extends StatefulWidget { + final EpisodeBrief episodeItem; + final String heroTag; + MenuBar({this.episodeItem, this.heroTag, Key key}) : super(key: key); + @override + _MenuBarState createState() => _MenuBarState(); +} + +class _MenuBarState extends State { + bool _liked; + int _like; + + Future saveLiked(String title) async { + var dbHelper = DBHelper(); + int result = await dbHelper.setLiked(title); + if (result == 1 && mounted) setState(() => _liked = true); + return result; + } + + Future setUnliked(String title) async { + var dbHelper = DBHelper(); + int result = await dbHelper.setUniked(title); + if (result == 1 && mounted) + setState(() { + _liked = false; + _like = 0; + }); + return result; + } + + @override + void initState() { + super.initState(); + _liked = false; + _like = widget.episodeItem.liked; + } + + @override + Widget build(BuildContext context) { + final urlChange = Provider.of(context); + return Consumer( + builder: (context, urlchange, _) => Container( + height: 50.0, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + (widget.episodeItem.title == urlChange.title && + urlChange.audioState == AudioState.play) + ? ImageRotate( + url: widget.episodeItem.imageUrl, + ) + : Hero( + tag: widget.episodeItem.enclosureUrl + widget.heroTag, + child: Container( + padding: EdgeInsets.all(10.0), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(15.0)), + child: Container( + height: 30.0, + width: 30.0, + color: Colors.white, + child: CachedNetworkImage( + imageUrl: widget.episodeItem.imageUrl, + ), + ), + ), + ), + ), + (_like == 0 && !_liked) + ? IconButton( + icon: Icon( + Icons.favorite_border, + color: Colors.grey[700], + ), + onPressed: () { + saveLiked(widget.episodeItem.title); + }, + ) + : IconButton( + icon: Icon( + Icons.favorite, + color: Colors.red, + ), + onPressed: () { + setUnliked(widget.episodeItem.title); + }, + ), + DownloadButton(episodeBrief: widget.episodeItem), + IconButton( + icon: Icon(Icons.playlist_add, color: Colors.grey[700]), + onPressed: () {/*TODO*/}, + ), + Spacer(), + (widget.episodeItem.title != urlchange.title) + ? IconButton( + icon: Icon( + Icons.play_arrow, + color: Colors.grey[700], + ), + onPressed: () { + urlChange.audioUrl = widget.episodeItem.enclosureUrl; + urlChange.rssTitle = widget.episodeItem.title; + urlChange.feedTitle = widget.episodeItem.feedTitle; + urlChange.imageUrl = widget.episodeItem.imageUrl; + }, + ) + : (widget.episodeItem.title == urlchange.title && + urlchange.audioState == AudioState.play) + ? Container( + padding: EdgeInsets.only(right: 15), + child: SizedBox( + width: 15, height: 15, child: WaveLoader())) + : Container( + padding: EdgeInsets.only(right: 15), + child: SizedBox( + width: 15, + height: 15, + child: LineLoader(), + ), + ), + ], + ), + ), + ); + } +} + +class LinePainter extends CustomPainter { + double _fraction; + Paint _paint; + LinePainter(this._fraction) { + _paint = Paint() + ..color = Colors.blue + ..strokeWidth = 2.0 + ..strokeCap = StrokeCap.round; + } + + @override + void paint(Canvas canvas, Size size) { + canvas.drawLine(Offset(0, size.height / 2.0), + Offset(size.width * _fraction, size.height / 2.0), _paint); + } + + @override + bool shouldRepaint(LinePainter oldDelegate) { + return oldDelegate._fraction != _fraction; + } +} + +class LineLoader extends StatefulWidget { + @override + _LineLoaderState createState() => _LineLoaderState(); +} + +class _LineLoaderState extends State + with SingleTickerProviderStateMixin { + double _fraction = 0.0; + Animation animation; + AnimationController controller; + @override + void initState() { + super.initState(); + controller = AnimationController( + vsync: this, duration: Duration(milliseconds: 1000)); + animation = Tween(begin: 0.0, end: 1.0).animate(controller) + ..addListener(() { + if (mounted) + setState(() { + _fraction = animation.value; + }); + }); + controller.forward(); + controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + controller.reset(); + } else if (status == AnimationStatus.dismissed) { + controller.forward(); + } + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return CustomPaint(painter: LinePainter(_fraction)); + } +} + +class WavePainter extends CustomPainter { + double _fraction; + double _value; + WavePainter(this._fraction); + @override + void paint(Canvas canvas, Size size) { + if (_fraction < 0.5) { + _value = _fraction; + } else { + _value = 1 - _fraction; + } + Path _path = Path(); + Paint _paint = Paint() + ..color = Colors.blue + ..strokeWidth = 2.0 + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + _path.moveTo(0, size.height / 2); + _path.lineTo(0, size.height / 2 + size.height * _value * 0.2); + _path.moveTo(0, size.height / 2); + _path.lineTo(0, size.height / 2 - size.height * _value * 0.2); + _path.moveTo(size.width / 4, size.height / 2); + _path.lineTo(size.width / 4, size.height / 2 + size.height * _value * 0.8); + _path.moveTo(size.width / 4, size.height / 2); + _path.lineTo(size.width / 4, size.height / 2 - size.height * _value * 0.8); + _path.moveTo(size.width / 2, size.height / 2); + _path.lineTo(size.width / 2, size.height / 2 + size.height * _value * 0.5); + _path.moveTo(size.width / 2, size.height / 2); + _path.lineTo(size.width / 2, size.height / 2 - size.height * _value * 0.5); + _path.moveTo(size.width * 3 / 4, size.height / 2); + _path.lineTo( + size.width * 3 / 4, size.height / 2 + size.height * _value * 0.6); + _path.moveTo(size.width * 3 / 4, size.height / 2); + _path.lineTo( + size.width * 3 / 4, size.height / 2 - size.height * _value * 0.6); + _path.moveTo(size.width, size.height / 2); + _path.lineTo(size.width, size.height / 2 + size.height * _value * 0.2); + _path.moveTo(size.width, size.height / 2); + _path.lineTo(size.width, size.height / 2 - size.height * _value * 0.2); + canvas.drawPath(_path, _paint); + } + + @override + bool shouldRepaint(WavePainter oldDelegate) { + return oldDelegate._fraction != _fraction; + } +} + +class WaveLoader extends StatefulWidget { + @override + _WaveLoaderState createState() => _WaveLoaderState(); +} + +class _WaveLoaderState extends State + with SingleTickerProviderStateMixin { + double _fraction = 0.0; + Animation animation; + AnimationController _controller; + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, duration: Duration(milliseconds: 1000)); + animation = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + if (mounted) + setState(() { + _fraction = animation.value; + }); + }); + _controller.forward(); + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _controller.reset(); + } else if (status == AnimationStatus.dismissed) { + _controller.forward(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return CustomPaint(painter: WavePainter(_fraction)); + } +} + +class ImageRotate extends StatefulWidget { + final String url; + ImageRotate({this.url, Key key}) : super(key: key); + @override + _ImageRotateState createState() => _ImageRotateState(); +} + +class _ImageRotateState extends State + with SingleTickerProviderStateMixin { + Animation _animation; + AnimationController _controller; + double _value; + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: 2000), + ); + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + if (mounted) + setState(() { + _value = _animation.value; + }); + }); + _controller.forward(); + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _controller.reset(); + } else if (status == AnimationStatus.dismissed) { + _controller.forward(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Transform.rotate( + angle: 2 * math.pi * _value, + child: Container( + padding: EdgeInsets.all(10.0), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(15.0)), + child: Container( + height: 30.0, + width: 30.0, + color: Colors.white, + child: CachedNetworkImage( + imageUrl: widget.url, + ), + ), + ), + ), + ); + } +} diff --git a/lib/episodedownload.dart b/lib/episodedownload.dart new file mode 100644 index 0000000..1d981d9 --- /dev/null +++ b/lib/episodedownload.dart @@ -0,0 +1,282 @@ +import 'dart:isolate'; +import 'dart:ui'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'package:path_provider/path_provider.dart'; +import 'package:flutter_downloader/flutter_downloader.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'class/episodebrief.dart'; +import 'class/sqflite_localpodcast.dart'; + +class DownloadButton extends StatefulWidget { + final EpisodeBrief episodeBrief; + DownloadButton({this.episodeBrief, Key key}) : super(key: key); + @override + _DownloadButtonState createState() => _DownloadButtonState(); +} + +class _DownloadButtonState extends State { + _TaskInfo _task; + bool _isLoading; + bool _permissionReady; + String _localPath; + ReceivePort _port = ReceivePort(); + + Future _getPath() async { + final dir = await getExternalStorageDirectory(); + return dir.path; + } + + @override + void initState() { + super.initState(); + + _bindBackgroundIsolate(); + + FlutterDownloader.registerCallback(downloadCallback); + + _isLoading = true; + _permissionReady = false; + + _prepare(); + } + + @override + void dispose() { + _unbindBackgroundIsolate(); + super.dispose(); + } + + void _bindBackgroundIsolate() { + bool isSuccess = IsolateNameServer.registerPortWithName( + _port.sendPort, 'downloader_send_port'); + if (!isSuccess) { + _unbindBackgroundIsolate(); + _bindBackgroundIsolate(); + return; + } + + _port.listen((dynamic data) { + print('UI isolate callback: $data'); + String id = data[0]; + DownloadTaskStatus status = data[1]; + int progress = data[2]; + if (_task.taskId == id) { + print(_task.progress); + setState(() { + _task.status = status; + _task.progress = progress; + }); + } + }); + } + + void _unbindBackgroundIsolate() { + IsolateNameServer.removePortNameMapping('downloader_send_port'); + } + + static void downloadCallback( + String id, DownloadTaskStatus status, int progress) { + print('Background callback task in $id status ($status) $progress'); + final SendPort send = + IsolateNameServer.lookupPortByName('downloader_send_port'); + send.send([id, status, progress]); + } + + void _requestDownload(_TaskInfo task) async { + _permissionReady = await _checkPermmison(); + if (_permissionReady) + task.taskId = await FlutterDownloader.enqueue( + url: task.link, + savedDir: _localPath, + showNotification: true, + openFileFromNotification: false, + ); + var dbHelper = DBHelper(); + await dbHelper.saveDownloaded(task.link, task.taskId); + Fluttertoast.showToast( + msg: 'Downloading', + gravity: ToastGravity.BOTTOM, + ); + } + + void _deleteDownload(_TaskInfo task) async { + await FlutterDownloader.remove( + taskId: task.taskId, shouldDeleteContent: true); + var dbHelper = DBHelper(); + await dbHelper.delDownloaded(task.link); + await _prepare(); + setState(() {}); + Fluttertoast.showToast( + msg: 'Download removed', + gravity: ToastGravity.BOTTOM, + ); + } + + void _pauseDownload(_TaskInfo task) async { + await FlutterDownloader.pause(taskId: task.taskId); + Fluttertoast.showToast( + msg: 'Download paused', + gravity: ToastGravity.BOTTOM, + ); + } + + void _resumeDownload(_TaskInfo task) async { + String newTaskId = await FlutterDownloader.resume(taskId: task.taskId); + task.taskId = newTaskId; + var dbHelper = DBHelper(); + await dbHelper.saveDownloaded(task.taskId, task.link); + Fluttertoast.showToast( + msg: 'Download resumed', + gravity: ToastGravity.BOTTOM, + ); + } + + void _retryDownload(_TaskInfo task) async { + String newTaskId = await FlutterDownloader.retry(taskId: task.taskId); + task.taskId = newTaskId; + var dbHelper = DBHelper(); + await dbHelper.saveDownloaded(task.taskId, task.link); + Fluttertoast.showToast( + msg: 'Download again', + gravity: ToastGravity.BOTTOM, + ); + } + + Future _prepare() async { + final tasks = await FlutterDownloader.loadTasks(); + + _task = _TaskInfo( + name: widget.episodeBrief.title, + link: widget.episodeBrief.enclosureUrl); + + tasks?.forEach((task) { + if (_task.link == task.url) { + _task.taskId = task.taskId; + _task.status = task.status; + _task.progress = task.progress; + } + }); + + _localPath = (await _getPath()) + '/' + widget.episodeBrief.feedTitle; + print(_localPath); + final saveDir = Directory(_localPath); + bool hasExisted = await saveDir.exists(); + if (!hasExisted) { + saveDir.create(); + } + setState(() { + _isLoading = false; + }); + } + + Future _checkPermmison() async { + PermissionStatus permission = await PermissionHandler() + .checkPermissionStatus(PermissionGroup.storage); + if (permission != PermissionStatus.granted) { + Map permissions = + await PermissionHandler() + .requestPermissions([PermissionGroup.storage]); + if (permissions[PermissionGroup.storage] == PermissionStatus.granted) { + return true; + } else { + return false; + } + } else { + return true; + } + } + + @override + Widget build(BuildContext context) { + return _downloadButton(_task); + } + + Widget _downloadButton(_TaskInfo task) { + if (_isLoading) + return Center(); + else if (task.status == DownloadTaskStatus.undefined) { + + return IconButton( + onPressed: () { + _requestDownload(task); + }, + icon: Icon( + Icons.arrow_downward, + color: Colors.grey[700], + ), + ); + } else if (task.status == DownloadTaskStatus.running) { + return InkWell( + onTap: () { + _pauseDownload(task); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 18), + child: SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator( + backgroundColor: Colors.grey[200], + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.blue), + value: task.progress / 100, + ), + ), + ), + ); + } else if (task.status == DownloadTaskStatus.paused) { + return InkWell( + onTap: () { + _resumeDownload(task); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 18), + child: SizedBox( + height: 18, + width: 18, + child: CircularProgressIndicator( + backgroundColor: Colors.grey[200], + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.red), + value: task.progress / 100, + ), + ), + ), + ); + } else if (task.status == DownloadTaskStatus.complete) { + + return IconButton( + icon: Icon( + Icons.done_all, + color: Colors.blue, + ), + onPressed: () { + _deleteDownload(task); + }, + ); + } else if (task.status == DownloadTaskStatus.failed) { + return IconButton( + icon: Icon(Icons.refresh, color: Colors.red), + onPressed: () { + _retryDownload(task); + }, + ); + } + return Center(); + } +} + +class _TaskInfo { + final String name; + final String link; + + String taskId; + int progress = 0; + DownloadTaskStatus status = DownloadTaskStatus.undefined; + + _TaskInfo({this.name, this.link}); +} \ No newline at end of file diff --git a/lib/episodegrid.dart b/lib/episodegrid.dart new file mode 100644 index 0000000..7792e27 --- /dev/null +++ b/lib/episodegrid.dart @@ -0,0 +1,323 @@ +import 'dart:convert'; +import 'dart:isolate'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_downloader/flutter_downloader.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'class/episodebrief.dart'; +import 'episodedetail.dart'; +import 'pageroute.dart'; + +class EpisodeGrid extends StatelessWidget { + final List podcast; + final bool showFavorite; + final bool showDownload; + final bool showNumber; + final String heroTag; + EpisodeGrid( + {Key key, + this.podcast, + this.showDownload, + this.showFavorite, + this.showNumber, + this.heroTag}) + : super(key: key); + @override + Widget build(BuildContext context) { + return CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + primary: false, + slivers: [ + SliverPadding( + padding: const EdgeInsets.all(5.0), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: 1.0, + crossAxisCount: 3, + mainAxisSpacing: 6.0, + crossAxisSpacing: 6.0, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + Color _c; + var color = json.decode(podcast[index].primaryColor); + (color[0] > 200 && color[1] > 200 && color[2] > 200) + ? _c = Color.fromRGBO( + (255 - color[0]), 255 - color[1], 255 - color[2], 1.0) + : _c = Color.fromRGBO(color[0], color[1], color[2], 1.0); + return InkWell( + onTap: () { + Navigator.push( + context, + ScaleRoute( + page: EpisodeDetail( + episodeItem: podcast[index], + heroTag: heroTag + )), + ); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + color: Theme.of(context).scaffoldBackgroundColor, + border: Border.all( + color: Colors.grey[100], + width: 3.0, + ), + boxShadow: [ + BoxShadow( + color: Colors.grey[100], + blurRadius: 1.0, + spreadRadius: 0.5, + ), + ]), + alignment: Alignment.center, + padding: EdgeInsets.all(8.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Hero( + tag: podcast[index].enclosureUrl + heroTag, + child: Container( + child: ClipRRect( + borderRadius: + BorderRadius.all(Radius.circular(15.0)), + child: Container( + height: 30.0, + width: 30.0, + child: CachedNetworkImage( + imageUrl: podcast[index].imageUrl, + ), + ), + ), + ), + ), + Spacer(), + showNumber + ? Container( + alignment: Alignment.topRight, + child: Text( + (podcast.length - index).toString(), + style: GoogleFonts.teko( + textStyle: TextStyle( + fontSize: 20.0, + color: _c, + ), + ), + ), + ) + : Center(), + ], + ), + ), + Expanded( + flex: 5, + child: Container( + padding: EdgeInsets.only(top: 2.0), + child: Text( + podcast[index].title, + style: TextStyle( + fontSize: 15.0, + ), + maxLines: 4, + ), + ), + ), + Expanded( + flex: 1, + child: Row( + children: [ + Align( + alignment: Alignment.bottomLeft, + child: Text( + podcast[index].pubDate.substring(4, 16), + style: TextStyle( + color: _c, fontStyle: FontStyle.italic), + ), + ), + Spacer(), + showDownload + ? DownloadIcon(episodeBrief: podcast[index]) + : Center(), + Padding( + padding: EdgeInsets.all(1), + ), + showFavorite + ? Container( + alignment: Alignment.bottomRight, + child: (podcast[index].liked == 0) + ? Center() + : IconTheme( + data: IconThemeData(size: 15), + child: Icon( + Icons.favorite, + color: Colors.red, + ), + ), + ) + : Center(), + ], + ), + ), + ], + ), + ), + ); + }, + childCount: podcast.length, + ), + ), + ), + ], + ); + } +} + +class DownloadIcon extends StatefulWidget { + final EpisodeBrief episodeBrief; + DownloadIcon({this.episodeBrief, Key key}) : super(key: key); + @override + _DownloadIconState createState() => _DownloadIconState(); +} + +class _DownloadIconState extends State { + _TaskInfo _task; + bool _isLoading; + ReceivePort _port = ReceivePort(); + + @override + void initState() { + super.initState(); + _bindBackgroundIsolate(); + + FlutterDownloader.registerCallback(downloadCallback); + + _isLoading = true; + _prepare(); + } + + @override + void dispose() { + _unbindBackgroundIsolate(); + super.dispose(); + } + + void _bindBackgroundIsolate() { + bool isSuccess = IsolateNameServer.registerPortWithName( + _port.sendPort, 'downloader_send_port'); + if (!isSuccess) { + _unbindBackgroundIsolate(); + _bindBackgroundIsolate(); + return; + } + + _port.listen((dynamic data) { + print('UI isolate callback: $data'); + String id = data[0]; + DownloadTaskStatus status = data[1]; + int progress = data[2]; + if (_task.taskId == id) { + setState(() { + _task.status = status; + _task.progress = progress; + }); + } + }); + } + + void _unbindBackgroundIsolate() { + IsolateNameServer.removePortNameMapping('downloader_send_port'); + } + + static void downloadCallback( + String id, DownloadTaskStatus status, int progress) { + print('Background callback task in $id status ($status) $progress'); + final SendPort send = + IsolateNameServer.lookupPortByName('downloader_send_port'); + send.send([id, status, progress]); + } + + Future _prepare() async { + final tasks = await FlutterDownloader.loadTasks(); + + _task = _TaskInfo( + name: widget.episodeBrief.title, + link: widget.episodeBrief.enclosureUrl); + + tasks?.forEach((task) { + if (_task.link == task.url) { + _task.taskId = task.taskId; + _task.status = task.status; + _task.progress = task.progress; + } + }); + setState(() { + _isLoading = false; + }); + } + + @override + Widget build(BuildContext context) { + return _downloadButton(_task); + } + + Widget _downloadButton(_TaskInfo task) { + if (_isLoading) + return Center(); + else if (task.status == DownloadTaskStatus.running) { + return SizedBox( + height: 12, + width: 12, + child: CircularProgressIndicator( + backgroundColor: Colors.grey[200], + strokeWidth: 1, + valueColor: AlwaysStoppedAnimation(Colors.blue), + value: task.progress / 100, + ), + ); + } else if (task.status == DownloadTaskStatus.paused) { + return SizedBox( + height: 12, + width: 12, + child: CircularProgressIndicator( + backgroundColor: Colors.grey[200], + strokeWidth: 1, + valueColor: AlwaysStoppedAnimation(Colors.red), + value: task.progress / 100, + ), + ); + } else if (task.status == DownloadTaskStatus.complete) { + return IconTheme( + data: IconThemeData(size: 15), + child: Icon( + Icons.done_all, + color: Colors.blue, + ), + ); + } else if (task.status == DownloadTaskStatus.failed) { + return IconTheme( + data: IconThemeData(size: 15), + child: Icon(Icons.refresh, color: Colors.red), + ); + } + return Center(); + } +} + +class _TaskInfo { + final String name; + final String link; + + String taskId; + int progress = 0; + DownloadTaskStatus status = DownloadTaskStatus.undefined; + + _TaskInfo({this.name, this.link}); +} diff --git a/lib/home.dart b/lib/home.dart new file mode 100644 index 0000000..f03be81 --- /dev/null +++ b/lib/home.dart @@ -0,0 +1,50 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'podcastlist.dart'; +import 'hometab.dart'; +import 'importompl.dart'; +import 'audio_player.dart'; +import 'homescroll.dart'; +import 'pageroute.dart'; + +class Home extends StatefulWidget { + @override + _HomeState createState() => _HomeState(); +} + +class _HomeState extends State { + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Import(), + Container( + height: 30, + padding: EdgeInsets.symmetric(horizontal: 15), + alignment: Alignment.bottomRight, + + child: GestureDetector( + onTap: () { + Navigator.push( + context, + SlideLeftRoute(page: Podcast()), + ); + }, + child: Text('See All', + style: TextStyle( + color: Colors.red[300], fontWeight: FontWeight.bold, )), + + )), + Container( + child: ScrollPodcasts()), + Expanded( + child: MainTab(), + ), + PlayerWidget(), + ], + ); + } +} \ No newline at end of file diff --git a/lib/homescroll.dart b/lib/homescroll.dart new file mode 100644 index 0000000..8309e2d --- /dev/null +++ b/lib/homescroll.dart @@ -0,0 +1,323 @@ +import 'dart:convert'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'class/episodebrief.dart'; +import 'class/podcastlocal.dart'; +import 'class/sqflite_localpodcast.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'episodedetail.dart'; +import 'podcastdetail.dart'; +import 'pageroute.dart'; + +class ScrollPodcasts extends StatefulWidget { + @override + _ScrollPodcastsState createState() => _ScrollPodcastsState(); +} + +class _ScrollPodcastsState extends State { + Future> getPodcastLocal() async { + var dbHelper = DBHelper(); + List podcastList = await dbHelper.getPodcastLocal(); + return podcastList; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: getPodcastLocal(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return DefaultTabController( + length: snapshot.data.length, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 70, + alignment: Alignment.centerLeft, + child: TabBar( + labelPadding: + EdgeInsets.only(bottom: 15.0, left: 6.0, right: 6.0), + indicator: + CircleTabIndicator(color: Colors.blue, radius: 3), + isScrollable: true, + tabs: snapshot.data.map((PodcastLocal podcastLocal) { + return Tab( + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(25.0)), + child: LimitedBox( + maxHeight: 50, + maxWidth: 50, + child: CachedNetworkImage( + imageUrl: podcastLocal.imageUrl, + placeholder: (context, url) => + CircularProgressIndicator(), + ), + ), + ), + ); + }).toList(), + ), + ), + Container( + height: 200, + margin: EdgeInsets.only(left: 10, right: 10), + decoration: BoxDecoration( + color: Colors.white, + ), + child: TabBarView( + children: + snapshot.data.map((PodcastLocal podcastLocal) { + return Container( + decoration: BoxDecoration(color: Colors.grey[100]), + margin: EdgeInsets.symmetric(horizontal: 5.0), + key: ObjectKey(podcastLocal.title), + child: PodcastPreview( + podcastLocal: podcastLocal, + ), + ); + }).toList(), + ), + ), + ], + ), + ); + } + return Center(); + }, + ); + } +} + +class PodcastPreview extends StatefulWidget { + final PodcastLocal podcastLocal; + PodcastPreview({this.podcastLocal, Key key}) : super(key: key); + @override + _PodcastPreviewState createState() => _PodcastPreviewState(); +} + +class _PodcastPreviewState extends State { + Future> _getRssItemTop(PodcastLocal podcastLocal) async { + var dbHelper = DBHelper(); + Future> episodes = + dbHelper.getRssItemTop(podcastLocal.title); + return episodes; + } + + Color _c; + @override + void initState() { + super.initState(); + var color = json.decode(widget.podcastLocal.primaryColor); + (color[0] > 200 && color[1] > 200 && color[2] > 200) + ? _c = Color.fromRGBO( + (255 - color[0]), 255 - color[1], 255 - color[2], 1.0) + : _c = Color.fromRGBO(color[0], color[1], color[2], 1.0); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: Container( + child: FutureBuilder>( + future: _getRssItemTop(widget.podcastLocal), + builder: (context, snapshot) { + if (snapshot.hasError) { + print(snapshot.error); + Center(child: CircularProgressIndicator()); + } + return (snapshot.hasData) + ? ShowEpisode( + podcast: snapshot.data, + podcastLocal: widget.podcastLocal) + : Center(child: CircularProgressIndicator()); + }, + ), + ), + ), + Container( + height: 40, + padding: EdgeInsets.symmetric(horizontal: 10.0), + alignment: Alignment.center, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text(widget.podcastLocal.title, + style: TextStyle(fontWeight: FontWeight.bold, color: _c)), + Spacer(), + IconButton( + icon: Icon(Icons.arrow_forward), + splashColor: Colors.transparent, + tooltip: 'See All', + onPressed: () { + Navigator.push( + context, + SlideLeftRoute( + page: PodcastDetail( + podcastLocal: widget.podcastLocal, + )), + ); + }, + ), + ], + ), + ), + ], + ); + } +} + +class ShowEpisode extends StatelessWidget { + final List podcast; + final PodcastLocal podcastLocal; + ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key); + @override + Widget build(BuildContext context) { + return CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + primary: false, + slivers: [ + SliverPadding( + padding: const EdgeInsets.all(5.0), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: 1.0, + crossAxisCount: 3, + mainAxisSpacing: 6.0, + crossAxisSpacing: 6.0, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + Color _c; + var color = json.decode(podcast[index].primaryColor); + (color[0] > 200 && color[1] > 200 && color[2] > 200) + ? _c = Color.fromRGBO( + (255 - color[0]), 255 - color[1], 255 - color[2], 1.0) + : _c = Color.fromRGBO(color[0], color[1], color[2], 1.0); + return InkWell( + onTap: () { + Navigator.push( + context, + ScaleRoute( + page: EpisodeDetail( + episodeItem: podcast[index], + heroTag: 'scroll', + )), + ); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + color: Theme.of(context).scaffoldBackgroundColor, + border: Border.all( + color: Colors.grey[100], + width: 3.0, + ), + boxShadow: [ + BoxShadow( + color: Colors.grey[100], + blurRadius: 1.0, + spreadRadius: 0.5, + ), + ]), + alignment: Alignment.center, + padding: EdgeInsets.all(10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Hero( + tag: podcast[index].enclosureUrl + 'scroll', + child: Container( + child: ClipRRect( + borderRadius: + BorderRadius.all(Radius.circular(15.0)), + child: Container( + height: 30.0, + width: 30.0, + child: CachedNetworkImage( + imageUrl: podcastLocal.imageUrl, + ), + ), + ), + ), + ), + Spacer(), + ], + ), + ), + Expanded( + flex: 5, + child: Container( + padding: EdgeInsets.only(top: 2.0), + child: Text( + podcast[index].title, + style: TextStyle( + fontSize: 15.0, + ), + maxLines: 4, + ), + ), + ), + Expanded( + flex: 1, + child: Align( + alignment: Alignment.bottomLeft, + child: Text( + podcast[index].pubDate.substring(4, 16), + style: TextStyle( + color: _c, + fontStyle: FontStyle.italic, + ), + ), + ), + ), + ], + ), + ), + ); + }, + childCount: (podcast.length > 3) ? 3 : podcast.length, + ), + ), + ), + ], + ); + } +} + +//Circle Indicator +class CircleTabIndicator extends Decoration { + final BoxPainter _painter; + CircleTabIndicator({@required Color color, @required double radius}) + : _painter = _CirclePainter(color, radius); + @override + BoxPainter createBoxPainter([onChanged]) => _painter; +} + +class _CirclePainter extends BoxPainter { + final Paint _paint; + final double radius; + + _CirclePainter(Color color, this.radius) + : _paint = Paint() + ..color = color + ..isAntiAlias = true; + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) { + final Offset circleOffset = + offset + Offset(cfg.size.width / 2, cfg.size.height - radius); + canvas.drawCircle(circleOffset, radius, _paint); + } +} + + diff --git a/lib/hometab.dart b/lib/hometab.dart new file mode 100644 index 0000000..608a2cc --- /dev/null +++ b/lib/hometab.dart @@ -0,0 +1,157 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'class/episodebrief.dart'; +import 'class/sqflite_localpodcast.dart'; +import 'episodegrid.dart'; + +class MainTab extends StatefulWidget { + @override + _MainTabState createState() => _MainTabState(); +} + +class _MainTabState extends State with TickerProviderStateMixin { + TabController _controller; + Decoration getIndicator() { + return const UnderlineTabIndicator( + borderSide: BorderSide(color: Colors.red, width: 2), + insets: EdgeInsets.only(left:20,top:10,) + );} + @override + void initState() { + super.initState(); + _controller = TabController(length: 3, vsync: this); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 50, + alignment: Alignment.centerLeft, + child: TabBar( + isScrollable: true, + labelPadding: + EdgeInsets.only(bottom:10.0,left: 20.0), + controller: _controller, + labelColor: Colors.red, + unselectedLabelColor: Colors.black, + indicator: getIndicator(), + tabs: [ + Text('Recent Update',style: TextStyle(fontWeight: FontWeight.bold),), + Text('Favorite',style: TextStyle(fontWeight: FontWeight.bold),), + Text('Dowloads',style: TextStyle(fontWeight: FontWeight.bold),), + ], + ), + ), + Expanded( + child: Container( + child: TabBarView( + controller: _controller, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 10.0), + child: RecentUpdate()), + Container( + padding: EdgeInsets.symmetric(horizontal: 10.0), + child: MyFavorite()), + Container( + padding: EdgeInsets.symmetric(horizontal: 10.0), + child: MyDownload()), + ], + ), + ), + ), + ], + ); + } +} + +class RecentUpdate extends StatefulWidget { + @override + _RecentUpdateState createState() => _RecentUpdateState(); +} + +class _RecentUpdateState extends State { + Future> _getRssItem() async { + var dbHelper = DBHelper(); + List episodes = await dbHelper.getRecentRssItem(); + return episodes; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _getRssItem(), + builder: (context, snapshot) { + if (snapshot.hasError) print(snapshot.error); + return (snapshot.hasData) + ? EpisodeGrid(podcast: snapshot.data, showDownload: false, showFavorite: false, showNumber: false, heroTag: 'recent',) + : Center(child: CircularProgressIndicator()); + }, + ); + } +} + +class MyFavorite extends StatefulWidget { + @override + _MyFavoriteState createState() => _MyFavoriteState(); +} + +class _MyFavoriteState extends State { + Future> _getLikedRssItem() async { + var dbHelper = DBHelper(); + List episodes =await dbHelper.getLikedRssItem(); + return episodes; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _getLikedRssItem(), + builder: (context, snapshot) { + if (snapshot.hasError) print(snapshot.error); + return (snapshot.hasData) + ? EpisodeGrid(podcast: snapshot.data, showDownload: false, showFavorite: false, showNumber: false, heroTag: 'favorite',) + : Center(child: CircularProgressIndicator()); + }, + ); + } +} + + class MyDownload extends StatefulWidget { + @override + _MyDownloadState createState() => _MyDownloadState(); +} + + +class _MyDownloadState extends State { + Future> _getDownloadedRssItem() async { + var dbHelper = DBHelper(); + List episodes =await dbHelper.getDownloadedRssItem(); + return episodes; + } + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _getDownloadedRssItem(), + builder: (context, snapshot) { + if (snapshot.hasError) print(snapshot.error); + return (snapshot.hasData) + ? EpisodeGrid(podcast: snapshot.data, showDownload: true, showFavorite: false, showNumber: false, heroTag: 'download',) + : Center(child: CircularProgressIndicator()); + }, + ); + } +} + diff --git a/lib/importompl.dart b/lib/importompl.dart new file mode 100644 index 0000000..de0ab54 --- /dev/null +++ b/lib/importompl.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'class/importompl.dart'; + +class Import extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, importOmpl, _) => Container( + child: importOmpl.importState == ImportState.start + ? Container( + height: 20.0, + alignment: Alignment.center, + child: Text('Start'), + ) + : importOmpl.importState == ImportState.import + ? Container( + height: 20.0, + alignment: Alignment.center, + child: Text('Importing'+(importOmpl.rsstitle))) + : importOmpl.importState == ImportState.complete + ? Container( + height: 20.0, + alignment: Alignment.center, + child: Text('Complete'), + ) + : importOmpl.importState == ImportState.stop + ? Center() + : Center())); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..c04616c --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_statusbarcolor/flutter_statusbarcolor.dart'; +import 'package:provider/provider.dart'; +import 'package:flutter_downloader/flutter_downloader.dart'; +import 'addpodcast.dart'; +import 'class/audiostate.dart'; + +void main() async { + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (context) => Urlchange()), + ], + child: MyApp(), + ), + ); + WidgetsFlutterBinding.ensureInitialized(); + await FlutterDownloader.initialize(); + await FlutterStatusbarcolor.setStatusBarColor(Colors.grey[100]); + await FlutterStatusbarcolor.setNavigationBarColor(Colors.grey[100]); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'TsacDop', + theme: ThemeData( + primaryColor: Colors.white, + ), + home: MyHomePage(), + ); + } +} diff --git a/lib/pageroute.dart b/lib/pageroute.dart new file mode 100644 index 0000000..8a8035a --- /dev/null +++ b/lib/pageroute.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; + +//Slide Transition +class SlideLeftRoute extends PageRouteBuilder { + final Widget page; + SlideLeftRoute({this.page}) + : super( + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => + page, + transitionsBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) => + SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(animation), + child: child, + ), + ); +} + +//Scale Pageroute +class ScaleRoute extends PageRouteBuilder { + final Widget page; + ScaleRoute({this.page}) + : super( + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) => + page, + transitionsBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) => + ScaleTransition( + scale: Tween( + begin: 0.0, + end: 1.0, + ).animate( + CurvedAnimation( + parent: animation, + curve: Curves.fastOutSlowIn, + ), + ), + child: child, + ), + ); +} diff --git a/lib/podcastdetail.dart b/lib/podcastdetail.dart new file mode 100644 index 0000000..cba155f --- /dev/null +++ b/lib/podcastdetail.dart @@ -0,0 +1,70 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:dio/dio.dart'; +import 'dart:async'; + +import 'class/podcastlocal.dart'; +import 'class/episodebrief.dart'; +import 'class/sqflite_localpodcast.dart'; +import 'episodegrid.dart'; + +class PodcastDetail extends StatefulWidget { + PodcastDetail({Key key, this.podcastLocal}) : super(key: key); + final PodcastLocal podcastLocal; + @override + _PodcastDetailState createState() => _PodcastDetailState(); +} + +class _PodcastDetailState extends State { + final GlobalKey _refreshIndicatorKey = + GlobalKey(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose(){ + super.dispose(); + } + + Future _updateRssItem(PodcastLocal podcastLocal) async { + var dbHelper = DBHelper(); + final response = await Dio().get(podcastLocal.rssUrl); + final result = await dbHelper.savePodcastRss(response.data); + if (result == 0 && mounted) setState(() {}); + } + + Future> _getRssItem(PodcastLocal podcastLocal) async { + var dbHelper = DBHelper(); + List episodes = await + dbHelper.getRssItem(podcastLocal.title); + return episodes; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.podcastLocal.title,), + elevation: 0.0, + backgroundColor: Colors.grey[100], + centerTitle: true, + ), + body: RefreshIndicator( + key: _refreshIndicatorKey, + color: Colors.blue[500], + onRefresh: () => _updateRssItem(widget.podcastLocal), + child: FutureBuilder>( + future: _getRssItem(widget.podcastLocal), + builder: (context, snapshot) { + if (snapshot.hasError) print(snapshot.error); + return (snapshot.hasData) + ? EpisodeGrid(podcast: snapshot.data, showDownload: true, showFavorite: true, showNumber: true, heroTag: 'podcast',) + : Center(child: CircularProgressIndicator()); + }, + )), + ); + } +} diff --git a/lib/podcastlist.dart b/lib/podcastlist.dart new file mode 100644 index 0000000..9c0f416 --- /dev/null +++ b/lib/podcastlist.dart @@ -0,0 +1,193 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:cached_network_image/cached_network_image.dart'; + +import 'class/podcastlocal.dart'; +import 'class/sqflite_localpodcast.dart'; +import 'podcastdetail.dart'; + +Future> getPodcastLocal() async { + var dbHelper = DBHelper(); + Future> podcastList = dbHelper.getPodcastLocal(); + return podcastList; +} + +class AboutPodcast extends StatefulWidget { + final PodcastLocal podcastLocal; + AboutPodcast({this.podcastLocal, Key key}) : super(key: key); + + @override + _AboutPodcastState createState() => _AboutPodcastState(); +} + +class _AboutPodcastState extends State { + void _unSubscribe(String t) async { + var dbHelper = DBHelper(); + dbHelper.delPodcastLocal(t); + print('Unsubscribe'); + } + + String _description; + bool _load; + + void getDescription(String title) async { + var dbHelper = DBHelper(); + String description = await dbHelper.getFeedDescription(title); + _description = description; + setState(() { + _load = true; + }); + } + + @override + void initState() { + super.initState(); + _load = false; + getDescription(widget.podcastLocal.title); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + actions: [ + FlatButton( + padding: EdgeInsets.all(10.0), + onPressed: () { + _unSubscribe(widget.podcastLocal.title); + Navigator.of(context).pop(); + }, + color: Colors.grey[200], + textColor: Colors.red, + child: Text( + 'UNSUBSCRIBE', + ), + ), + ], + title: Text(widget.podcastLocal.title), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + !_load + ? Center() + : _description != null ? Text(_description) : Center(), + (widget.podcastLocal.author != null) + ? Text(widget.podcastLocal.author, + style: TextStyle(color: Colors.blue)) + : Center(), + ], + ), + ); + } +} + +class PodcastList extends StatefulWidget { + @override + _PodcastListState createState() => _PodcastListState(); +} + +class _PodcastListState extends State { + @override + Widget build(BuildContext context) { + return Container( + color: Colors.grey[100], + child: FutureBuilder>( + future: getPodcastLocal(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return CustomScrollView( + primary: false, + slivers: [ + SliverPadding( + padding: const EdgeInsets.all(10.0), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: 0.8, + crossAxisCount: 3, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PodcastDetail( + podcastLocal: snapshot.data[index], + )), + ); + }, + onLongPress: () { + showDialog( + context: context, + builder: (BuildContext context) => AboutPodcast( + podcastLocal: snapshot.data[index]), + ).then((_) => setState(() {})); + }, + child: Container( + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + height: 10.0, + ), + ClipRRect( + borderRadius: + BorderRadius.all(Radius.circular(60.0)), + child: Container( + height: 120.0, + width: 120.0, + child: CachedNetworkImage( + imageUrl: snapshot.data[index].imageUrl, + placeholder: (context, url) => + CircularProgressIndicator(), + ), + ), + ), + Container( + padding: EdgeInsets.all(4.0), + child: Text( + snapshot.data[index].title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16.0, + color: Colors.black.withOpacity(0.5), + ), + maxLines: 2, + ), + ), + ], + ), + ), + ); + }, + childCount: snapshot.data.length, + ), + ), + ), + ], + ); + } + return Text('NoData'); + }, + ), + ); + } +} + +class Podcast extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Colors.grey[100], + elevation: 0, + title: Text('Podcasts'), + ), + body: Container(child: PodcastList()), + ); + } +} diff --git a/lib/popupmenu.dart b/lib/popupmenu.dart new file mode 100644 index 0000000..c53a894 --- /dev/null +++ b/lib/popupmenu.dart @@ -0,0 +1,112 @@ +import 'dart:io'; +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:dio/dio.dart'; +import 'package:provider/provider.dart'; +import 'package:xml/xml.dart' as xml; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/services.dart'; +import 'about.dart'; +import 'class/podcastlocal.dart'; +import 'class/sqflite_localpodcast.dart'; +import 'class/importompl.dart'; +import 'webfeed/webfeed.dart'; + +class OmplOutline { + final String text; + final String xmlUrl; + OmplOutline({this.text, this.xmlUrl}); + + factory OmplOutline.parse(xml.XmlElement element) { + if (element == null) return null; + return OmplOutline( + text: element.getAttribute("text")?.trim(), + xmlUrl: element.getAttribute("xmlUrl")?.trim(), + ); + } +} + +class PopupMenu extends StatelessWidget { + + Future saveOmpl(String rss) async { + var dbHelper = DBHelper(); + try { + Response response = await Dio().get(rss); + var _p = RssFeed.parse(response.data); + String _primaryColor = '[100,100,100]'; + PodcastLocal podcastLocal = PodcastLocal(_p.title, _p.itunes.image.href, + rss, _primaryColor, _p.author); + podcastLocal.description = _p.description; + int total = await dbHelper.savePodcastLocal(podcastLocal); + return total; + } catch (e) { + return 0; + } + } + + @override + Widget build(BuildContext context) { + final importOmpl = Provider.of(context); + + void _saveOmpl(String path) async { + File file = File(path); + String opml = file.readAsStringSync(); + try { + var content = xml.parse(opml); + importOmpl.importState = ImportState.import; + var total = content + .findAllElements('outline') + .map((ele) => OmplOutline.parse(ele)) + .toList(); + for (int i = 0; i < total.length; i++) { + if (total[i].xmlUrl != null) + await saveOmpl(total[i].xmlUrl); + importOmpl.rssTitle = total[i].text; + print(total[i].text); + } + importOmpl.importState = ImportState.complete; + print('Import fisnished'); + } catch (e) { + print(e); + importOmpl.importState = ImportState.error; + } + } + + void _getFilePath() async { + try { + String filePath = await FilePicker.getFilePath(type: FileType.ANY); + if (filePath == '') { + return; + } + print('File Path' + filePath); + importOmpl.importState = ImportState.start; + _saveOmpl(filePath); + } on PlatformException catch (e) { + print(e.toString()); + } + } + + return PopupMenuButton( + elevation: 2, + tooltip: 'Menu', + itemBuilder: (context) => [ + PopupMenuItem( + value: 1, + child: Text('Impoer OMPL'), + ), + PopupMenuItem( + value: 2, + child: Text('About'), + ), + ], + onSelected: (value) { + if (value == 2) { + Navigator.push( + context, MaterialPageRoute(builder: (context) => AboutApp())); + } else if (value == 1) { + _getFilePath(); + } + }, + ); + } +} diff --git a/lib/webfeed/domain/atom_category.dart b/lib/webfeed/domain/atom_category.dart new file mode 100644 index 0000000..22c6a97 --- /dev/null +++ b/lib/webfeed/domain/atom_category.dart @@ -0,0 +1,16 @@ +import 'package:xml/xml.dart'; + +class AtomCategory { + final String term; + final String scheme; + final String label; + + AtomCategory(this.term, this.scheme, this.label); + + factory AtomCategory.parse(XmlElement element) { + var term = element.getAttribute("term"); + var scheme = element.getAttribute("scheme"); + var label = element.getAttribute("label"); + return AtomCategory(term, scheme, label); + } +} diff --git a/lib/webfeed/domain/atom_feed.dart b/lib/webfeed/domain/atom_feed.dart new file mode 100644 index 0000000..92f2cd6 --- /dev/null +++ b/lib/webfeed/domain/atom_feed.dart @@ -0,0 +1,77 @@ +import 'package:webfeed/domain/atom_category.dart'; +import 'package:webfeed/domain/atom_generator.dart'; +import 'package:webfeed/domain/atom_item.dart'; +import 'package:webfeed/domain/atom_link.dart'; +import 'package:webfeed/domain/atom_person.dart'; +import 'package:webfeed/util/helpers.dart'; +import 'package:xml/xml.dart'; + +class AtomFeed { + final String id; + final String title; + final String updated; + final List items; + + final List links; + final List authors; + final List contributors; + final List categories; + final AtomGenerator generator; + final String icon; + final String logo; + final String rights; + final String subtitle; + + AtomFeed({ + this.id, + this.title, + this.updated, + this.items, + this.links, + this.authors, + this.contributors, + this.categories, + this.generator, + this.icon, + this.logo, + this.rights, + this.subtitle, + }); + + factory AtomFeed.parse(String xmlString) { + var document = parse(xmlString); + XmlElement feedElement; + try { + feedElement = document.findElements("feed").first; + } on StateError { + throw new ArgumentError("feed not found"); + } + + return AtomFeed( + id: findElementOrNull(feedElement, "id")?.text, + title: findElementOrNull(feedElement, "title")?.text, + updated: findElementOrNull(feedElement, "updated")?.text, + items: feedElement.findElements("entry").map((element) { + return AtomItem.parse(element); + }).toList(), + links: feedElement.findElements("link").map((element) { + return AtomLink.parse(element); + }).toList(), + authors: feedElement.findElements("author").map((element) { + return AtomPerson.parse(element); + }).toList(), + contributors: feedElement.findElements("contributor").map((element) { + return AtomPerson.parse(element); + }).toList(), + categories: feedElement.findElements("category").map((element) { + return AtomCategory.parse(element); + }).toList(), + generator: + AtomGenerator.parse(findElementOrNull(feedElement, "generator")), + icon: findElementOrNull(feedElement, "icon")?.text, + logo: findElementOrNull(feedElement, "logo")?.text, + rights: findElementOrNull(feedElement, "rights")?.text, + subtitle: findElementOrNull(feedElement, "subtitle")?.text, + ); + } +} diff --git a/lib/webfeed/domain/atom_generator.dart b/lib/webfeed/domain/atom_generator.dart new file mode 100644 index 0000000..7fd8579 --- /dev/null +++ b/lib/webfeed/domain/atom_generator.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +class AtomGenerator { + final String uri; + final String version; + final String value; + + AtomGenerator(this.uri, this.version, this.value); + + factory AtomGenerator.parse(XmlElement element) { + if (element == null) { + return null; + } + var uri = element.getAttribute("uri"); + var version = element.getAttribute("version"); + var value = element.text; + return new AtomGenerator(uri, version, value); + } +} diff --git a/lib/webfeed/domain/atom_item.dart b/lib/webfeed/domain/atom_item.dart new file mode 100644 index 0000000..618015b --- /dev/null +++ b/lib/webfeed/domain/atom_item.dart @@ -0,0 +1,66 @@ +import 'package:webfeed/domain/atom_category.dart'; +import 'package:webfeed/domain/atom_link.dart'; +import 'package:webfeed/domain/atom_person.dart'; +import 'package:webfeed/domain/atom_source.dart'; +import 'package:webfeed/domain/media/media.dart'; +import 'package:webfeed/util/helpers.dart'; +import 'package:xml/xml.dart'; + +class AtomItem { + final String id; + final String title; + final String updated; + + final List authors; + final List links; + final List categories; + final List contributors; + final AtomSource source; + final String published; + final String content; + final String summary; + final String rights; + final Media media; + + AtomItem({ + this.id, + this.title, + this.updated, + this.authors, + this.links, + this.categories, + this.contributors, + this.source, + this.published, + this.content, + this.summary, + this.rights, + this.media, + }); + + factory AtomItem.parse(XmlElement element) { + return AtomItem( + id: findElementOrNull(element, "id")?.text, + title: findElementOrNull(element, "title")?.text, + updated: findElementOrNull(element, "updated")?.text, + authors: element.findElements("author").map((element) { + return AtomPerson.parse(element); + }).toList(), + links: element.findElements("link").map((element) { + return AtomLink.parse(element); + }).toList(), + categories: element.findElements("category").map((element) { + return AtomCategory.parse(element); + }).toList(), + contributors: element.findElements("contributor").map((element) { + return AtomPerson.parse(element); + }).toList(), + source: AtomSource.parse(findElementOrNull(element, "source")), + published: findElementOrNull(element, "published")?.text, + content: findElementOrNull(element, "content")?.text, + summary: findElementOrNull(element, "summary")?.text, + rights: findElementOrNull(element, "rights")?.text, + media: Media.parse(element), + ); + } +} diff --git a/lib/webfeed/domain/atom_link.dart b/lib/webfeed/domain/atom_link.dart new file mode 100644 index 0000000..3b570f2 --- /dev/null +++ b/lib/webfeed/domain/atom_link.dart @@ -0,0 +1,32 @@ +import 'package:xml/xml.dart'; + +class AtomLink { + final String href; + final String rel; + final String type; + final String hreflang; + final String title; + final int length; + + AtomLink( + this.href, + this.rel, + this.type, + this.hreflang, + this.title, + this.length, + ); + + factory AtomLink.parse(XmlElement element) { + var href = element.getAttribute("href"); + var rel = element.getAttribute("rel"); + var type = element.getAttribute("type"); + var title = element.getAttribute("title"); + var hreflang = element.getAttribute("hreflang"); + var length = 0; + if (element.getAttribute("length") != null) { + length = int.parse(element.getAttribute("length")); + } + return AtomLink(href, rel, type, hreflang, title, length); + } +} diff --git a/lib/webfeed/domain/atom_person.dart b/lib/webfeed/domain/atom_person.dart new file mode 100644 index 0000000..3eb1e4a --- /dev/null +++ b/lib/webfeed/domain/atom_person.dart @@ -0,0 +1,17 @@ +import 'package:webfeed/util/helpers.dart'; +import 'package:xml/xml.dart'; + +class AtomPerson { + final String name; + final String uri; + final String email; + + AtomPerson(this.name, this.uri, this.email); + + factory AtomPerson.parse(XmlElement element) { + var name = findElementOrNull(element, "name")?.text; + var uri = findElementOrNull(element, "uri")?.text; + var email = findElementOrNull(element, "email")?.text; + return AtomPerson(name, uri, email); + } +} diff --git a/lib/webfeed/domain/atom_source.dart b/lib/webfeed/domain/atom_source.dart new file mode 100644 index 0000000..37fb619 --- /dev/null +++ b/lib/webfeed/domain/atom_source.dart @@ -0,0 +1,21 @@ +import 'package:webfeed/util/helpers.dart'; +import 'package:xml/xml.dart'; + +class AtomSource { + final String id; + final String title; + final String updated; + + AtomSource(this.id, this.title, this.updated); + + factory AtomSource.parse(XmlElement element) { + if (element == null) { + return null; + } + var id = findElementOrNull(element, "id")?.text; + var title = findElementOrNull(element, "title")?.text; + var updated = findElementOrNull(element, "updated")?.text; + + return AtomSource(id, title, updated); + } +} diff --git a/lib/webfeed/domain/dublin_core/dublin_core.dart b/lib/webfeed/domain/dublin_core/dublin_core.dart new file mode 100644 index 0000000..564bb4c --- /dev/null +++ b/lib/webfeed/domain/dublin_core/dublin_core.dart @@ -0,0 +1,61 @@ +import 'package:webfeed/util/helpers.dart'; +import 'package:xml/xml.dart'; + +class DublinCore { + final String title; + final String description; + final String creator; + final String subject; + final String publisher; + final String contributor; + final String date; + final String type; + final String format; + final String identifier; + final String source; + final String language; + final String relation; + final String coverage; + final String rights; + + DublinCore({ + this.title, + this.description, + this.creator, + this.subject, + this.publisher, + this.contributor, + this.date, + this.type, + this.format, + this.identifier, + this.source, + this.language, + this.relation, + this.coverage, + this.rights, + }); + + factory DublinCore.parse(XmlElement element) { + if (element == null) { + return null; + } + return DublinCore( + title: findElementOrNull(element, "dc:title")?.text, + description: findElementOrNull(element, "dc:description")?.text, + creator: findElementOrNull(element, "dc:creator")?.text, + subject: findElementOrNull(element, "dc:subject")?.text, + publisher: findElementOrNull(element, "dc:publisher")?.text, + contributor: findElementOrNull(element, "dc:contributor")?.text, + date: findElementOrNull(element, "dc:date")?.text, + type: findElementOrNull(element, "dc:type")?.text, + format: findElementOrNull(element, "dc:format")?.text, + identifier: findElementOrNull(element, "dc:identifier")?.text, + source: findElementOrNull(element, "dc:source")?.text, + language: findElementOrNull(element, "dc:language")?.text, + relation: findElementOrNull(element, "dc:relation")?.text, + coverage: findElementOrNull(element, "dc:coverage")?.text, + rights: findElementOrNull(element, "dc:rights")?.text, + ); + } +} diff --git a/lib/webfeed/domain/media/category.dart b/lib/webfeed/domain/media/category.dart new file mode 100644 index 0000000..4796cd7 --- /dev/null +++ b/lib/webfeed/domain/media/category.dart @@ -0,0 +1,24 @@ +import 'package:xml/xml.dart'; + +class Category { + final String scheme; + final String label; + final String value; + + Category({ + this.scheme, + this.label, + this.value, + }); + + factory Category.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Category( + scheme: element.getAttribute("scheme"), + label: element.getAttribute("label"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/community.dart b/lib/webfeed/domain/media/community.dart new file mode 100644 index 0000000..b301050 --- /dev/null +++ b/lib/webfeed/domain/media/community.dart @@ -0,0 +1,34 @@ +import 'package:webfeed/domain/media/star_rating.dart'; +import 'package:webfeed/domain/media/statistics.dart'; +import 'package:webfeed/domain/media/tags.dart'; +import 'package:webfeed/util/helpers.dart'; +import 'package:xml/xml.dart'; + +class Community { + final StarRating starRating; + final Statistics statistics; + final Tags tags; + + Community({ + this.starRating, + this.statistics, + this.tags, + }); + + factory Community.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Community( + starRating: new StarRating.parse( + findElementOrNull(element, "media:starRating"), + ), + statistics: new Statistics.parse( + findElementOrNull(element, "media:statistics"), + ), + tags: new Tags.parse( + findElementOrNull(element, "media:tags"), + ), + ); + } +} diff --git a/lib/webfeed/domain/media/content.dart b/lib/webfeed/domain/media/content.dart new file mode 100644 index 0000000..3a4ebaa --- /dev/null +++ b/lib/webfeed/domain/media/content.dart @@ -0,0 +1,56 @@ +import 'package:xml/xml.dart'; + +class Content { + final String url; + final String type; + final int fileSize; + final String medium; + final bool isDefault; + final String expression; + final int bitrate; + final double framerate; + final double samplingrate; + final int channels; + final int duration; + final int height; + final int width; + final String lang; + + Content({ + this.url, + this.type, + this.fileSize, + this.medium, + this.isDefault, + this.expression, + this.bitrate, + this.framerate, + this.samplingrate, + this.channels, + this.duration, + this.height, + this.width, + this.lang, + }); + + factory Content.parse(XmlElement element) { + return new Content( + url: element.getAttribute("url"), + type: element.getAttribute("type"), + fileSize: int.tryParse(element.getAttribute("fileSize") ?? "0"), + medium: element.getAttribute("medium"), + isDefault: element.getAttribute("isDefault") == "true", + expression: element.getAttribute("expression"), + bitrate: int.tryParse(element.getAttribute("bitrate") ?? "0"), + framerate: double.tryParse(element.getAttribute("framerate") ?? "0"), + samplingrate: double.tryParse( + element.getAttribute("samplingrate") ?? "0", + ), + channels: int.tryParse(element.getAttribute("channels") ?? "0"), + duration: int.tryParse(element.getAttribute("duration") ?? "0"), + height: int.tryParse(element.getAttribute("height") ?? "0"), + width: int.tryParse(element.getAttribute("width") ?? "0"), + lang: element.getAttribute("lang"), + ); + } +} diff --git a/lib/webfeed/domain/media/copyright.dart b/lib/webfeed/domain/media/copyright.dart new file mode 100644 index 0000000..1cad059 --- /dev/null +++ b/lib/webfeed/domain/media/copyright.dart @@ -0,0 +1,21 @@ +import 'package:xml/xml.dart'; + +class Copyright { + final String url; + final String value; + + Copyright({ + this.url, + this.value, + }); + + factory Copyright.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Copyright( + url: element.getAttribute("url"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/credit.dart b/lib/webfeed/domain/media/credit.dart new file mode 100644 index 0000000..70a5e50 --- /dev/null +++ b/lib/webfeed/domain/media/credit.dart @@ -0,0 +1,21 @@ +import 'package:xml/xml.dart'; + +class Credit { + final String role; + final String scheme; + final String value; + + Credit({ + this.role, + this.scheme, + this.value, + }); + + factory Credit.parse(XmlElement element) { + return new Credit( + role: element.getAttribute("role"), + scheme: element.getAttribute("scheme"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/description.dart b/lib/webfeed/domain/media/description.dart new file mode 100644 index 0000000..ba6f0e0 --- /dev/null +++ b/lib/webfeed/domain/media/description.dart @@ -0,0 +1,21 @@ +import 'package:xml/xml.dart'; + +class Description { + final String type; + final String value; + + Description({ + this.type, + this.value, + }); + + factory Description.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Description( + type: element.getAttribute("type"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/embed.dart b/lib/webfeed/domain/media/embed.dart new file mode 100644 index 0000000..1cca476 --- /dev/null +++ b/lib/webfeed/domain/media/embed.dart @@ -0,0 +1,30 @@ +import 'package:webfeed/domain/media/param.dart'; +import 'package:xml/xml.dart'; + +class Embed { + final String url; + final int width; + final int height; + final List params; + + Embed({ + this.url, + this.width, + this.height, + this.params, + }); + + factory Embed.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Embed( + url: element.getAttribute("url"), + width: int.tryParse(element.getAttribute("width") ?? "0"), + height: int.tryParse(element.getAttribute("height") ?? "0"), + params: element.findElements("media:param").map((e) { + return new Param.parse(e); + }).toList(), + ); + } +} diff --git a/lib/webfeed/domain/media/group.dart b/lib/webfeed/domain/media/group.dart new file mode 100644 index 0000000..1a9a1cb --- /dev/null +++ b/lib/webfeed/domain/media/group.dart @@ -0,0 +1,40 @@ +import 'package:webfeed/domain/media/category.dart'; +import 'package:webfeed/domain/media/content.dart'; +import 'package:webfeed/domain/media/credit.dart'; +import 'package:webfeed/domain/media/rating.dart'; +import 'package:webfeed/util/helpers.dart'; +import 'package:xml/xml.dart'; + +class Group { + final List contents; + final List credits; + final Category category; + final Rating rating; + + Group({ + this.contents, + this.credits, + this.category, + this.rating, + }); + + factory Group.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Group( + contents: element.findElements("media:content").map((e) { + return new Content.parse(e); + }).toList(), + credits: element.findElements("media:credit").map((e) { + return new Credit.parse(e); + }).toList(), + category: new Category.parse( + findElementOrNull(element, "media:category"), + ), + rating: new Rating.parse( + findElementOrNull(element, "media:rating"), + ), + ); + } +} diff --git a/lib/webfeed/domain/media/hash.dart b/lib/webfeed/domain/media/hash.dart new file mode 100644 index 0000000..72ce859 --- /dev/null +++ b/lib/webfeed/domain/media/hash.dart @@ -0,0 +1,21 @@ +import 'package:xml/xml.dart'; + +class Hash { + final String algo; + final String value; + + Hash({ + this.algo, + this.value, + }); + + factory Hash.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Hash( + algo: element.getAttribute("algo"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/license.dart b/lib/webfeed/domain/media/license.dart new file mode 100644 index 0000000..c52a8c6 --- /dev/null +++ b/lib/webfeed/domain/media/license.dart @@ -0,0 +1,24 @@ +import 'package:xml/xml.dart'; + +class License { + final String type; + final String href; + final String value; + + License({ + this.type, + this.href, + this.value, + }); + + factory License.parse(XmlElement element) { + if (element == null) { + return null; + } + return new License( + type: element.getAttribute("type"), + href: element.getAttribute("href"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/media.dart b/lib/webfeed/domain/media/media.dart new file mode 100644 index 0000000..2f685c1 --- /dev/null +++ b/lib/webfeed/domain/media/media.dart @@ -0,0 +1,169 @@ +import 'package:webfeed/domain/media/category.dart'; +import 'package:webfeed/domain/media/community.dart'; +import 'package:webfeed/domain/media/content.dart'; +import 'package:webfeed/domain/media/copyright.dart'; +import 'package:webfeed/domain/media/credit.dart'; +import 'package:webfeed/domain/media/description.dart'; +import 'package:webfeed/domain/media/embed.dart'; +import 'package:webfeed/domain/media/group.dart'; +import 'package:webfeed/domain/media/hash.dart'; +import 'package:webfeed/domain/media/license.dart'; +import 'package:webfeed/domain/media/peer_link.dart'; +import 'package:webfeed/domain/media/player.dart'; +import 'package:webfeed/domain/media/price.dart'; +import 'package:webfeed/domain/media/rating.dart'; +import 'package:webfeed/domain/media/restriction.dart'; +import 'package:webfeed/domain/media/rights.dart'; +import 'package:webfeed/domain/media/scene.dart'; +import 'package:webfeed/domain/media/status.dart'; +import 'package:webfeed/domain/media/text.dart'; +import 'package:webfeed/domain/media/thumbnail.dart'; +import 'package:webfeed/domain/media/title.dart'; +import 'package:webfeed/util/helpers.dart'; +import 'package:xml/xml.dart'; + +class Media { + final Group group; + final List contents; + final List credits; + final Category category; + final Rating rating; + final Title title; + final Description description; + final String keywords; + final List thumbnails; + final Hash hash; + final Player player; + final Copyright copyright; + final Text text; + final Restriction restriction; + final Community community; + final List comments; + final Embed embed; + final List responses; + final List backLinks; + final Status status; + final List prices; + final License license; + final PeerLink peerLink; + final Rights rights; + final List scenes; + + Media({ + this.group, + this.contents, + this.credits, + this.category, + this.rating, + this.title, + this.description, + this.keywords, + this.thumbnails, + this.hash, + this.player, + this.copyright, + this.text, + this.restriction, + this.community, + this.comments, + this.embed, + this.responses, + this.backLinks, + this.status, + this.prices, + this.license, + this.peerLink, + this.rights, + this.scenes, + }); + + factory Media.parse(XmlElement element) { + return new Media( + group: new Group.parse( + findElementOrNull(element, "media:group"), + ), + contents: element.findElements("media:content").map((e) { + return new Content.parse(e); + }).toList(), + credits: element.findElements("media:credit").map((e) { + return new Credit.parse(e); + }).toList(), + category: new Category.parse( + findElementOrNull(element, "media:category"), + ), + rating: new Rating.parse( + findElementOrNull(element, "media:rating"), + ), + title: new Title.parse( + findElementOrNull(element, "media:title"), + ), + description: new Description.parse( + findElementOrNull(element, "media:description"), + ), + keywords: findElementOrNull(element, "media:keywords")?.text, + thumbnails: element.findElements("media:thumbnail").map((e) { + return new Thumbnail.parse(e); + }).toList(), + hash: new Hash.parse( + findElementOrNull(element, "media:hash"), + ), + player: new Player.parse( + findElementOrNull(element, "media:player"), + ), + copyright: new Copyright.parse( + findElementOrNull(element, "media:copyright"), + ), + text: new Text.parse( + findElementOrNull(element, "media:text"), + ), + restriction: new Restriction.parse( + findElementOrNull(element, "media:restriction"), + ), + community: new Community.parse( + findElementOrNull(element, "media:community"), + ), + comments: findElementOrNull(element, "media:comments") + ?.findElements("media:comment") + ?.map((e) { + return e.text; + })?.toList() ?? + [], + embed: new Embed.parse( + findElementOrNull(element, "media:embed"), + ), + responses: findElementOrNull(element, "media:responses") + ?.findElements("media:response") + ?.map((e) { + return e.text; + })?.toList() ?? + [], + backLinks: findElementOrNull(element, "media:backLinks") + ?.findElements("media:backLink") + ?.map((e) { + return e.text; + })?.toList() ?? + [], + status: new Status.parse( + findElementOrNull(element, "media:status"), + ), + prices: element.findElements("media:price").map((e) { + return new Price.parse(e); + }).toList(), + license: new License.parse( + findElementOrNull(element, "media:license"), + ), + peerLink: new PeerLink.parse( + findElementOrNull(element, "media:peerLink"), + ), + rights: new Rights.parse( + findElementOrNull(element, "media:rights"), + ), + scenes: findElementOrNull(element, "media:scenes") + ?.findElements("media:scene") + ?.map((e) { + return new Scene.parse(e); + })?.toList() ?? + [], + ); + } +} diff --git a/lib/webfeed/domain/media/param.dart b/lib/webfeed/domain/media/param.dart new file mode 100644 index 0000000..175e6b8 --- /dev/null +++ b/lib/webfeed/domain/media/param.dart @@ -0,0 +1,21 @@ +import 'package:xml/xml.dart'; + +class Param { + final String name; + final String value; + + Param({ + this.name, + this.value, + }); + + factory Param.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Param( + name: element.getAttribute("name"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/peer_link.dart b/lib/webfeed/domain/media/peer_link.dart new file mode 100644 index 0000000..6c51a17 --- /dev/null +++ b/lib/webfeed/domain/media/peer_link.dart @@ -0,0 +1,24 @@ +import 'package:xml/xml.dart'; + +class PeerLink { + final String type; + final String href; + final String value; + + PeerLink({ + this.type, + this.href, + this.value, + }); + + factory PeerLink.parse(XmlElement element) { + if (element == null) { + return null; + } + return new PeerLink( + type: element.getAttribute("type"), + href: element.getAttribute("href"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/player.dart b/lib/webfeed/domain/media/player.dart new file mode 100644 index 0000000..23d5e7a --- /dev/null +++ b/lib/webfeed/domain/media/player.dart @@ -0,0 +1,27 @@ +import 'package:xml/xml.dart'; + +class Player { + final String url; + final int width; + final int height; + final String value; + + Player({ + this.url, + this.width, + this.height, + this.value, + }); + + factory Player.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Player( + url: element.getAttribute("url"), + width: int.tryParse(element.getAttribute("width") ?? "0"), + height: int.tryParse(element.getAttribute("height") ?? "0"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/price.dart b/lib/webfeed/domain/media/price.dart new file mode 100644 index 0000000..6da075b --- /dev/null +++ b/lib/webfeed/domain/media/price.dart @@ -0,0 +1,24 @@ +import 'package:xml/xml.dart'; + +class Price { + final double price; + final String type; + final String info; + final String currency; + + Price({ + this.price, + this.type, + this.info, + this.currency, + }); + + factory Price.parse(XmlElement element) { + return new Price( + price: double.tryParse(element.getAttribute("price") ?? "0"), + type: element.getAttribute("type"), + info: element.getAttribute("info"), + currency: element.getAttribute("currency"), + ); + } +} diff --git a/lib/webfeed/domain/media/rating.dart b/lib/webfeed/domain/media/rating.dart new file mode 100644 index 0000000..77c2b12 --- /dev/null +++ b/lib/webfeed/domain/media/rating.dart @@ -0,0 +1,21 @@ +import 'package:xml/xml.dart'; + +class Rating { + final String scheme; + final String value; + + Rating({ + this.scheme, + this.value, + }); + + factory Rating.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Rating( + scheme: element.getAttribute("scheme"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/restriction.dart b/lib/webfeed/domain/media/restriction.dart new file mode 100644 index 0000000..4aa56bd --- /dev/null +++ b/lib/webfeed/domain/media/restriction.dart @@ -0,0 +1,24 @@ +import 'package:xml/xml.dart'; + +class Restriction { + final String relationship; + final String type; + final String value; + + Restriction({ + this.relationship, + this.type, + this.value, + }); + + factory Restriction.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Restriction( + relationship: element.getAttribute("relationship"), + type: element.getAttribute("type"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/rights.dart b/lib/webfeed/domain/media/rights.dart new file mode 100644 index 0000000..eb00d4d --- /dev/null +++ b/lib/webfeed/domain/media/rights.dart @@ -0,0 +1,18 @@ +import 'package:xml/xml.dart'; + +class Rights { + final String status; + + Rights({ + this.status, + }); + + factory Rights.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Rights( + status: element.getAttribute("status"), + ); + } +} diff --git a/lib/webfeed/domain/media/scene.dart b/lib/webfeed/domain/media/scene.dart new file mode 100644 index 0000000..9eab8d0 --- /dev/null +++ b/lib/webfeed/domain/media/scene.dart @@ -0,0 +1,28 @@ +import 'package:webfeed/util/helpers.dart'; +import 'package:xml/xml.dart'; + +class Scene { + final String title; + final String description; + final String startTime; + final String endTime; + + Scene({ + this.title, + this.description, + this.startTime, + this.endTime, + }); + + factory Scene.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Scene( + title: findElementOrNull(element, "sceneTitle")?.text, + description: findElementOrNull(element, "sceneDescription")?.text, + startTime: findElementOrNull(element, "sceneStartTime")?.text, + endTime: findElementOrNull(element, "sceneEndTime")?.text, + ); + } +} diff --git a/lib/webfeed/domain/media/star_rating.dart b/lib/webfeed/domain/media/star_rating.dart new file mode 100644 index 0000000..ae4c400 --- /dev/null +++ b/lib/webfeed/domain/media/star_rating.dart @@ -0,0 +1,24 @@ +import 'package:xml/xml.dart'; + +class StarRating { + final double average; + final int count; + final int min; + final int max; + + StarRating({ + this.average, + this.count, + this.min, + this.max, + }); + + factory StarRating.parse(XmlElement element) { + return new StarRating( + average: double.tryParse(element.getAttribute("average") ?? "0"), + count: int.tryParse(element.getAttribute("count") ?? "0"), + min: int.tryParse(element.getAttribute("min") ?? "0"), + max: int.tryParse(element.getAttribute("max") ?? "0"), + ); + } +} diff --git a/lib/webfeed/domain/media/statistics.dart b/lib/webfeed/domain/media/statistics.dart new file mode 100644 index 0000000..d93461f --- /dev/null +++ b/lib/webfeed/domain/media/statistics.dart @@ -0,0 +1,18 @@ +import 'package:xml/xml.dart'; + +class Statistics { + final int views; + final int favorites; + + Statistics({ + this.views, + this.favorites, + }); + + factory Statistics.parse(XmlElement element) { + return new Statistics( + views: int.tryParse(element.getAttribute("views") ?? "0"), + favorites: int.tryParse(element.getAttribute("favorites") ?? "0"), + ); + } +} diff --git a/lib/webfeed/domain/media/status.dart b/lib/webfeed/domain/media/status.dart new file mode 100644 index 0000000..3071a09 --- /dev/null +++ b/lib/webfeed/domain/media/status.dart @@ -0,0 +1,21 @@ +import 'package:xml/xml.dart'; + +class Status { + final String state; + final String reason; + + Status({ + this.state, + this.reason, + }); + + factory Status.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Status( + state: element.getAttribute("state"), + reason: element.getAttribute("reason"), + ); + } +} diff --git a/lib/webfeed/domain/media/tags.dart b/lib/webfeed/domain/media/tags.dart new file mode 100644 index 0000000..c7001a7 --- /dev/null +++ b/lib/webfeed/domain/media/tags.dart @@ -0,0 +1,21 @@ +import 'package:xml/xml.dart'; + +class Tags { + final String tags; + final int weight; + + Tags({ + this.tags, + this.weight, + }); + + factory Tags.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Tags( + tags: element.text, + weight: int.tryParse(element.getAttribute("weight") ?? "1"), + ); + } +} diff --git a/lib/webfeed/domain/media/text.dart b/lib/webfeed/domain/media/text.dart new file mode 100644 index 0000000..1ff886b --- /dev/null +++ b/lib/webfeed/domain/media/text.dart @@ -0,0 +1,30 @@ +import 'package:xml/xml.dart'; + +class Text { + final String type; + final String lang; + final String start; + final String end; + final String value; + + Text({ + this.type, + this.lang, + this.start, + this.end, + this.value, + }); + + factory Text.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Text( + type: element.getAttribute("type"), + lang: element.getAttribute("lang"), + start: element.getAttribute("start"), + end: element.getAttribute("end"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/media/thumbnail.dart b/lib/webfeed/domain/media/thumbnail.dart new file mode 100644 index 0000000..8adce3b --- /dev/null +++ b/lib/webfeed/domain/media/thumbnail.dart @@ -0,0 +1,24 @@ +import 'package:xml/xml.dart'; + +class Thumbnail { + final String url; + final String width; + final String height; + final String time; + + Thumbnail({ + this.url, + this.width, + this.height, + this.time, + }); + + factory Thumbnail.parse(XmlElement element) { + return new Thumbnail( + url: element.getAttribute("url"), + width: element.getAttribute("width"), + height: element.getAttribute("height"), + time: element.getAttribute("time"), + ); + } +} diff --git a/lib/webfeed/domain/media/title.dart b/lib/webfeed/domain/media/title.dart new file mode 100644 index 0000000..8c875a6 --- /dev/null +++ b/lib/webfeed/domain/media/title.dart @@ -0,0 +1,21 @@ +import 'package:xml/xml.dart'; + +class Title { + final String type; + final String value; + + Title({ + this.type, + this.value, + }); + + factory Title.parse(XmlElement element) { + if (element == null) { + return null; + } + return new Title( + type: element.getAttribute("type"), + value: element.text, + ); + } +} diff --git a/lib/webfeed/domain/rss_category.dart b/lib/webfeed/domain/rss_category.dart new file mode 100644 index 0000000..845a837 --- /dev/null +++ b/lib/webfeed/domain/rss_category.dart @@ -0,0 +1,18 @@ +import 'package:xml/xml.dart'; + +class RssCategory { + final String domain; + final String value; + + RssCategory(this.domain, this.value); + + factory RssCategory.parse(XmlElement element) { + if (element == null) { + return null; + } + var domain = element.getAttribute("domain"); + var value = element.text; + + return RssCategory(domain, value); + } +} diff --git a/lib/webfeed/domain/rss_cloud.dart b/lib/webfeed/domain/rss_cloud.dart new file mode 100644 index 0000000..74d8b6a --- /dev/null +++ b/lib/webfeed/domain/rss_cloud.dart @@ -0,0 +1,29 @@ +import 'package:xml/xml.dart'; + +class RssCloud { + final String domain; + final String port; + final String path; + final String registerProcedure; + final String protocol; + + RssCloud( + this.domain, + this.port, + this.path, + this.registerProcedure, + this.protocol, + ); + + factory RssCloud.parse(XmlElement node) { + if (node == null) { + return null; + } + var domain = node.getAttribute("domain"); + var port = node.getAttribute("port"); + var path = node.getAttribute("path"); + var registerProcedure = node.getAttribute("registerProcedure"); + var protocol = node.getAttribute("protocol"); + return RssCloud(domain, port, path, registerProcedure, protocol); + } +} diff --git a/lib/webfeed/domain/rss_content.dart b/lib/webfeed/domain/rss_content.dart new file mode 100644 index 0000000..ded57bc --- /dev/null +++ b/lib/webfeed/domain/rss_content.dart @@ -0,0 +1,30 @@ +import 'package:xml/xml.dart'; + +final _imagesRegExp = new RegExp( + "]+)(?:'|\")", + multiLine: true, + caseSensitive: false, +); + +/// For RSS Content Module: +/// +/// - `xmlns:content="http://purl.org/rss/1.0/modules/content/"` +/// +class RssContent { + String value; + Iterable images; + + RssContent(this.value, this.images); + + factory RssContent.parse(XmlElement element) { + if (element == null) { + return null; + } + final content = element.text; + final images = []; + _imagesRegExp.allMatches(content).forEach((match) { + images.add(match.group(1)); + }); + return RssContent(content, images); + } +} diff --git a/lib/webfeed/domain/rss_enclosure.dart b/lib/webfeed/domain/rss_enclosure.dart new file mode 100644 index 0000000..4b125e2 --- /dev/null +++ b/lib/webfeed/domain/rss_enclosure.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +class RssEnclosure { + final String url; + final String type; + final int length; + + RssEnclosure(this.url, this.type, this.length); + + factory RssEnclosure.parse(XmlElement element) { + if (element == null) { + return null; + } + var url = element.getAttribute("url"); + var type = element.getAttribute("type"); + var length = int.tryParse(element.getAttribute("length") ?? "0"); + return RssEnclosure(url, type, length); + } +} diff --git a/lib/webfeed/domain/rss_feed.dart b/lib/webfeed/domain/rss_feed.dart new file mode 100644 index 0000000..7f995a9 --- /dev/null +++ b/lib/webfeed/domain/rss_feed.dart @@ -0,0 +1,108 @@ +import 'dart:core'; + +import '../domain/dublin_core/dublin_core.dart'; +import '../domain/rss_category.dart'; +import '../domain/rss_cloud.dart'; +import '../domain/rss_image.dart'; +import '../domain/rss_item.dart'; +import '../util/helpers.dart'; +import 'package:xml/xml.dart'; + +import 'rss_itunes.dart'; + +class RssFeed { + final String title; + final String author; + final String description; + final String link; + final List items; + + final RssImage image; + final RssCloud cloud; + final List categories; + final List skipDays; + final List skipHours; + final String lastBuildDate; + final String language; + final String generator; + final String copyright; + final String docs; + final String managingEditor; + final String rating; + final String webMaster; + final int ttl; + final DublinCore dc; + final RssItunes itunes; + + RssFeed({ + this.title, + this.author, + this.description, + this.link, + this.items, + this.image, + this.cloud, + this.categories, + this.skipDays, + this.skipHours, + this.lastBuildDate, + this.language, + this.generator, + this.copyright, + this.docs, + this.managingEditor, + this.rating, + this.webMaster, + this.ttl, + this.dc, + this.itunes, + }); + + factory RssFeed.parse(String xmlString) { + var document = parse(xmlString); + XmlElement channelElement; + try { + channelElement = document.findAllElements("channel").first; + } on StateError { + throw ArgumentError("channel not found"); + } + + return RssFeed( + title: findElementOrNull(channelElement, "title")?.text, + author: findElementOrNull(channelElement, "author")?.text, + description: findElementOrNull(channelElement, "description")?.text, + link: findElementOrNull(channelElement, "link")?.text, + items: channelElement.findElements("item").map((element) { + return RssItem.parse(element); + }).toList(), + image: RssImage.parse(findElementOrNull(channelElement, "image")), + cloud: RssCloud.parse(findElementOrNull(channelElement, "cloud")), + categories: channelElement.findElements("category").map((element) { + return RssCategory.parse(element); + }).toList(), + skipDays: findElementOrNull(channelElement, "skipDays") + ?.findAllElements("day") + ?.map((element) { + return element.text; + })?.toList() ?? + [], + skipHours: findElementOrNull(channelElement, "skipHours") + ?.findAllElements("hour") + ?.map((element) { + return int.tryParse(element.text ?? "0"); + })?.toList() ?? + [], + lastBuildDate: findElementOrNull(channelElement, "lastBuildDate")?.text, + language: findElementOrNull(channelElement, "language")?.text, + generator: findElementOrNull(channelElement, "generator")?.text, + copyright: findElementOrNull(channelElement, "copyright")?.text, + docs: findElementOrNull(channelElement, "docs")?.text, + managingEditor: findElementOrNull(channelElement, "managingEditor")?.text, + rating: findElementOrNull(channelElement, "rating")?.text, + webMaster: findElementOrNull(channelElement, "webMaster")?.text, + ttl: int.tryParse(findElementOrNull(channelElement, "ttl")?.text ?? "0"), + dc: DublinCore.parse(channelElement), + itunes: RssItunes.parse(channelElement), + ); + } +} diff --git a/lib/webfeed/domain/rss_image.dart b/lib/webfeed/domain/rss_image.dart new file mode 100644 index 0000000..680c47d --- /dev/null +++ b/lib/webfeed/domain/rss_image.dart @@ -0,0 +1,21 @@ +import '../util/helpers.dart'; +import 'package:xml/xml.dart'; + +class RssImage { + final String title; + final String url; + final String link; + + RssImage(this.title, this.url, this.link); + + factory RssImage.parse(XmlElement element) { + if (element == null) { + return null; + } + var title = findElementOrNull(element, "title")?.text; + var url = findElementOrNull(element, "url")?.text; + var link = findElementOrNull(element, "link")?.text; + + return RssImage(title, url, link); + } +} diff --git a/lib/webfeed/domain/rss_item.dart b/lib/webfeed/domain/rss_item.dart new file mode 100644 index 0000000..cf42669 --- /dev/null +++ b/lib/webfeed/domain/rss_item.dart @@ -0,0 +1,66 @@ +import '../domain/dublin_core/dublin_core.dart'; +import '../domain/media/media.dart'; +import '../domain/rss_category.dart'; +import '../domain/rss_content.dart'; +import '../domain/rss_enclosure.dart'; +import '../domain/rss_source.dart'; +import '../util/helpers.dart'; +import 'package:xml/xml.dart'; + +import 'rss_item_itunes.dart'; + +class RssItem { + final String title; + final String description; + final String link; + + final List categories; + final String guid; + final String pubDate; + final String author; + final String comments; + final RssSource source; + final RssContent content; + final Media media; + final RssEnclosure enclosure; + final DublinCore dc; + final RssItemItunes itunes; + + RssItem({ + this.title, + this.description, + this.link, + this.categories, + this.guid, + this.pubDate, + this.author, + this.comments, + this.source, + this.content, + this.media, + this.enclosure, + this.dc, + this.itunes, + }); + + factory RssItem.parse(XmlElement element) { + return RssItem( + title: findElementOrNull(element, "title")?.text, + description: findElementOrNull(element, "description")?.text, + link: findElementOrNull(element, "link")?.text, + categories: element.findElements("category").map((element) { + return RssCategory.parse(element); + }).toList(), + guid: findElementOrNull(element, "guid")?.text, + pubDate: findElementOrNull(element, "pubDate")?.text, + author: findElementOrNull(element, "author")?.text, + comments: findElementOrNull(element, "comments")?.text, + source: RssSource.parse(findElementOrNull(element, "source")), + content: RssContent.parse(findElementOrNull(element, "content:encoded")), + media: Media.parse(element), + enclosure: RssEnclosure.parse(findElementOrNull(element, "enclosure")), + dc: DublinCore.parse(element), + itunes: RssItemItunes.parse(element), + ); + } +} diff --git a/lib/webfeed/domain/rss_item_itunes.dart b/lib/webfeed/domain/rss_item_itunes.dart new file mode 100644 index 0000000..e4295e5 --- /dev/null +++ b/lib/webfeed/domain/rss_item_itunes.dart @@ -0,0 +1,83 @@ +import '../util/helpers.dart'; +import 'package:xml/xml.dart'; + +import 'rss_itunes_category.dart'; +import 'rss_itunes_episode_type.dart'; +import 'rss_itunes_image.dart'; + +class RssItemItunes { + final String title; + final int episode; + final int season; + final Duration duration; + final RssItunesEpisodeType episodeType; + final String author; + final String summary; + final bool explicit; + final String subtitle; + final List keywords; + final RssItunesImage image; + final RssItunesCategory category; + final bool block; + + RssItemItunes({ + this.title, + this.episode, + this.season, + this.duration, + this.episodeType, + this.author, + this.summary, + this.explicit, + this.subtitle, + this.keywords, + this.image, + this.category, + this.block, + }); + + factory RssItemItunes.parse(XmlElement element) { + if (element == null) { + return null; + } + var episodeStr = findElementOrNull(element, "itunes:episode")?.text?.trim(); + var seasonStr = findElementOrNull(element, "itunes:season")?.text?.trim(); + var durationStr = findElementOrNull(element, "itunes:duration")?.text?.trim(); + + return RssItemItunes( + title: findElementOrNull(element, "itunes:title")?.text?.trim(), + //episode: episodeStr == null ? null : int.parse(episodeStr), + //season: seasonStr == null ? null : int.parse(seasonStr), + duration: durationStr == null ? null : parseDuration(durationStr), + episodeType: newRssItunesEpisodeType(findElementOrNull(element, "itunes:episodeType")), + author: findElementOrNull(element, "itunes:author")?.text?.trim(), + summary: findElementOrNull(element, "itunes:summary")?.text?.trim(), + explicit: parseBoolLiteral(element, "itunes:explicit"), + subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(), + keywords: findElementOrNull(element, "itunes:keywords")?.text?.split(",")?.map((keyword) => keyword.trim())?.toList(), + image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")), + category: RssItunesCategory.parse( + findElementOrNull(element, "itunes:category")), + block: parseBoolLiteral(element, "itunes:block"), + ); + } +} + +Duration parseDuration(String s) { + var hours = 0; + var minutes = 0; + var seconds = 0; + var parts = s.split(':'); + if (parts.length > 2) { + hours = int.parse(parts[parts.length - 3]); + } + if (parts.length > 1) { + minutes = int.parse(parts[parts.length - 2]); + } + seconds = int.parse(parts[parts.length - 1]); + return Duration( + hours: hours, + minutes: minutes, + seconds: seconds, + ); +} diff --git a/lib/webfeed/domain/rss_itunes.dart b/lib/webfeed/domain/rss_itunes.dart new file mode 100644 index 0000000..4a260c8 --- /dev/null +++ b/lib/webfeed/domain/rss_itunes.dart @@ -0,0 +1,70 @@ +import '../util/helpers.dart'; +import 'package:xml/xml.dart'; + +//import 'package:webfeed/util/helpers.dart'; + +import 'rss_itunes_category.dart'; +import 'rss_itunes_image.dart'; +import 'rss_itunes_owner.dart'; +import 'rss_itunes_type.dart'; + +class RssItunes { + final String author; + final String summary; + final bool explicit; + final String title; + final String subtitle; + final RssItunesOwner owner; + final List keywords; + final RssItunesImage image; + final List categories; + final RssItunesType type; + final String newFeedUrl; + final bool block; + final bool complete; + + RssItunes({ + this.author, + this.summary, + this.explicit, + this.title, + this.subtitle, + this.owner, + this.keywords, + this.image, + this.categories, + this.type, + this.newFeedUrl, + this.block, + this.complete, + }); + + factory RssItunes.parse(XmlElement element) { + if (element == null) { + return null; + } + return RssItunes( + author: findElementOrNull(element, "itunes:author")?.text?.trim(), + summary: findElementOrNull(element, "itunes:summary")?.text?.trim(), + explicit: parseBoolLiteral(element, "itunes:explicit"), + title: findElementOrNull(element, "itunes:title")?.text?.trim(), + subtitle: findElementOrNull(element, "itunes:subtitle")?.text?.trim(), + owner: RssItunesOwner.parse(findElementOrNull(element, "itunes:owner")), + keywords: findElementOrNull(element, "itunes:keywords") + ?.text + ?.split(",") + ?.map((keyword) => keyword.trim()) + ?.toList(), + image: RssItunesImage.parse(findElementOrNull(element, "itunes:image")), + categories: findAllDirectElementsOrNull(element, "itunes:category") + .map((ele) => RssItunesCategory.parse(ele)) + .toList(), + type: newRssItunesType(findElementOrNull(element, "itunes:type")), + newFeedUrl: + findElementOrNull(element, "itunes:new-feed-url")?.text?.trim(), + block: parseBoolLiteral(element, "itunes:block"), + complete: parseBoolLiteral(element, "itunes:complete"), + ); + } +} + diff --git a/lib/webfeed/domain/rss_itunes_category.dart b/lib/webfeed/domain/rss_itunes_category.dart new file mode 100644 index 0000000..b79f339 --- /dev/null +++ b/lib/webfeed/domain/rss_itunes_category.dart @@ -0,0 +1,24 @@ +import 'package:xml/xml.dart'; + +class RssItunesCategory { + final String category; + final List subCategories; + + RssItunesCategory({this.category, this.subCategories}); + + factory RssItunesCategory.parse(XmlElement element) { + if (element == null) return null; + + Iterable subCategories; + try { + subCategories = element.findElements("itunes:category"); + } on StateError { + subCategories = null; + } + return RssItunesCategory( + category: element.getAttribute("text")?.trim(), + subCategories: + subCategories?.map((ele) => ele.getAttribute("text")?.trim())?.toList(), + ); + } +} diff --git a/lib/webfeed/domain/rss_itunes_episode_type.dart b/lib/webfeed/domain/rss_itunes_episode_type.dart new file mode 100644 index 0000000..30e639d --- /dev/null +++ b/lib/webfeed/domain/rss_itunes_episode_type.dart @@ -0,0 +1,19 @@ +import 'package:xml/xml.dart'; + +enum RssItunesEpisodeType {full, trailer, bonus} + +RssItunesEpisodeType newRssItunesEpisodeType(XmlElement element) { + // "full" is default type + if (element == null) return RssItunesEpisodeType.full; + + switch (element.text) { + case "full": + return RssItunesEpisodeType.full; + case "trailer": + return RssItunesEpisodeType.trailer; + case "bonus": + return RssItunesEpisodeType.bonus; + default: + return null; + } +} diff --git a/lib/webfeed/domain/rss_itunes_image.dart b/lib/webfeed/domain/rss_itunes_image.dart new file mode 100644 index 0000000..7b2ef2f --- /dev/null +++ b/lib/webfeed/domain/rss_itunes_image.dart @@ -0,0 +1,14 @@ +import 'package:xml/xml.dart'; + +class RssItunesImage { + final String href; + + RssItunesImage({this.href}); + + factory RssItunesImage.parse(XmlElement element) { + if (element == null) return null; + return RssItunesImage( + href: element.getAttribute("href")?.trim(), + ); + } +} diff --git a/lib/webfeed/domain/rss_itunes_owner.dart b/lib/webfeed/domain/rss_itunes_owner.dart new file mode 100644 index 0000000..4bcadc5 --- /dev/null +++ b/lib/webfeed/domain/rss_itunes_owner.dart @@ -0,0 +1,18 @@ +import 'package:xml/xml.dart'; + +import '../util/helpers.dart'; + +class RssItunesOwner { + final String name; + final String email; + + RssItunesOwner({this.name, this.email}); + + factory RssItunesOwner.parse(XmlElement element) { + if (element == null) return null; + return RssItunesOwner( + name: findElementOrNull(element, "itunes:name")?.text?.trim(), + email: findElementOrNull(element, "itunes:email")?.text?.trim(), + ); + } +} diff --git a/lib/webfeed/domain/rss_itunes_type.dart b/lib/webfeed/domain/rss_itunes_type.dart new file mode 100644 index 0000000..b2cb031 --- /dev/null +++ b/lib/webfeed/domain/rss_itunes_type.dart @@ -0,0 +1,17 @@ +import 'package:xml/xml.dart'; + +enum RssItunesType { episodic, serial } + +RssItunesType newRssItunesType(XmlElement element) { + // "episodic" is default type + if (element == null) return RssItunesType.episodic; + + switch (element.text) { + case "episodic": + return RssItunesType.episodic; + case "serial": + return RssItunesType.serial; + default: + return null; + } +} diff --git a/lib/webfeed/domain/rss_source.dart b/lib/webfeed/domain/rss_source.dart new file mode 100644 index 0000000..6d82be8 --- /dev/null +++ b/lib/webfeed/domain/rss_source.dart @@ -0,0 +1,18 @@ +import 'package:xml/xml.dart'; + +class RssSource { + final String url; + final String value; + + RssSource(this.url, this.value); + + factory RssSource.parse(XmlElement element) { + if (element == null) { + return null; + } + var url = element.getAttribute("url"); + var value = element.text; + + return RssSource(url, value); + } +} diff --git a/lib/webfeed/util/helpers.dart b/lib/webfeed/util/helpers.dart new file mode 100644 index 0000000..179926c --- /dev/null +++ b/lib/webfeed/util/helpers.dart @@ -0,0 +1,28 @@ +import 'dart:core'; + +import 'package:xml/xml.dart'; + +XmlElement findElementOrNull(XmlElement element, String name, + {String namespace}) { + try { + return element.findAllElements(name, namespace: namespace).first; + } on StateError { + return null; + } +} + +List findAllDirectElementsOrNull(XmlElement element, String name, + {String namespace}) { + try { + return element.findElements(name, namespace: namespace).toList(); + } on StateError { + return null; + } +} + +bool parseBoolLiteral(XmlElement element, String tagName) { + var v = findElementOrNull(element, tagName)?.text?.toLowerCase()?.trim(); + if (v == null) return null; + return ["yes", "true"].contains(v); +} + diff --git a/lib/webfeed/webfeed.dart b/lib/webfeed/webfeed.dart new file mode 100644 index 0000000..94c2632 --- /dev/null +++ b/lib/webfeed/webfeed.dart @@ -0,0 +1,13 @@ +export 'domain/atom_category.dart'; +export 'domain/atom_feed.dart'; +export 'domain/atom_generator.dart'; +export 'domain/atom_item.dart'; +export 'domain/atom_link.dart'; +export 'domain/atom_person.dart'; +export 'domain/atom_source.dart'; +export 'domain/rss_category.dart'; +export 'domain/rss_cloud.dart'; +export 'domain/rss_feed.dart'; +export 'domain/rss_image.dart'; +export 'domain/rss_item.dart'; +export 'domain/rss_source.dart'; diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..a370d47 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,446 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + archive: + dependency: transitive + description: + name: archive + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.11" + args: + dependency: transitive + description: + name: args + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.2" + async: + dependency: transitive + description: + name: async + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" + audiofileplayer: + dependency: "direct dev" + description: + name: audiofileplayer + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + cached_network_image: + dependency: "direct dev" + description: + name: cached_network_image + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.14.11" + color_thief_flutter: + dependency: "direct dev" + description: + name: color_thief_flutter + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.2" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.16.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3" + dio: + dependency: "direct dev" + description: + name: dio + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.8" + file_picker: + dependency: "direct dev" + description: + name: file_picker + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.3+2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.3" + flutter_downloader: + dependency: "direct dev" + description: + name: flutter_downloader + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + flutter_html: + dependency: "direct dev" + description: + name: flutter_html + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.1" + flutter_statusbarcolor: + dependency: "direct dev" + description: + name: flutter_statusbarcolor + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct dev" + description: + name: fluttertoast + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" + google_fonts: + dependency: "direct dev" + description: + name: google_fonts + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.2" + html: + dependency: transitive + description: + name: html + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.14.0+3" + http: + dependency: transitive + description: + name: http + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.0+4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" + image: + dependency: transitive + description: + name: image + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + intl: + dependency: "direct dev" + description: + name: intl + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.16.1" + json_annotation: + dependency: "direct dev" + description: + name: json_annotation + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.4" + marquee: + dependency: "direct dev" + description: + name: marquee + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.6" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.8" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.0.4" + network_image_to_byte: + dependency: "direct dev" + description: + name: network_image_to_byte + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.0.1" + path: + dependency: transitive + description: + name: path + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.4" + path_provider: + dependency: "direct dev" + description: + name: path_provider + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.1" + pedantic: + dependency: transitive + description: + name: pedantic + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.8.0+1" + permission_handler: + dependency: "direct dev" + description: + name: permission_handler + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.0+hotfix.3" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + provider: + dependency: "direct dev" + description: + name: provider + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.2" + quantize_dart: + dependency: transitive + description: + name: quantize_dart + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.3" + quiver: + dependency: transitive + description: + name: quiver + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.5.5" + sqflite: + dependency: "direct dev" + description: + name: sqflite + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.3" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.11" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.6" + url_launcher: + dependency: "direct dev" + description: + name: url_launcher + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.4.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.0.1+2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.5" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.1" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.4" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.8" + webfeed: + dependency: "direct dev" + description: + name: webfeed + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.2" + xml: + dependency: "direct dev" + description: + name: xml + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.5.0" +sdks: + dart: ">=2.6.0 <3.0.0" + flutter: ">=1.12.13 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..1274640 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,94 @@ +name: Tsacdop +description: An easy-use podacasts player. + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +version: 0.1.0 + +environment: + sdk: ">=2.3.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^0.1.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_statusbarcolor: ^0.2.3 + json_annotation: any + cached_network_image: any + sqflite: any + flutter_html: any + webfeed: any + path_provider: any + color_thief_flutter: ^1.0.1 + provider: ^4.0.1 + google_fonts: ^0.3.2 + dio: ^3.0.8 + network_image_to_byte: ^0.0.1 + file_picker: ^1.2.0 + xml: ^3.5.0 + marquee: ^1.3.1 + audiofileplayer: ^1.1.1 + flutter_downloader: ^1.4.1 + permission_handler: ^4.2.0+hotfix.3 + fluttertoast: ^3.1.3 + intl: ^0.16.1 + url_launcher: ^5.4.1 + + +# For information on the generic Dart part of this file, see the +# following page: https://www.dartlang.org/tools/pub/pubspec + +# The following section is specific to Flutter. +flutter: + assets: + - assets/ + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.io/assets-and-images/#resolution-aware. + + # For details regarding adding assets from package dependencies, see + # https://flutter.io/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.io/custom-fonts/#from-packages diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..54ecf53 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility that Flutter provides. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:Tsacdop/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}