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 0000000..f9c251c Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_stat_forward_30.png differ 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 0000000..0668dbe Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_stat_pause_circle_filled.png differ 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 0000000..b513412 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_stat_play_circle_filled.png differ 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 0000000..d2ad64d Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_stat_replay_10.png differ 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 0000000..8f07c28 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_stat_forward_30.png differ 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 0000000..c20d52e Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_stat_pause_circle_filled.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_play_circle_filled.png b/android/app/src/main/res/drawable-mdpi/ic_stat_play_circle_filled.png new file mode 100644 index 0000000..f11ffc9 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_stat_play_circle_filled.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_replay_10.png b/android/app/src/main/res/drawable-mdpi/ic_stat_replay_10.png new file mode 100644 index 0000000..6e893ef Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_stat_replay_10.png differ 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 0000000..ef4d6b0 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_stat_forward_30.png differ 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 0000000..b498a33 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_stat_pause_circle_filled.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_stat_play_circle_filled.png b/android/app/src/main/res/drawable-xhdpi/ic_stat_play_circle_filled.png new file mode 100644 index 0000000..69a7f1d Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_stat_play_circle_filled.png differ 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 0000000..ff8dfc5 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_stat_replay_10.png differ 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 0000000..425c29d Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_stat_forward_30.png differ 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 0000000..163ea36 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_stat_pause_circle_filled.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_stat_play_circle_filled.png b/android/app/src/main/res/drawable-xxhdpi/ic_stat_play_circle_filled.png new file mode 100644 index 0000000..09b3a48 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_stat_play_circle_filled.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_stat_replay_10.png b/android/app/src/main/res/drawable-xxhdpi/ic_stat_replay_10.png new file mode 100644 index 0000000..fc0fd8d Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_stat_replay_10.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_stat_forward_30.png b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_forward_30.png new file mode 100644 index 0000000..fa6885a Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_forward_30.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_stat_pause_circle_filled.png b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_pause_circle_filled.png new file mode 100644 index 0000000..0559b5b Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_pause_circle_filled.png differ 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 0000000..e60a66e Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_play_circle_filled.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_stat_replay_10.png b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_replay_10.png new file mode 100644 index 0000000..3069637 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_replay_10.png differ 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 0000000..4b5a9b2 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ 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 0000000..f2a66fe Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ 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 0000000..c81f42f Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ 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 0000000..ca10e91 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ 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 0000000..3ec65e0 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ 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 0000000..cfad713 Binary files /dev/null and b/assets/listennote.png differ 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 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..28c6bf0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ 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 0000000..f091b6b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cde121 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ 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 0000000..d0ef06e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..dcdc230 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..2ccbfd9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ 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 0000000..c8f9ed8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ 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 0000000..a6d6b86 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ 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 0000000..a6d6b86 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ 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 0000000..75b2d16 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ 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 0000000..c4df70d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..6a84f41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..d0e1f58 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ 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 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ 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 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ 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); + }); +}