commit 422a3f880238f3885dac1e6ec2575ef2b818b45a Author: fengshengxiong Date: Tue Jun 11 11:53:38 2024 +0800 first commit diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 0000000..4efffa7 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.22.1" +} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7faac7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,141 @@ +# Do not remove or rename entries in this file, only add new ones +# See https://github.com/flutter/flutter/issues/128635 for more context. + +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.classpath +.project +.settings/ +.vscode/* + +# Flutter repo-specific +/bin/cache/ +/bin/internal/bootstrap.bat +/bin/internal/bootstrap.sh +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/devicelab/ABresults*.json +/dev/docs/doc/ +/dev/docs/api_docs.zip +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version +analysis_benchmark.json + +# packages file containing multi-root paths +.packages.generated + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +**/generated_plugin_registrant.dart +.packages +.pub-preload-cache/ +.pub-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds + +# Android related +**/android/**/gradle-wrapper.jar +.gradle/ +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +#*.jks + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# macOS +**/Flutter/ephemeral/ +**/Pods/ +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/ephemeral +**/xcuserdata/ + +# Windows +**/windows/flutter/generated_plugin_registrant.cc +**/windows/flutter/generated_plugin_registrant.h +**/windows/flutter/generated_plugins.cmake + +# Linux +**/linux/flutter/generated_plugin_registrant.cc +**/linux/flutter/generated_plugin_registrant.h +**/linux/flutter/generated_plugins.cmake + +# Coverage +coverage/ + +# Symbols +app.*.symbols + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +!/dev/ci/**/Gemfile.lock +!.vscode/settings.json + +# FVM Version Cache +.fvm/ \ No newline at end of file diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..30cb34c --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# 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: "a14f74ff3a1cbd521163c5f03d68113d50af93d3" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + - platform: android + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + - platform: ios + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad56e02 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# music_player + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..ce65aea --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +#key.properties +#**/*.keystore +#**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000..a1a328e --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,87 @@ +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file("local.properties") +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader("UTF-8") { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") +if (flutterVersionCode == null) { + flutterVersionCode = "1" +} + +def flutterVersionName = localProperties.getProperty("flutter.versionName") +if (flutterVersionName == null) { + flutterVersionName = "1.0" +} + +def keystorePropertiesFile = rootProject.file("key.properties") +def keystoreProperties = new Properties() +keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + +android { + namespace = "com.tone.music.offline" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.tone.music.offline" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. +// minSdk = flutter.minSdkVersion + minSdk = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName + } + + signingConfigs { + release { + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + } + } + + buildTypes { + debug { + signingConfig signingConfigs.release + } + + release { + ndk{ + abiFilters "armeabi", "armeabi-v7a", "arm64-v8a" + } + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.release + } + } + + android.applicationVariants.all { variant -> + variant.outputs.all { output -> + if (!variant.buildType.isDebuggable()) { + // 配置APK输出文件名 + outputFileName = "ToneSnap-${flutterVersionName}-${variant.buildType.name}.apk" + } + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /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..7933e3e --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/tone/music/offline/MainActivity.kt b/android/app/src/main/kotlin/com/tone/music/offline/MainActivity.kt new file mode 100644 index 0000000..0185449 --- /dev/null +++ b/android/app/src/main/kotlin/com/tone/music/offline/MainActivity.kt @@ -0,0 +1,5 @@ +package com.tone.music.offline + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..47f03ce --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + 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..47f03ce --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,11 @@ + + + + + + + + + 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..db77bb4 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-hdpi/launch_image.png b/android/app/src/main/res/mipmap-hdpi/launch_image.png new file mode 100755 index 0000000..63d0584 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launch_image.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png new file mode 100644 index 0000000..0feb5ba Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/launcher_icon.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..17987b7 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-mdpi/launch_image.png b/android/app/src/main/res/mipmap-mdpi/launch_image.png new file mode 100755 index 0000000..f6c79dd Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launch_image.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png new file mode 100644 index 0000000..8d3e176 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/launcher_icon.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..09d4391 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-xhdpi/launch_image.png b/android/app/src/main/res/mipmap-xhdpi/launch_image.png new file mode 100755 index 0000000..c57fd06 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launch_image.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png new file mode 100644 index 0000000..c3fa46e Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.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..d5f1c8d 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-xxhdpi/launch_image.png b/android/app/src/main/res/mipmap-xxhdpi/launch_image.png new file mode 100755 index 0000000..b00732b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launch_image.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png new file mode 100644 index 0000000..f80e815 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.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..4d6372e 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/mipmap-xxxhdpi/launch_image.png b/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png new file mode 100755 index 0000000..6550306 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launch_image.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png new file mode 100644 index 0000000..b5a020b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/android/app/src/main/res/values-night-v27/styles.xml b/android/app/src/main/res/values-night-v27/styles.xml new file mode 100644 index 0000000..2acbf37 --- /dev/null +++ b/android/app/src/main/res/values-night-v27/styles.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..8503258 --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/values-v27/styles.xml b/android/app/src/main/res/values-v27/styles.xml new file mode 100644 index 0000000..77b27e9 --- /dev/null +++ b/android/app/src/main/res/values-v27/styles.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file 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..3304935 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /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..fba34c4 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,20 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +ext.flutterFFmpegPackage = 'audio-lts' + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..3b5b324 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx4G -XX:+HeapDumpOnOutOfMemoryError +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..e1ca574 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..536165d --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} + +include ":app" diff --git a/android/tone_snap.jks b/android/tone_snap.jks new file mode 100644 index 0000000..b57840b Binary files /dev/null and b/android/tone_snap.jks differ diff --git a/assets/change_voice/audio/boy.jpeg b/assets/change_voice/audio/boy.jpeg new file mode 100755 index 0000000..7be949a Binary files /dev/null and b/assets/change_voice/audio/boy.jpeg differ diff --git a/assets/change_voice/audio/boy.mp3 b/assets/change_voice/audio/boy.mp3 new file mode 100644 index 0000000..8860487 Binary files /dev/null and b/assets/change_voice/audio/boy.mp3 differ diff --git a/assets/change_voice/audio/child.mp3 b/assets/change_voice/audio/child.mp3 new file mode 100644 index 0000000..38ce9ef Binary files /dev/null and b/assets/change_voice/audio/child.mp3 differ diff --git a/assets/change_voice/audio/child.png b/assets/change_voice/audio/child.png new file mode 100755 index 0000000..81eb50b Binary files /dev/null and b/assets/change_voice/audio/child.png differ diff --git a/assets/change_voice/audio/elder.mp3 b/assets/change_voice/audio/elder.mp3 new file mode 100644 index 0000000..6403a51 Binary files /dev/null and b/assets/change_voice/audio/elder.mp3 differ diff --git a/assets/change_voice/audio/elder.png b/assets/change_voice/audio/elder.png new file mode 100755 index 0000000..afad274 Binary files /dev/null and b/assets/change_voice/audio/elder.png differ diff --git a/assets/change_voice/audio/girl.jpeg b/assets/change_voice/audio/girl.jpeg new file mode 100755 index 0000000..c8cc102 Binary files /dev/null and b/assets/change_voice/audio/girl.jpeg differ diff --git a/assets/change_voice/audio/girl.mp3 b/assets/change_voice/audio/girl.mp3 new file mode 100644 index 0000000..4f80d1a Binary files /dev/null and b/assets/change_voice/audio/girl.mp3 differ diff --git a/assets/change_voice/audio/man.mp3 b/assets/change_voice/audio/man.mp3 new file mode 100644 index 0000000..ef871dd Binary files /dev/null and b/assets/change_voice/audio/man.mp3 differ diff --git a/assets/change_voice/audio/man.png b/assets/change_voice/audio/man.png new file mode 100755 index 0000000..92ab77b Binary files /dev/null and b/assets/change_voice/audio/man.png differ diff --git a/assets/change_voice/audio/monsters.mp3 b/assets/change_voice/audio/monsters.mp3 new file mode 100644 index 0000000..7c751d5 Binary files /dev/null and b/assets/change_voice/audio/monsters.mp3 differ diff --git a/assets/change_voice/audio/monsters.png b/assets/change_voice/audio/monsters.png new file mode 100755 index 0000000..6d9eb39 Binary files /dev/null and b/assets/change_voice/audio/monsters.png differ diff --git a/assets/change_voice/audio/rapper.jpeg b/assets/change_voice/audio/rapper.jpeg new file mode 100755 index 0000000..63a0e84 Binary files /dev/null and b/assets/change_voice/audio/rapper.jpeg differ diff --git a/assets/change_voice/audio/rapper.mp3 b/assets/change_voice/audio/rapper.mp3 new file mode 100644 index 0000000..c3c1e6c Binary files /dev/null and b/assets/change_voice/audio/rapper.mp3 differ diff --git a/assets/change_voice/audio/robot.jpeg b/assets/change_voice/audio/robot.jpeg new file mode 100755 index 0000000..0f3d960 Binary files /dev/null and b/assets/change_voice/audio/robot.jpeg differ diff --git a/assets/change_voice/audio/robot.wav b/assets/change_voice/audio/robot.wav new file mode 100644 index 0000000..3371d89 Binary files /dev/null and b/assets/change_voice/audio/robot.wav differ diff --git a/assets/change_voice/audio/sexy.jpeg b/assets/change_voice/audio/sexy.jpeg new file mode 100755 index 0000000..f3dfc03 Binary files /dev/null and b/assets/change_voice/audio/sexy.jpeg differ diff --git a/assets/change_voice/audio/sexy.mp3 b/assets/change_voice/audio/sexy.mp3 new file mode 100644 index 0000000..1a88cfb Binary files /dev/null and b/assets/change_voice/audio/sexy.mp3 differ diff --git a/assets/change_voice/audio/woman.mp3 b/assets/change_voice/audio/woman.mp3 new file mode 100644 index 0000000..c8fc0b6 Binary files /dev/null and b/assets/change_voice/audio/woman.mp3 differ diff --git a/assets/change_voice/audio/woman.png b/assets/change_voice/audio/woman.png new file mode 100755 index 0000000..4920617 Binary files /dev/null and b/assets/change_voice/audio/woman.png differ diff --git a/assets/change_voice/images/2.0x/about.png b/assets/change_voice/images/2.0x/about.png new file mode 100644 index 0000000..2e0830a Binary files /dev/null and b/assets/change_voice/images/2.0x/about.png differ diff --git a/assets/change_voice/images/2.0x/arrow_down_back.png b/assets/change_voice/images/2.0x/arrow_down_back.png new file mode 100644 index 0000000..f3b0056 Binary files /dev/null and b/assets/change_voice/images/2.0x/arrow_down_back.png differ diff --git a/assets/change_voice/images/2.0x/bnb1_selected.png b/assets/change_voice/images/2.0x/bnb1_selected.png new file mode 100644 index 0000000..360553a Binary files /dev/null and b/assets/change_voice/images/2.0x/bnb1_selected.png differ diff --git a/assets/change_voice/images/2.0x/bnb1_unselected.png b/assets/change_voice/images/2.0x/bnb1_unselected.png new file mode 100644 index 0000000..ce2955d Binary files /dev/null and b/assets/change_voice/images/2.0x/bnb1_unselected.png differ diff --git a/assets/change_voice/images/2.0x/bnb2_selected.png b/assets/change_voice/images/2.0x/bnb2_selected.png new file mode 100644 index 0000000..6a99806 Binary files /dev/null and b/assets/change_voice/images/2.0x/bnb2_selected.png differ diff --git a/assets/change_voice/images/2.0x/bnb2_unselected.png b/assets/change_voice/images/2.0x/bnb2_unselected.png new file mode 100644 index 0000000..fbd7d23 Binary files /dev/null and b/assets/change_voice/images/2.0x/bnb2_unselected.png differ diff --git a/assets/change_voice/images/2.0x/bnb3_selected.png b/assets/change_voice/images/2.0x/bnb3_selected.png new file mode 100644 index 0000000..26dfaf7 Binary files /dev/null and b/assets/change_voice/images/2.0x/bnb3_selected.png differ diff --git a/assets/change_voice/images/2.0x/bnb3_unselected.png b/assets/change_voice/images/2.0x/bnb3_unselected.png new file mode 100644 index 0000000..d9d4978 Binary files /dev/null and b/assets/change_voice/images/2.0x/bnb3_unselected.png differ diff --git a/assets/change_voice/images/2.0x/bnb4_selected.png b/assets/change_voice/images/2.0x/bnb4_selected.png new file mode 100644 index 0000000..73cdb63 Binary files /dev/null and b/assets/change_voice/images/2.0x/bnb4_selected.png differ diff --git a/assets/change_voice/images/2.0x/bnb4_unselected.png b/assets/change_voice/images/2.0x/bnb4_unselected.png new file mode 100644 index 0000000..c6120f1 Binary files /dev/null and b/assets/change_voice/images/2.0x/bnb4_unselected.png differ diff --git a/assets/change_voice/images/2.0x/change_voice_bg.png b/assets/change_voice/images/2.0x/change_voice_bg.png new file mode 100644 index 0000000..57dfdf6 Binary files /dev/null and b/assets/change_voice/images/2.0x/change_voice_bg.png differ diff --git a/assets/change_voice/images/2.0x/change_voice_item_frame.png b/assets/change_voice/images/2.0x/change_voice_item_frame.png new file mode 100644 index 0000000..03400b8 Binary files /dev/null and b/assets/change_voice/images/2.0x/change_voice_item_frame.png differ diff --git a/assets/change_voice/images/2.0x/change_voice_item_selected.png b/assets/change_voice/images/2.0x/change_voice_item_selected.png new file mode 100644 index 0000000..67e9007 Binary files /dev/null and b/assets/change_voice/images/2.0x/change_voice_item_selected.png differ diff --git a/assets/change_voice/images/2.0x/favorite.png b/assets/change_voice/images/2.0x/favorite.png new file mode 100644 index 0000000..cea2d8d Binary files /dev/null and b/assets/change_voice/images/2.0x/favorite.png differ diff --git a/assets/change_voice/images/2.0x/home_bg.png b/assets/change_voice/images/2.0x/home_bg.png new file mode 100644 index 0000000..d237f1f Binary files /dev/null and b/assets/change_voice/images/2.0x/home_bg.png differ diff --git a/assets/change_voice/images/2.0x/home_bnb_bg.png b/assets/change_voice/images/2.0x/home_bnb_bg.png new file mode 100644 index 0000000..8c5385a Binary files /dev/null and b/assets/change_voice/images/2.0x/home_bnb_bg.png differ diff --git a/assets/change_voice/images/2.0x/icon_chevron_right.png b/assets/change_voice/images/2.0x/icon_chevron_right.png new file mode 100644 index 0000000..2d431b1 Binary files /dev/null and b/assets/change_voice/images/2.0x/icon_chevron_right.png differ diff --git a/assets/change_voice/images/2.0x/more.png b/assets/change_voice/images/2.0x/more.png new file mode 100644 index 0000000..6c142b0 Binary files /dev/null and b/assets/change_voice/images/2.0x/more.png differ diff --git a/assets/change_voice/images/2.0x/not_favorite.png b/assets/change_voice/images/2.0x/not_favorite.png new file mode 100644 index 0000000..8c7a8b5 Binary files /dev/null and b/assets/change_voice/images/2.0x/not_favorite.png differ diff --git a/assets/change_voice/images/2.0x/not_played.png b/assets/change_voice/images/2.0x/not_played.png new file mode 100644 index 0000000..9920272 Binary files /dev/null and b/assets/change_voice/images/2.0x/not_played.png differ diff --git a/assets/change_voice/images/2.0x/not_played1.png b/assets/change_voice/images/2.0x/not_played1.png new file mode 100644 index 0000000..b6c2b1f Binary files /dev/null and b/assets/change_voice/images/2.0x/not_played1.png differ diff --git a/assets/change_voice/images/2.0x/play_sound_bg.png b/assets/change_voice/images/2.0x/play_sound_bg.png new file mode 100644 index 0000000..44cc30b Binary files /dev/null and b/assets/change_voice/images/2.0x/play_sound_bg.png differ diff --git a/assets/change_voice/images/2.0x/playing.png b/assets/change_voice/images/2.0x/playing.png new file mode 100644 index 0000000..f89e9cf Binary files /dev/null and b/assets/change_voice/images/2.0x/playing.png differ diff --git a/assets/change_voice/images/2.0x/playing1.png b/assets/change_voice/images/2.0x/playing1.png new file mode 100644 index 0000000..a2fa374 Binary files /dev/null and b/assets/change_voice/images/2.0x/playing1.png differ diff --git a/assets/change_voice/images/2.0x/privacy.png b/assets/change_voice/images/2.0x/privacy.png new file mode 100644 index 0000000..ae9acc6 Binary files /dev/null and b/assets/change_voice/images/2.0x/privacy.png differ diff --git a/assets/change_voice/images/2.0x/settings_bg.png b/assets/change_voice/images/2.0x/settings_bg.png new file mode 100644 index 0000000..d4d35cf Binary files /dev/null and b/assets/change_voice/images/2.0x/settings_bg.png differ diff --git a/assets/change_voice/images/2.0x/subtract.png b/assets/change_voice/images/2.0x/subtract.png new file mode 100644 index 0000000..08d66a4 Binary files /dev/null and b/assets/change_voice/images/2.0x/subtract.png differ diff --git a/assets/change_voice/images/2.0x/the_monster.png b/assets/change_voice/images/2.0x/the_monster.png new file mode 100644 index 0000000..621f5f6 Binary files /dev/null and b/assets/change_voice/images/2.0x/the_monster.png differ diff --git a/assets/change_voice/images/2.0x/the_monster1.png b/assets/change_voice/images/2.0x/the_monster1.png new file mode 100644 index 0000000..2e98096 Binary files /dev/null and b/assets/change_voice/images/2.0x/the_monster1.png differ diff --git a/assets/change_voice/images/2.0x/the_monster2.png b/assets/change_voice/images/2.0x/the_monster2.png new file mode 100644 index 0000000..44b22d4 Binary files /dev/null and b/assets/change_voice/images/2.0x/the_monster2.png differ diff --git a/assets/change_voice/images/2.0x/upload_method_bg.png b/assets/change_voice/images/2.0x/upload_method_bg.png new file mode 100644 index 0000000..f40b631 Binary files /dev/null and b/assets/change_voice/images/2.0x/upload_method_bg.png differ diff --git a/assets/change_voice/images/2.0x/upload_pick.png b/assets/change_voice/images/2.0x/upload_pick.png new file mode 100644 index 0000000..7b0472d Binary files /dev/null and b/assets/change_voice/images/2.0x/upload_pick.png differ diff --git a/assets/change_voice/images/2.0x/upload_record_sound.png b/assets/change_voice/images/2.0x/upload_record_sound.png new file mode 100644 index 0000000..6aadc04 Binary files /dev/null and b/assets/change_voice/images/2.0x/upload_record_sound.png differ diff --git a/assets/change_voice/images/2.0x/user_agreement.png b/assets/change_voice/images/2.0x/user_agreement.png new file mode 100644 index 0000000..ac69958 Binary files /dev/null and b/assets/change_voice/images/2.0x/user_agreement.png differ diff --git a/assets/change_voice/images/2.0x/voice_change.png b/assets/change_voice/images/2.0x/voice_change.png new file mode 100644 index 0000000..2d703d0 Binary files /dev/null and b/assets/change_voice/images/2.0x/voice_change.png differ diff --git a/assets/change_voice/images/2.0x/voice_default.png b/assets/change_voice/images/2.0x/voice_default.png new file mode 100644 index 0000000..aaee93a Binary files /dev/null and b/assets/change_voice/images/2.0x/voice_default.png differ diff --git a/assets/change_voice/images/3.0x/about.png b/assets/change_voice/images/3.0x/about.png new file mode 100644 index 0000000..a2ae827 Binary files /dev/null and b/assets/change_voice/images/3.0x/about.png differ diff --git a/assets/change_voice/images/3.0x/arrow_down_back.png b/assets/change_voice/images/3.0x/arrow_down_back.png new file mode 100644 index 0000000..cbd0c9b Binary files /dev/null and b/assets/change_voice/images/3.0x/arrow_down_back.png differ diff --git a/assets/change_voice/images/3.0x/bnb1_selected.png b/assets/change_voice/images/3.0x/bnb1_selected.png new file mode 100644 index 0000000..f731a76 Binary files /dev/null and b/assets/change_voice/images/3.0x/bnb1_selected.png differ diff --git a/assets/change_voice/images/3.0x/bnb1_unselected.png b/assets/change_voice/images/3.0x/bnb1_unselected.png new file mode 100644 index 0000000..c3b9ebc Binary files /dev/null and b/assets/change_voice/images/3.0x/bnb1_unselected.png differ diff --git a/assets/change_voice/images/3.0x/bnb2_selected.png b/assets/change_voice/images/3.0x/bnb2_selected.png new file mode 100644 index 0000000..b67e9e5 Binary files /dev/null and b/assets/change_voice/images/3.0x/bnb2_selected.png differ diff --git a/assets/change_voice/images/3.0x/bnb2_unselected.png b/assets/change_voice/images/3.0x/bnb2_unselected.png new file mode 100644 index 0000000..f9112af Binary files /dev/null and b/assets/change_voice/images/3.0x/bnb2_unselected.png differ diff --git a/assets/change_voice/images/3.0x/bnb3_selected.png b/assets/change_voice/images/3.0x/bnb3_selected.png new file mode 100644 index 0000000..7974e0b Binary files /dev/null and b/assets/change_voice/images/3.0x/bnb3_selected.png differ diff --git a/assets/change_voice/images/3.0x/bnb3_unselected.png b/assets/change_voice/images/3.0x/bnb3_unselected.png new file mode 100644 index 0000000..d2f74be Binary files /dev/null and b/assets/change_voice/images/3.0x/bnb3_unselected.png differ diff --git a/assets/change_voice/images/3.0x/bnb4_selected.png b/assets/change_voice/images/3.0x/bnb4_selected.png new file mode 100644 index 0000000..870869b Binary files /dev/null and b/assets/change_voice/images/3.0x/bnb4_selected.png differ diff --git a/assets/change_voice/images/3.0x/bnb4_unselected.png b/assets/change_voice/images/3.0x/bnb4_unselected.png new file mode 100644 index 0000000..80c8923 Binary files /dev/null and b/assets/change_voice/images/3.0x/bnb4_unselected.png differ diff --git a/assets/change_voice/images/3.0x/change_voice_bg.png b/assets/change_voice/images/3.0x/change_voice_bg.png new file mode 100644 index 0000000..489031c Binary files /dev/null and b/assets/change_voice/images/3.0x/change_voice_bg.png differ diff --git a/assets/change_voice/images/3.0x/change_voice_item_frame.png b/assets/change_voice/images/3.0x/change_voice_item_frame.png new file mode 100644 index 0000000..34ac9ca Binary files /dev/null and b/assets/change_voice/images/3.0x/change_voice_item_frame.png differ diff --git a/assets/change_voice/images/3.0x/change_voice_item_selected.png b/assets/change_voice/images/3.0x/change_voice_item_selected.png new file mode 100644 index 0000000..8d4a4d8 Binary files /dev/null and b/assets/change_voice/images/3.0x/change_voice_item_selected.png differ diff --git a/assets/change_voice/images/3.0x/favorite.png b/assets/change_voice/images/3.0x/favorite.png new file mode 100644 index 0000000..b621f8e Binary files /dev/null and b/assets/change_voice/images/3.0x/favorite.png differ diff --git a/assets/change_voice/images/3.0x/home_bg.png b/assets/change_voice/images/3.0x/home_bg.png new file mode 100644 index 0000000..8fff64a Binary files /dev/null and b/assets/change_voice/images/3.0x/home_bg.png differ diff --git a/assets/change_voice/images/3.0x/home_bnb_bg.png b/assets/change_voice/images/3.0x/home_bnb_bg.png new file mode 100644 index 0000000..c9b78b6 Binary files /dev/null and b/assets/change_voice/images/3.0x/home_bnb_bg.png differ diff --git a/assets/change_voice/images/3.0x/icon_chevron_right.png b/assets/change_voice/images/3.0x/icon_chevron_right.png new file mode 100644 index 0000000..01baaac Binary files /dev/null and b/assets/change_voice/images/3.0x/icon_chevron_right.png differ diff --git a/assets/change_voice/images/3.0x/more.png b/assets/change_voice/images/3.0x/more.png new file mode 100644 index 0000000..d6ddfd4 Binary files /dev/null and b/assets/change_voice/images/3.0x/more.png differ diff --git a/assets/change_voice/images/3.0x/not_favorite.png b/assets/change_voice/images/3.0x/not_favorite.png new file mode 100644 index 0000000..517a273 Binary files /dev/null and b/assets/change_voice/images/3.0x/not_favorite.png differ diff --git a/assets/change_voice/images/3.0x/not_played.png b/assets/change_voice/images/3.0x/not_played.png new file mode 100644 index 0000000..9dc0446 Binary files /dev/null and b/assets/change_voice/images/3.0x/not_played.png differ diff --git a/assets/change_voice/images/3.0x/not_played1.png b/assets/change_voice/images/3.0x/not_played1.png new file mode 100644 index 0000000..c956d11 Binary files /dev/null and b/assets/change_voice/images/3.0x/not_played1.png differ diff --git a/assets/change_voice/images/3.0x/play_sound_bg.png b/assets/change_voice/images/3.0x/play_sound_bg.png new file mode 100644 index 0000000..e47f0a6 Binary files /dev/null and b/assets/change_voice/images/3.0x/play_sound_bg.png differ diff --git a/assets/change_voice/images/3.0x/playing.png b/assets/change_voice/images/3.0x/playing.png new file mode 100644 index 0000000..8768bff Binary files /dev/null and b/assets/change_voice/images/3.0x/playing.png differ diff --git a/assets/change_voice/images/3.0x/playing1.png b/assets/change_voice/images/3.0x/playing1.png new file mode 100644 index 0000000..cba3593 Binary files /dev/null and b/assets/change_voice/images/3.0x/playing1.png differ diff --git a/assets/change_voice/images/3.0x/privacy.png b/assets/change_voice/images/3.0x/privacy.png new file mode 100644 index 0000000..0817b16 Binary files /dev/null and b/assets/change_voice/images/3.0x/privacy.png differ diff --git a/assets/change_voice/images/3.0x/settings_bg.png b/assets/change_voice/images/3.0x/settings_bg.png new file mode 100644 index 0000000..1356a5b Binary files /dev/null and b/assets/change_voice/images/3.0x/settings_bg.png differ diff --git a/assets/change_voice/images/3.0x/subtract.png b/assets/change_voice/images/3.0x/subtract.png new file mode 100644 index 0000000..e2b8941 Binary files /dev/null and b/assets/change_voice/images/3.0x/subtract.png differ diff --git a/assets/change_voice/images/3.0x/the_monster.png b/assets/change_voice/images/3.0x/the_monster.png new file mode 100644 index 0000000..dae358d Binary files /dev/null and b/assets/change_voice/images/3.0x/the_monster.png differ diff --git a/assets/change_voice/images/3.0x/the_monster1.png b/assets/change_voice/images/3.0x/the_monster1.png new file mode 100644 index 0000000..c38e1c9 Binary files /dev/null and b/assets/change_voice/images/3.0x/the_monster1.png differ diff --git a/assets/change_voice/images/3.0x/the_monster2.png b/assets/change_voice/images/3.0x/the_monster2.png new file mode 100644 index 0000000..a32cad8 Binary files /dev/null and b/assets/change_voice/images/3.0x/the_monster2.png differ diff --git a/assets/change_voice/images/3.0x/upload_method_bg.png b/assets/change_voice/images/3.0x/upload_method_bg.png new file mode 100644 index 0000000..633272c Binary files /dev/null and b/assets/change_voice/images/3.0x/upload_method_bg.png differ diff --git a/assets/change_voice/images/3.0x/upload_pick.png b/assets/change_voice/images/3.0x/upload_pick.png new file mode 100644 index 0000000..92de659 Binary files /dev/null and b/assets/change_voice/images/3.0x/upload_pick.png differ diff --git a/assets/change_voice/images/3.0x/upload_record_sound.png b/assets/change_voice/images/3.0x/upload_record_sound.png new file mode 100644 index 0000000..df06d12 Binary files /dev/null and b/assets/change_voice/images/3.0x/upload_record_sound.png differ diff --git a/assets/change_voice/images/3.0x/user_agreement.png b/assets/change_voice/images/3.0x/user_agreement.png new file mode 100644 index 0000000..e9d7853 Binary files /dev/null and b/assets/change_voice/images/3.0x/user_agreement.png differ diff --git a/assets/change_voice/images/3.0x/voice_change.png b/assets/change_voice/images/3.0x/voice_change.png new file mode 100644 index 0000000..67c741d Binary files /dev/null and b/assets/change_voice/images/3.0x/voice_change.png differ diff --git a/assets/change_voice/images/3.0x/voice_default.png b/assets/change_voice/images/3.0x/voice_default.png new file mode 100644 index 0000000..d456712 Binary files /dev/null and b/assets/change_voice/images/3.0x/voice_default.png differ diff --git a/assets/change_voice/images/about.png b/assets/change_voice/images/about.png new file mode 100644 index 0000000..c2b364b Binary files /dev/null and b/assets/change_voice/images/about.png differ diff --git a/assets/change_voice/images/arrow_down_back.png b/assets/change_voice/images/arrow_down_back.png new file mode 100644 index 0000000..501737a Binary files /dev/null and b/assets/change_voice/images/arrow_down_back.png differ diff --git a/assets/change_voice/images/bnb1_selected.png b/assets/change_voice/images/bnb1_selected.png new file mode 100644 index 0000000..03c05e7 Binary files /dev/null and b/assets/change_voice/images/bnb1_selected.png differ diff --git a/assets/change_voice/images/bnb1_unselected.png b/assets/change_voice/images/bnb1_unselected.png new file mode 100644 index 0000000..c76010f Binary files /dev/null and b/assets/change_voice/images/bnb1_unselected.png differ diff --git a/assets/change_voice/images/bnb2_selected.png b/assets/change_voice/images/bnb2_selected.png new file mode 100644 index 0000000..a2d30a5 Binary files /dev/null and b/assets/change_voice/images/bnb2_selected.png differ diff --git a/assets/change_voice/images/bnb2_unselected.png b/assets/change_voice/images/bnb2_unselected.png new file mode 100644 index 0000000..60b4e03 Binary files /dev/null and b/assets/change_voice/images/bnb2_unselected.png differ diff --git a/assets/change_voice/images/bnb3_selected.png b/assets/change_voice/images/bnb3_selected.png new file mode 100644 index 0000000..092cada Binary files /dev/null and b/assets/change_voice/images/bnb3_selected.png differ diff --git a/assets/change_voice/images/bnb3_unselected.png b/assets/change_voice/images/bnb3_unselected.png new file mode 100644 index 0000000..6ef5b18 Binary files /dev/null and b/assets/change_voice/images/bnb3_unselected.png differ diff --git a/assets/change_voice/images/bnb4_selected.png b/assets/change_voice/images/bnb4_selected.png new file mode 100644 index 0000000..824f01e Binary files /dev/null and b/assets/change_voice/images/bnb4_selected.png differ diff --git a/assets/change_voice/images/bnb4_unselected.png b/assets/change_voice/images/bnb4_unselected.png new file mode 100644 index 0000000..fd128f9 Binary files /dev/null and b/assets/change_voice/images/bnb4_unselected.png differ diff --git a/assets/change_voice/images/change_voice_bg.png b/assets/change_voice/images/change_voice_bg.png new file mode 100644 index 0000000..88f3387 Binary files /dev/null and b/assets/change_voice/images/change_voice_bg.png differ diff --git a/assets/change_voice/images/change_voice_item_frame.png b/assets/change_voice/images/change_voice_item_frame.png new file mode 100644 index 0000000..282c870 Binary files /dev/null and b/assets/change_voice/images/change_voice_item_frame.png differ diff --git a/assets/change_voice/images/change_voice_item_selected.png b/assets/change_voice/images/change_voice_item_selected.png new file mode 100644 index 0000000..ddb2128 Binary files /dev/null and b/assets/change_voice/images/change_voice_item_selected.png differ diff --git a/assets/change_voice/images/favorite.png b/assets/change_voice/images/favorite.png new file mode 100644 index 0000000..7c1ea2d Binary files /dev/null and b/assets/change_voice/images/favorite.png differ diff --git a/assets/change_voice/images/home_bg.png b/assets/change_voice/images/home_bg.png new file mode 100644 index 0000000..180bc3d Binary files /dev/null and b/assets/change_voice/images/home_bg.png differ diff --git a/assets/change_voice/images/home_bnb_bg.png b/assets/change_voice/images/home_bnb_bg.png new file mode 100644 index 0000000..891d327 Binary files /dev/null and b/assets/change_voice/images/home_bnb_bg.png differ diff --git a/assets/change_voice/images/icon_chevron_right.png b/assets/change_voice/images/icon_chevron_right.png new file mode 100644 index 0000000..1c3733b Binary files /dev/null and b/assets/change_voice/images/icon_chevron_right.png differ diff --git a/assets/change_voice/images/img_error.png b/assets/change_voice/images/img_error.png new file mode 100644 index 0000000..7ef1e09 Binary files /dev/null and b/assets/change_voice/images/img_error.png differ diff --git a/assets/change_voice/images/img_placeholder.png b/assets/change_voice/images/img_placeholder.png new file mode 100644 index 0000000..f7a5faf Binary files /dev/null and b/assets/change_voice/images/img_placeholder.png differ diff --git a/assets/change_voice/images/more.png b/assets/change_voice/images/more.png new file mode 100644 index 0000000..ac179f4 Binary files /dev/null and b/assets/change_voice/images/more.png differ diff --git a/assets/change_voice/images/not_favorite.png b/assets/change_voice/images/not_favorite.png new file mode 100644 index 0000000..9d4166e Binary files /dev/null and b/assets/change_voice/images/not_favorite.png differ diff --git a/assets/change_voice/images/not_played.png b/assets/change_voice/images/not_played.png new file mode 100644 index 0000000..cba9c9c Binary files /dev/null and b/assets/change_voice/images/not_played.png differ diff --git a/assets/change_voice/images/not_played1.png b/assets/change_voice/images/not_played1.png new file mode 100644 index 0000000..161001a Binary files /dev/null and b/assets/change_voice/images/not_played1.png differ diff --git a/assets/change_voice/images/play_sound_bg.png b/assets/change_voice/images/play_sound_bg.png new file mode 100644 index 0000000..09d5c8b Binary files /dev/null and b/assets/change_voice/images/play_sound_bg.png differ diff --git a/assets/change_voice/images/playing.png b/assets/change_voice/images/playing.png new file mode 100644 index 0000000..fceb125 Binary files /dev/null and b/assets/change_voice/images/playing.png differ diff --git a/assets/change_voice/images/playing1.png b/assets/change_voice/images/playing1.png new file mode 100644 index 0000000..3a7366f Binary files /dev/null and b/assets/change_voice/images/playing1.png differ diff --git a/assets/change_voice/images/privacy.png b/assets/change_voice/images/privacy.png new file mode 100644 index 0000000..387de99 Binary files /dev/null and b/assets/change_voice/images/privacy.png differ diff --git a/assets/change_voice/images/settings_bg.png b/assets/change_voice/images/settings_bg.png new file mode 100644 index 0000000..3719821 Binary files /dev/null and b/assets/change_voice/images/settings_bg.png differ diff --git a/assets/change_voice/images/subtract.png b/assets/change_voice/images/subtract.png new file mode 100644 index 0000000..aaa3f8d Binary files /dev/null and b/assets/change_voice/images/subtract.png differ diff --git a/assets/change_voice/images/the_monster.png b/assets/change_voice/images/the_monster.png new file mode 100644 index 0000000..6bc7897 Binary files /dev/null and b/assets/change_voice/images/the_monster.png differ diff --git a/assets/change_voice/images/the_monster1.png b/assets/change_voice/images/the_monster1.png new file mode 100644 index 0000000..9f11978 Binary files /dev/null and b/assets/change_voice/images/the_monster1.png differ diff --git a/assets/change_voice/images/the_monster2.png b/assets/change_voice/images/the_monster2.png new file mode 100644 index 0000000..a440183 Binary files /dev/null and b/assets/change_voice/images/the_monster2.png differ diff --git a/assets/change_voice/images/upload_method_bg.png b/assets/change_voice/images/upload_method_bg.png new file mode 100644 index 0000000..62d8e3f Binary files /dev/null and b/assets/change_voice/images/upload_method_bg.png differ diff --git a/assets/change_voice/images/upload_pick.png b/assets/change_voice/images/upload_pick.png new file mode 100644 index 0000000..812446e Binary files /dev/null and b/assets/change_voice/images/upload_pick.png differ diff --git a/assets/change_voice/images/upload_record_sound.png b/assets/change_voice/images/upload_record_sound.png new file mode 100644 index 0000000..aef78b4 Binary files /dev/null and b/assets/change_voice/images/upload_record_sound.png differ diff --git a/assets/change_voice/images/user_agreement.png b/assets/change_voice/images/user_agreement.png new file mode 100644 index 0000000..d1e5380 Binary files /dev/null and b/assets/change_voice/images/user_agreement.png differ diff --git a/assets/change_voice/images/voice_change.png b/assets/change_voice/images/voice_change.png new file mode 100644 index 0000000..e62707c Binary files /dev/null and b/assets/change_voice/images/voice_change.png differ diff --git a/assets/change_voice/images/voice_default.png b/assets/change_voice/images/voice_default.png new file mode 100644 index 0000000..b495758 Binary files /dev/null and b/assets/change_voice/images/voice_default.png differ diff --git a/assets/icon/app_icon.jpeg b/assets/icon/app_icon.jpeg new file mode 100755 index 0000000..85d2aeb Binary files /dev/null and b/assets/icon/app_icon.jpeg differ diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.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/ephemeral/ +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..7c56964 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..11dbbce --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,63 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + + target.build_configurations.each do |config| + # You can remove unused permissions here + # for more information: https://github.com/BaseflowIT/flutter-permission-handler/blob/master/permission_handler/ios/Classes/PermissionHandlerEnums.h + # e.g. when you don't need camera permission, just add 'PERMISSION_CAMERA=0' + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + + ## dart: PermissionGroup.microphone + 'PERMISSION_MICROPHONE=1', + + ## dart: PermissionGroup.photos + 'PERMISSION_PHOTOS=1', + + ## dart: PermissionGroup.mediaLibrary + 'PERMISSION_MEDIA_LIBRARY=1', + ] + + end + end +end diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..5754e6e --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,781 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 925BBB4BF91FBC45FBB1A990 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C91796818A214085F4162C91 /* Pods_Runner.framework */; }; + 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 */; }; + A6510AE14FDB018A6A1FC10C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BEB22C16B3B24D2C591559EB /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + 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 = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3D79BFA926F2692E951E423D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 6DC536ACE9EAB665A5771F84 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; 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 = ""; }; + 81A8CE8EC3A57F78C3CD66A5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.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 = ""; }; + 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 = ""; }; + BA731EFA0086ECEDB82472B1 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + BEB22C16B3B24D2C591559EB /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C91796818A214085F4162C91 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D92471E2F24A6FD8A5BA58E3 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + FB8787F438B0AA18A3B0D78F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 919C09B0ABA7AEAA1D338A7F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A6510AE14FDB018A6A1FC10C /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 925BBB4BF91FBC45FBB1A990 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 06B37D8EB177ACB4CB662CE4 /* Pods */ = { + isa = PBXGroup; + children = ( + FB8787F438B0AA18A3B0D78F /* Pods-Runner.debug.xcconfig */, + 81A8CE8EC3A57F78C3CD66A5 /* Pods-Runner.release.xcconfig */, + BA731EFA0086ECEDB82472B1 /* Pods-Runner.profile.xcconfig */, + 3D79BFA926F2692E951E423D /* Pods-RunnerTests.debug.xcconfig */, + 6DC536ACE9EAB665A5771F84 /* Pods-RunnerTests.release.xcconfig */, + D92471E2F24A6FD8A5BA58E3 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 06B37D8EB177ACB4CB662CE4 /* Pods */, + E55198C68B319926C19EBEAD /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + E55198C68B319926C19EBEAD /* Frameworks */ = { + isa = PBXGroup; + children = ( + C91796818A214085F4162C91 /* Pods_Runner.framework */, + BEB22C16B3B24D2C591559EB /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 3328770FD52F6A91D69781E2 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 919C09B0ABA7AEAA1D338A7F /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C542A9B4237B5E1EE18032BC /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + E55B512904DEEE14C1BC5CE8 /* [CP] Embed Pods Frameworks */, + 4500EF8764FAF4DD82D7D320 /* [CP] Copy Pods Resources */, + ); + 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 = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 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 */ + 3328770FD52F6A91D69781E2 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 4500EF8764FAF4DD82D7D320 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + 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"; + }; + C542A9B4237B5E1EE18032BC /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + E55B512904DEEE14C1BC5CE8 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency 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; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + 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 = 12.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; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = UH427LWP22; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tone.music.offline; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ToneSnap; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3D79BFA926F2692E951E423D /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fl.musicPlayer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6DC536ACE9EAB665A5771F84 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fl.musicPlayer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D92471E2F24A6FD8A5BA58E3 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.fl.musicPlayer.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + 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 = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; + 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; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + 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 = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + 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; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = UH427LWP22; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tone.music.offline; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ToneSnap; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution"; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = UH427LWP22; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 12.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tone.music.offline; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = ToneSnap; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 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..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..497328a --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..9074fee --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@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..cabc5c1 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..a1955d4 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..0ddf2ad 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..aa38e17 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..cd48534 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..44c9e75 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..76707ef 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..0ddf2ad 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..3ebe76f 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..4676e9f 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-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 0000000..2e15a2a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 0000000..e2f3503 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 0000000..48aa112 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 0000000..1f01a0b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.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..4676e9f 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..105dd9f 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-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 0000000..0feb5ba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 0000000..f80e815 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.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..bb8e4d9 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..4e61973 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..2374ebc 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..e2bf2d1 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "launch_bg.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "launch_bg@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "launch_bg@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "localizable" : true + } +} 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/Assets.xcassets/LaunchImage.imageset/launch_bg.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_bg.png new file mode 100644 index 0000000..f6c79dd Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_bg.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_bg@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_bg@2x.png new file mode 100644 index 0000000..c57fd06 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_bg@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_bg@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_bg@3x.png new file mode 100644 index 0000000..b00732b Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/launch_bg@3x.png differ diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..6962ebc --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..35e7292 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,55 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + ToneSnap + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + tone_snap + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSMicrophoneUsageDescription + We need to access the microphone to record or select audio files. + NSPhotoLibraryUsageDescription + We need access to the photo library to pick audio files. + NSAppleMusicUsageDescription + We need to access the device media library to select audio files. + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + 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/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/lib/components/base_easyloading.dart b/lib/components/base_easyloading.dart new file mode 100644 index 0000000..7831fb1 --- /dev/null +++ b/lib/components/base_easyloading.dart @@ -0,0 +1,39 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 加载、进度、提示框 + +import 'package:flutter_easyloading/flutter_easyloading.dart'; + +class BaseEasyLoading { + static void initGlobalConfig() { + EasyLoading.instance + // 当loading展示的时候,是否允许用户操作. + ..userInteractions = false + // 点击背景是否关闭. + ..dismissOnTap = false; + } + + static void toast(String? value, {bool? dismissOnTap, bool show = true}) { + EasyLoading.instance.userInteractions = true; + if (show && value != null) { + EasyLoading.showToast( + value, + dismissOnTap: dismissOnTap, + maskType: EasyLoadingMaskType.none, + ); + } + } + + static void loading({ + String? value, + bool? dismissOnTap, + bool show = true, + }) { + EasyLoading.instance.userInteractions = false; + if (show) EasyLoading.show(status: value, dismissOnTap: dismissOnTap); + } + + static void dismiss({bool dismiss = true}) { + if (dismiss) EasyLoading.dismiss(); + } +} diff --git a/lib/components/circular_notch_clipper.dart b/lib/components/circular_notch_clipper.dart new file mode 100644 index 0000000..f84eba6 --- /dev/null +++ b/lib/components/circular_notch_clipper.dart @@ -0,0 +1,32 @@ +// Author: fengshengxiong +// Date: 2024/5/29 +// Description: 圆形缺口裁剪 + +import 'package:flutter/material.dart'; + +class CircularNotchClipper extends CustomClipper { + final double notchRadius; + + CircularNotchClipper({required this.notchRadius}); + + @override + Path getClip(Size size) { + final Path path = Path() + ..lineTo(size.width, 0) + ..lineTo(size.width, size.height - notchRadius - 2.5) + ..arcToPoint( + Offset(size.width - notchRadius - 2.5, size.height), + radius: Radius.circular(notchRadius - 2.5), + clockwise: false, + ) + ..lineTo(0, size.height) + ..lineTo(0, 0) + ..close(); + return path; + } + + @override + bool shouldReclip(CircularNotchClipper oldClipper) { + return oldClipper.notchRadius != notchRadius; + } +} \ No newline at end of file diff --git a/lib/components/dialog/remind_dialog.dart b/lib/components/dialog/remind_dialog.dart new file mode 100644 index 0000000..0f8bbab --- /dev/null +++ b/lib/components/dialog/remind_dialog.dart @@ -0,0 +1,120 @@ +// Author: fengshengxiong +// Date: 2024/5/11 +// Description: 提示框 + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/divider_widget.dart'; + +class RemindDialog extends StatelessWidget { + const RemindDialog({ + super.key, + this.title, + this.content, + this.showCancelBtn = true, + this.cancelText, + this.confirmText, + this.confirmOnTap, + this.popScope = false, + }); + + final String? title; + final String? content; + final bool? showCancelBtn; + final String? cancelText; + final String? confirmText; + final Function()? confirmOnTap; + final bool popScope; + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: popScope, + child: Center( + child: Container( + width: 0.8.sw, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8), + ), + child: IntrinsicHeight( + child: Column( + children: [ + SizedBox(height: 10.h), + Text( + title ?? 'Reminder', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + SizedBox(height: 12.h), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10).w, + child: Text( + content ?? '', + textAlign: TextAlign.center, + style: TextStyle( + color: const Color(0xFF333333), + fontSize: 16.sp, + ), + ), + ), + SizedBox(height: 12.h), + const DividerWidget(), + SizedBox( + height: 52.h, + child: Row( + children: [ + if (showCancelBtn!) ...[ + _optionButton(cancelText ?? 'Cancel', false), + Container( + width: 1.w, + height: double.infinity, + color: const Color(0xFFE5E5E5), + ), + ], + _optionButton(confirmText ?? 'Confirm', true), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _optionButton(String label, bool isConfirm) { + return Expanded( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + Get.back(); + if (isConfirm && confirmOnTap != null) confirmOnTap!(); + }, + child: SizedBox( + height: double.infinity, + child: Center( + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + color: isConfirm ? Colors.black : const Color(0xFF666666), + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/components/dialog/rename_dialog.dart b/lib/components/dialog/rename_dialog.dart new file mode 100644 index 0000000..d21d8ac --- /dev/null +++ b/lib/components/dialog/rename_dialog.dart @@ -0,0 +1,169 @@ +// Author: fengshengxiong +// Date: 2024/5/11 +// Description: 提示框 + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/components/divider_widget.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class RenameDialog extends StatefulWidget { + const RenameDialog({ + super.key, + required this.name, + required this.confirmOnTap, + }); + + final String name; + final Function(String value) confirmOnTap; + + @override + State createState() => RenameDialogState(); +} + +class RenameDialogState extends State { + final _textEditingController = TextEditingController(); + int _cursorPosition = 0; + + @override + void initState() { + super.initState(); + _textEditingController.text = widget.name; + _textEditingController.addListener(_saveCursorPosition); + } + + @override + void dispose() { + _textEditingController.removeListener(_saveCursorPosition); + _textEditingController.dispose(); + super.dispose(); + } + + void _saveCursorPosition() { + _cursorPosition = _textEditingController.selection.base.offset; + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: true, + child: Center( + child: SingleChildScrollView( + primary: true, + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: Container( + width: 0.8.sw, + clipBehavior: Clip.antiAlias, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(8).r, + ), + child: IntrinsicHeight( + child: Column( + children: [ + SizedBox(height: 10.h), + Text( + 'Rename', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + Container( + height: 40.h, + alignment: Alignment.center, + margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 12.h), + decoration: BoxDecoration( + color: const Color(0xFFf9f9fc), + borderRadius: BorderRadius.circular(8).r, + ), + child: FocusScope( + canRequestFocus: true, + child: TextField( + maxLines: 1, + controller: _textEditingController, + textInputAction: TextInputAction.done, + textAlign: TextAlign.start, + keyboardType: TextInputType.text, + style: TextStyle(color: Colors.black, fontSize: 16.sp), + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + counterText: '', + hintText: 'please enter', + hintStyle: TextStyle(color: const Color(0xFF999999), fontSize: 16.sp,), + contentPadding: const EdgeInsets.symmetric(horizontal: 10).w, + isCollapsed: true, + border: InputBorder.none, + ), + onChanged: (text) { + _textEditingController.selection = TextSelection.fromPosition( + TextPosition(offset: _cursorPosition), + ); + }, + ), + ), + ), + const DividerWidget(), + SizedBox( + height: 52.h, + child: Row( + children: [ + _optionButton('Cancel', false), + Container( + width: 1.w, + height: double.infinity, + color: const Color(0xFFE5E5E5), + ), + _optionButton('Confirm', true), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _optionButton(String label, bool isConfirm) { + return Expanded( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + if (isConfirm) { + if (!ObjUtil.isNotEmptyStr(_textEditingController.text)) { + BaseEasyLoading.toast('name cannot be empty'); + return; + } + Get.back(); + widget.confirmOnTap(_textEditingController.text); + } else { + Get.back(); + } + }, + child: SizedBox( + height: double.infinity, + child: Center( + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + color: isConfirm ? Colors.black : const Color(0xFF666666), + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/components/divider_widget.dart b/lib/components/divider_widget.dart new file mode 100644 index 0000000..49c96f4 --- /dev/null +++ b/lib/components/divider_widget.dart @@ -0,0 +1,22 @@ +// Author: fengshengxiong +// Date: 2024/5/9 +// Description: 分割线 + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class DividerWidget extends StatelessWidget { + const DividerWidget({super.key, this.height, this.color}); + + final double? height; + final Color? color; + + @override + Widget build(BuildContext context) { + return Divider( + height: height ?? 1.w, + thickness: height ?? 1.w, + color: color ?? const Color(0xFFE5E5E5), + ); + } +} diff --git a/lib/components/keep_alive_wrapper.dart b/lib/components/keep_alive_wrapper.dart new file mode 100644 index 0000000..f97428e --- /dev/null +++ b/lib/components/keep_alive_wrapper.dart @@ -0,0 +1,30 @@ +// Author: fengshengxiong +// Date: 2024/5/9 +// Description: 保持组件状态 + +import 'package:flutter/material.dart'; + +class KeepAliveWrapper extends StatefulWidget { + const KeepAliveWrapper({ + super.key, + required this.child, + this.keepAlive = true, + }); + + final Widget child; + final bool keepAlive; + + @override + State createState() => _KeepAliveWrapperState(); +} + +class _KeepAliveWrapperState extends State with AutomaticKeepAliveClientMixin { + @override + Widget build(BuildContext context) { + super.build(context); + return widget.child; + } + + @override + bool get wantKeepAlive => widget.keepAlive; +} diff --git a/lib/components/my_custom_indicator.dart b/lib/components/my_custom_indicator.dart new file mode 100644 index 0000000..d1a91b4 --- /dev/null +++ b/lib/components/my_custom_indicator.dart @@ -0,0 +1,59 @@ +// Author: fengshengxiong +// Date: 2024/6/4 +// Description: 自定义TabBar指示器 + +import 'package:flutter/material.dart'; + +class MyCustomIndicator extends Decoration { + final double indWidth; + final double indHeight; + final Color color; + final double radius; + + const MyCustomIndicator({ + this.indWidth = 16.0, + this.indHeight = 4.0, + this.color = Colors.white, + this.radius = 3.5, + }); + + @override + BoxPainter createBoxPainter([VoidCallback? onChanged]) { + return _CustomBoxPainter( + this, onChanged, indWidth, indHeight, color, radius); + } +} + +class _CustomBoxPainter extends BoxPainter { + final MyCustomIndicator decoration; + final double indWidth; + final double indHeight; + final Color color; + final double radius; + + _CustomBoxPainter( + this.decoration, + VoidCallback? onChanged, + this.indWidth, + this.indHeight, + this.color, + this.radius, + ) : super(onChanged); + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + final size = configuration.size!; + final newOffset = Offset( + offset.dx + (size.width - indWidth) / 2, + size.height - indHeight, + ); + final Rect rect = newOffset & Size(indWidth, indHeight); + final Paint paint = Paint(); + paint.color = color; + paint.style = PaintingStyle.fill; + canvas.drawRRect( + RRect.fromRectAndRadius(rect, Radius.circular(radius)), + paint, + ); + } +} diff --git a/lib/components/my_marquee_text.dart b/lib/components/my_marquee_text.dart new file mode 100644 index 0000000..0b42e69 --- /dev/null +++ b/lib/components/my_marquee_text.dart @@ -0,0 +1,24 @@ +// Author: fengshengxiong +// Date: 2024/6/5 +// Description: 跑马灯 + +import 'package:flutter/cupertino.dart'; +import 'package:widget_marquee/widget_marquee.dart'; + +class MyMarqueeText extends StatelessWidget { + const MyMarqueeText({super.key, required this.text, required this.textStyle}); + + final String text; + final TextStyle textStyle; + + @override + Widget build(BuildContext context) { + return Marquee( + delay: const Duration(seconds: 2), + duration: const Duration(seconds: 6), + pause: Duration.zero, + gap: 60, + child: Text(text, style: textStyle), + ); + } +} \ No newline at end of file diff --git a/lib/components/navigation_bar/base_appbar.dart b/lib/components/navigation_bar/base_appbar.dart new file mode 100644 index 0000000..86abdf8 --- /dev/null +++ b/lib/components/navigation_bar/base_appbar.dart @@ -0,0 +1,62 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: BaseAppBar + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +class BaseAppBar extends StatelessWidget implements PreferredSizeWidget { + const BaseAppBar( + this.title, { + super.key, + this.backgroundColor, + this.backWidget, + this.onBackTap, + }); + + final Color? backgroundColor; + final Widget? backWidget; + final String title; + final Function()? onBackTap; + + @override + Widget build(BuildContext context) { + return AppBar( + centerTitle: true, + backgroundColor: backgroundColor, + title: _buildTitle(), + leading: backWidget ?? _buildBackWidget(), + ); + } + + Widget _buildBackWidget() { + return ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onBackTap ?? Get.back, + child: Icon( + Icons.arrow_back_rounded, + size: 32.w, + color: Colors.white, + ), + ), + ), + ); + } + + Widget _buildTitle() { + return Text( + title, + style: TextStyle( + color: Colors.white, + fontSize: 20.sp, + fontWeight: FontWeight.w600, + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} diff --git a/lib/components/private/head_label.dart b/lib/components/private/head_label.dart new file mode 100644 index 0000000..bcab30b --- /dev/null +++ b/lib/components/private/head_label.dart @@ -0,0 +1,34 @@ +// Author: fengshengxiong +// Date: 2024/6/5 +// Description: 头部标签 + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class HeadLabel extends StatelessWidget { + const HeadLabel({ + super.key, + required this.assets, + required this.width, + required this.height, + }); + + final String assets; + final double width; + final double height; + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(20.w, MediaQuery.of(context).padding.top + 24.h, 20.w, 14.h), + child: Align( + alignment: Alignment.topLeft, + child: Image.asset( + assets, + width: width, + height: height, + ), + ), + ); + } +} diff --git a/lib/components/private/my_voice_item.dart b/lib/components/private/my_voice_item.dart new file mode 100644 index 0000000..bf75769 --- /dev/null +++ b/lib/components/private/my_voice_item.dart @@ -0,0 +1,94 @@ +// Author: fengshengxiong +// Date: 2024/6/5 +// Description: 我的音频item + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:tone_snap/components/my_marquee_text.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class MyVoiceItem extends StatelessWidget { + const MyVoiceItem({ + super.key, + required this.item, + required this.onTapItem, + required this.onReName, + required this.onDelete, + }); + + final VoiceModel item; + final Function() onTapItem; + final Function() onReName; + final Function() onDelete; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTapItem, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 8.h), + child: Row( + children: [ + ClipOval( + child: Image.asset( + ObjUtil.isNotEmptyStr(item.cover) ? item.cover! : Assets.imagesVoiceDefault, + width: 52.w, + height: 52.w, + fit: BoxFit.cover, + ), + ), + SizedBox(width: 12.w), + Expanded( + child: MyMarqueeText( + text: item.name, + textStyle: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(width: 12.w), + PopupMenuButton( + offset: Offset(0, 38.w), + color: Colors.white, + padding: EdgeInsets.zero, + itemBuilder: (context) { + return [ + PopupMenuItem( + onTap: onReName, + child: Center( + child: Text( + 'Rename', + style: TextStyle(fontSize: 15.sp, fontWeight: FontWeight.w500), + ), + ), + ), + PopupMenuItem( + onTap: onDelete, + child: Center( + child: Text( + 'Delete', + style: TextStyle(fontSize: 15.sp, fontWeight: FontWeight.w500), + ), + ), + ), + ]; + }, + child: Padding( + padding: const EdgeInsets.all(6).w, + child: Image.asset( + Assets.imagesMore, + width: 32.w, + height: 32.w, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/components/refresh/base_easyrefresh.dart b/lib/components/refresh/base_easyrefresh.dart new file mode 100644 index 0000000..dce1782 --- /dev/null +++ b/lib/components/refresh/base_easyrefresh.dart @@ -0,0 +1,58 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 下拉刷新、上拉加载 + +import 'dart:async'; +import 'package:easy_refresh/easy_refresh.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; + +class BaseEasyRefresh extends StatelessWidget { + const BaseEasyRefresh({ + super.key, + required this.controller, + this.header, + this.footer, + this.onRefresh, + this.onLoad, + this.refreshOnStart = false, + required this.viewState, + required this.child, + this.width, + this.height, + }); + + final EasyRefreshController controller; + final Header? header; + final Footer? footer; + final FutureOr Function()? onRefresh; + final FutureOr Function()? onLoad; + final bool refreshOnStart; + final ViewState viewState; + final Widget child; + final double? width; + final double? height; + + @override + Widget build(BuildContext context) { + return EasyRefresh( + controller: controller, + header: header ?? const ClassicHeader(), + footer: footer ?? const ClassicFooter(), + onRefresh: onRefresh, + onLoad: onLoad, + refreshOnStart: refreshOnStart, + child: viewState == ViewState.normal ? child : SingleChildScrollView( + child: SizedBox( + width: width ?? 1.sw, + height: height ?? 300.h, + child: ViewStateWidget( + viewState: viewState, + child: child, + ), + ), + ), + ); + } +} diff --git a/lib/components/round_rect_slider_thumb_shape.dart b/lib/components/round_rect_slider_thumb_shape.dart new file mode 100644 index 0000000..97a57e2 --- /dev/null +++ b/lib/components/round_rect_slider_thumb_shape.dart @@ -0,0 +1,55 @@ +// Author: fengshengxiong +// Date: 2024/6/2 +// Description: 自定义RoundRectSliderThumbShape + +import 'package:flutter/material.dart'; + +class RoundRectSliderThumbShape extends SliderComponentShape { + final double thumbHeight; + final double thumbWidth; + final double thumbRadius; + final Color thumbColor; + + const RoundRectSliderThumbShape({ + this.thumbHeight = 20.0, + this.thumbWidth = 20.0, + this.thumbRadius = 6.0, + this.thumbColor = Colors.black, + }); + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return Size(thumbWidth, thumbHeight); + } + + @override + void paint(PaintingContext context, Offset center, + {required Animation activationAnimation, + required Animation enableAnimation, + required bool isDiscrete, + required TextPainter labelPainter, + required RenderBox parentBox, + required SliderThemeData sliderTheme, + required TextDirection textDirection, + required double value, + required double textScaleFactor, + required Size sizeWithOverflow}) { + + final Paint thumbPaint = Paint() + ..color = thumbColor + ..style = PaintingStyle.fill; + + final Rect thumbRect = Rect.fromCenter( + center: center, + width: thumbWidth, + height: thumbHeight, + ); + + final RRect thumbRRect = RRect.fromRectAndRadius( + thumbRect, + Radius.circular(thumbRadius), + ); + + context.canvas.drawRRect(thumbRRect, thumbPaint); + } +} diff --git a/lib/components/round_rect_slider_track_shape.dart b/lib/components/round_rect_slider_track_shape.dart new file mode 100644 index 0000000..510eef7 --- /dev/null +++ b/lib/components/round_rect_slider_track_shape.dart @@ -0,0 +1,79 @@ +// Author: fengshengxiong +// Date: 2024/6/2 +// Description: 自定义RoundedRectSliderTrackShape + +import 'package:flutter/material.dart'; + +class RoundRectSliderTrackShape extends SliderTrackShape { + final double trackHeight; + final double trackRadius; + + const RoundRectSliderTrackShape({ + this.trackHeight = 4.0, + this.trackRadius = 8.0, + }); + + @override + void paint(PaintingContext context, Offset offset, + {required RenderBox parentBox, + required SliderThemeData sliderTheme, + required Animation enableAnimation, + required Offset thumbCenter, + Offset? secondaryOffset, + bool isEnabled = false, + bool isDiscrete = false, + required TextDirection textDirection}) { + if (sliderTheme.trackHeight == 0) return; + + final Rect trackRect = getPreferredRect( + parentBox: parentBox, + offset: offset, + sliderTheme: sliderTheme, + isEnabled: isEnabled, + isDiscrete: isDiscrete, + ); + + final Paint leftTrackPaint = Paint() + ..color = sliderTheme.activeTrackColor! + ..style = PaintingStyle.fill; + + final Paint rightTrackPaint = Paint() + ..color = sliderTheme.inactiveTrackColor! + ..style = PaintingStyle.fill; + + final RRect leftTrackSegment = RRect.fromLTRBR( + trackRect.left, + trackRect.top, + thumbCenter.dx, + trackRect.bottom, + Radius.circular(trackRadius), + ); + + final RRect rightTrackSegment = RRect.fromLTRBR( + thumbCenter.dx, + trackRect.top, + trackRect.right, + trackRect.bottom, + Radius.circular(trackRadius), + ); + + context.canvas.drawRRect(leftTrackSegment, leftTrackPaint); + context.canvas.drawRRect(rightTrackSegment, rightTrackPaint); + } + + @override + Rect getPreferredRect({ + required RenderBox parentBox, + Offset offset = Offset.zero, + required SliderThemeData sliderTheme, + bool isEnabled = false, + bool isDiscrete = false, + }) { + final double trackHeight = this.trackHeight; + final double trackLeft = offset.dx; + final double trackTop = + offset.dy + (parentBox.size.height - trackHeight) / 2; + final double trackWidth = parentBox.size.width; + return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight); + } +} diff --git a/lib/components/view_state_widget.dart b/lib/components/view_state_widget.dart new file mode 100644 index 0000000..06a492a --- /dev/null +++ b/lib/components/view_state_widget.dart @@ -0,0 +1,79 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 状态视图 + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +/// 四种视图状态 +enum ViewState { normal, error, loading, empty } + +class ViewStateWidget extends StatelessWidget { + const ViewStateWidget({ + super.key, + required this.viewState, + required this.child, + }); + + final ViewState viewState; + final Widget child; + + @override + Widget build(BuildContext context) { + switch (viewState) { + case ViewState.normal: + return child; + case ViewState.loading: + return loadingView(); + case ViewState.empty: + return emptyView(); + case ViewState.error: + return errorView(); + } + } +} + +/// 加载中视图 +Widget loadingView({ + Color? color, + Animation? valueColor, + Color? backgroundColor, + double? value, +}) { + return Center( + child: CircularProgressIndicator( + color: color, + valueColor: valueColor, + backgroundColor: backgroundColor ?? Colors.white, + value: value, + ), + ); +} + +/// 空视图 +Widget emptyView({String? msg, Color? textColor}) { + return Center( + child: Text( + msg ?? 'No data', + textAlign: TextAlign.center, + style: TextStyle( + color: textColor ?? Colors.white, + fontSize: 15.sp, + ), + ), + ); +} + +/// 错误视图 +Widget errorView({String? msg, Color? textColor}) { + return Center( + child: Text( + msg ?? 'An error occurred, please try again later', + textAlign: TextAlign.center, + style: TextStyle( + color: textColor ?? Colors.white, + fontSize: 15.sp, + ), + ), + ); +} diff --git a/lib/controllers/player_controller.dart b/lib/controllers/player_controller.dart new file mode 100644 index 0000000..788371d --- /dev/null +++ b/lib/controllers/player_controller.dart @@ -0,0 +1,182 @@ +// Author: fengshengxiong +// Date: 2024/5/30 +// Description: 播放器控制器 + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:just_audio/just_audio.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/utils/audio_util.dart'; +import 'package:tone_snap/utils/log_print.dart'; + +class PlayerController extends GetxController { + static PlayerController get to => Get.put(PlayerController()); + + StreamSubscription? _playerStateSubscription; + StreamSubscription? _durationSubscription; + + final _player = AudioPlayer(); + var filePath = ''; + var isPlaying = false.obs; + var isCompleted = true.obs; + var positionValue = 0.0.obs; + var duration = const Duration().obs; + var positionDuration = const Duration().obs; + var isReady = false; + + @override + void onInit() { + super.onInit(); + AudioUtil.configAudioSession(); + } + + @override + void onClose() { + _stopListening(); + _player.dispose(); + super.onClose(); + } + + Future setFilePath(String filePath) async { + if (!filePath.contains('assets') && !await _fileExists(filePath)) return; + if (this.filePath != filePath) { + this.filePath = filePath; + try { + isReady = false; + if (filePath.contains('assets')) { + duration.value = await _player.setAsset(filePath) ?? Duration.zero; + } else { + duration.value = await _player.setFilePath(filePath) ?? Duration.zero; + } + isReady = true; + } on PlayerException catch (e) { + LogPrint.e("Error code: ${e.code}"); + LogPrint.e("Error message: ${e.message}"); + BaseEasyLoading.toast("Error message: ${e.message}"); + } on PlayerInterruptedException catch (e) { + LogPrint.e("Connection aborted: ${e.message}"); + BaseEasyLoading.toast("Connection aborted: ${e.message}"); + } catch (e) { + LogPrint.e('An error occured: $e'); + BaseEasyLoading.toast('An error occured: $e'); + } + } + } + + /// 监听 + void _startListening() { + _playerStateSubscription = _player.playerStateStream.listen((playerState) { + switch (playerState.processingState) { + case ProcessingState.idle: + break; + case ProcessingState.loading: + break; + case ProcessingState.buffering: + break; + case ProcessingState.ready: + break; + case ProcessingState.completed: + isCompleted.value = true; + _player.seek(Duration.zero); + pausePlay(); + break; + } + }); + + _durationSubscription = _player.positionStream.listen((position) { + if (_player.duration != null) { + duration.value = _player.duration!; + positionDuration.value = position; + positionValue.value = position.inMilliseconds.toDouble() / _player.duration!.inMilliseconds.toDouble(); + } + }, onError: (Object e, StackTrace st) { + if (e is PlatformException) { + LogPrint.e('Error code: ${e.code}'); + LogPrint.e('Error message: ${e.message}'); + LogPrint.e('AudioSource index: ${e.details?['index']}'); + BaseEasyLoading.toast('Error message: ${e.message}'); + } else { + LogPrint.e('An error occurred: $e'); + BaseEasyLoading.toast('An error occurred: $e'); + } + }); + } + + void _stopListening() { + _playerStateSubscription?.cancel(); + _playerStateSubscription = null; + _durationSubscription?.cancel(); + _durationSubscription = null; + } + + /// 开始播放 + Future startPlay() async { + if (!filePath.contains('assets') && !await _fileExists(filePath)) return; + if (!isReady) { + BaseEasyLoading.toast('Loading audio source'); + return; + } + _startListening(); + _player.play(); + isPlaying.value = true; + isCompleted.value = false; + } + + /// 暂停播放 + Future pausePlay() async { + await _player.pause(); + isPlaying.value = false; + _stopListening(); + } + + /// 停止播放并释放资源 + Future stopPlay() async { + await _player.stop(); + _stopListening(); + _player.seek(Duration.zero); + isPlaying.value = false; + isCompleted.value = true; + positionValue.value = 0.0; + duration.value = Duration.zero; + positionDuration.value = Duration.zero; + } + + String getDuration() { + return printDuration(duration.value); + } + + String getPositionDuration() { + return printDuration(positionDuration.value); + } + + /// 打印持续时间 + String printDuration(Duration duration) { + String twoDigits(int n) => n.toString().padLeft(2, '0'); + final minutes = twoDigits(duration.inMinutes.remainder(60)); + final seconds = twoDigits(duration.inSeconds.remainder(60)); + return '$minutes:$seconds'; + } + + /// 判断文件是否存在 + Future _fileExists(String path) async { + if (await File(path).exists()) { + return true; + } else { + BaseEasyLoading.toast('Audio does not exist'); + return false; + } + } + + /// 设置声调 + Future setPitch(double setPitch) async { + await _player.setPitch(setPitch); + } + + /// 设置声速 + Future setSpeed(double speed) async { + await _player.setSpeed(speed); + } +} diff --git a/lib/data/models/voice_model.dart b/lib/data/models/voice_model.dart new file mode 100644 index 0000000..0fd57c5 --- /dev/null +++ b/lib/data/models/voice_model.dart @@ -0,0 +1,54 @@ +// Author: fengshengxiong +// Date: 2024/6/4 +// Description: 音频实体类 + +import 'dart:convert'; + +import 'package:hive/hive.dart'; + +part 'voice_model.g.dart'; + +VoiceModel voiceModelFromJson(String str) => VoiceModel.fromJson(json.decode(str)); + +String voiceModelToJson(VoiceModel data) => json.encode(data.toJson()); + +@HiveType(typeId: 0) +class VoiceModel extends HiveObject { + @HiveField(0) + String name; + + @HiveField(1) + String path; + + @HiveField(2) + String? cover; + + VoiceModel({ + required this.name, + required this.path, + this.cover, + }); + + VoiceModel copyWith({ + String? name, + String? path, + String? cover, + }) => + VoiceModel( + name: name ?? this.name, + path: path ?? this.path, + cover: cover ?? this.cover, + ); + + factory VoiceModel.fromJson(Map json) => VoiceModel( + name: json["name"], + path: json["path"], + cover: json["cover"], + ); + + Map toJson() => { + "name": name, + "path": path, + "cover": cover, + }; +} diff --git a/lib/data/models/voice_model.g.dart b/lib/data/models/voice_model.g.dart new file mode 100644 index 0000000..d0ba131 --- /dev/null +++ b/lib/data/models/voice_model.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'voice_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class VoiceModelAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + VoiceModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return VoiceModel( + name: fields[0] as String, + path: fields[1] as String, + cover: fields[2] as String?, + ); + } + + @override + void write(BinaryWriter writer, VoiceModel obj) { + writer + ..writeByte(3) + ..writeByte(0) + ..write(obj.name) + ..writeByte(1) + ..write(obj.path) + ..writeByte(2) + ..write(obj.cover); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VoiceModelAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/data/storage/favorite_data.dart b/lib/data/storage/favorite_data.dart new file mode 100644 index 0000000..d1b062c --- /dev/null +++ b/lib/data/storage/favorite_data.dart @@ -0,0 +1,45 @@ +// Author: fengshengxiong +// Date: 2024/5/8 +// Description: 收藏数据 + +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/data/storage/hive_storage.dart'; + +class FavoriteData { + /// 私有构造函数 + FavoriteData._(); + + /// 静态常量用于保存类的唯一实例 + static final FavoriteData _instance = FavoriteData._(); + + /// 工厂构造函数返回类的唯一实例 + factory FavoriteData() { + return _instance; + } + + /// 声明盒子 + /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 + final _box = getFavoriteBox(); + + /// 获取数据 + List getList() { + return _box.values.toList(); + } + + /// 添加数据 + Future addData(VoiceModel voiceModel) async { + return await _box.add(voiceModel); + } + + /// 删除 + Future delete(int index) async { + await _box.deleteAt(index); + await _box.flush(); + } + + /// 清空所有数据 + Future clear() async { + await _box.clear(); + await _box.flush(); + } +} diff --git a/lib/data/storage/hive_storage.dart b/lib/data/storage/hive_storage.dart new file mode 100644 index 0000000..7e4e530 --- /dev/null +++ b/lib/data/storage/hive_storage.dart @@ -0,0 +1,27 @@ +// Author: fengshengxiong +// Date: 2024/5/8 +// Description: 持久化储存 + +import 'package:hive_flutter/hive_flutter.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; + +const myVoiceBox = 'myVoiceBox'; +const favoriteBox = 'favoriteBox'; + +Future initHive() async { + // 初始化 + await Hive.initFlutter(); + // 注册类型适配器 + Hive.registerAdapter(VoiceModelAdapter()); + // 打开盒子 + await Hive.openBox(myVoiceBox); + await Hive.openBox(favoriteBox); +} + +Box getMyVoiceBox() { + return Hive.box(myVoiceBox); +} + +Box getFavoriteBox() { + return Hive.box(favoriteBox); +} \ No newline at end of file diff --git a/lib/data/storage/my_voice_data.dart b/lib/data/storage/my_voice_data.dart new file mode 100644 index 0000000..3157d08 --- /dev/null +++ b/lib/data/storage/my_voice_data.dart @@ -0,0 +1,45 @@ +// Author: fengshengxiong +// Date: 2024/5/8 +// Description: 收藏数据 + +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/data/storage/hive_storage.dart'; + +class MyVoiceData { + /// 私有构造函数 + MyVoiceData._(); + + /// 静态常量用于保存类的唯一实例 + static final MyVoiceData _instance = MyVoiceData._(); + + /// 工厂构造函数返回类的唯一实例 + factory MyVoiceData() { + return _instance; + } + + /// 声明盒子 + /// 注意, main函数中这个盒子已经打开, 可以进行存储操作 + final _box = getMyVoiceBox(); + + /// 获取数据 + List getList() { + return _box.values.toList(); + } + + /// 添加数据 + Future addData(VoiceModel voiceModel) async { + return await _box.add(voiceModel); + } + + /// 删除 + Future delete(index) async { + await _box.deleteAt(index); + await _box.flush(); + } + + /// 清空所有数据 + Future clear() async { + await _box.clear(); + await _box.flush(); + } +} diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart new file mode 100644 index 0000000..f235a1e --- /dev/null +++ b/lib/generated/assets.dart @@ -0,0 +1,65 @@ +///This file is automatically generated. DO NOT EDIT, all your changes would be lost. +class Assets { + Assets._(); + + static const String audioBoy = 'assets/change_voice/audio/boy.mp3'; + static const String audioChild = 'assets/change_voice/audio/child.mp3'; + static const String audioElder = 'assets/change_voice/audio/elder.mp3'; + static const String audioGirl = 'assets/change_voice/audio/girl.mp3'; + static const String audioMan = 'assets/change_voice/audio/man.mp3'; + static const String audioMonsters = 'assets/change_voice/audio/monsters.mp3'; + static const String audioRapper = 'assets/change_voice/audio/rapper.mp3'; + static const String audioRobot = 'assets/change_voice/audio/robot.wav'; + static const String audioSexy = 'assets/change_voice/audio/sexy.mp3'; + static const String audioWoman = 'assets/change_voice/audio/woman.mp3'; + static const String changeVoiceAudioBoy = 'assets/change_voice/audio/boy.jpeg'; + static const String changeVoiceAudioChild = 'assets/change_voice/audio/child.png'; + static const String changeVoiceAudioElder = 'assets/change_voice/audio/elder.png'; + static const String changeVoiceAudioGirl = 'assets/change_voice/audio/girl.jpeg'; + static const String changeVoiceAudioMan = 'assets/change_voice/audio/man.png'; + static const String changeVoiceAudioMonsters = 'assets/change_voice/audio/monsters.png'; + static const String changeVoiceAudioRapper = 'assets/change_voice/audio/rapper.jpeg'; + static const String changeVoiceAudioRobot = 'assets/change_voice/audio/robot.jpeg'; + static const String changeVoiceAudioSexy = 'assets/change_voice/audio/sexy.jpeg'; + static const String changeVoiceAudioWoman = 'assets/change_voice/audio/woman.png'; + static const String iconAppIcon = 'assets/icon/app_icon.jpeg'; + static const String imagesAbout = 'assets/change_voice/images/about.png'; + static const String imagesArrowDownBack = 'assets/change_voice/images/arrow_down_back.png'; + static const String imagesBnb1Selected = 'assets/change_voice/images/bnb1_selected.png'; + static const String imagesBnb1Unselected = 'assets/change_voice/images/bnb1_unselected.png'; + static const String imagesBnb2Selected = 'assets/change_voice/images/bnb2_selected.png'; + static const String imagesBnb2Unselected = 'assets/change_voice/images/bnb2_unselected.png'; + static const String imagesBnb3Selected = 'assets/change_voice/images/bnb3_selected.png'; + static const String imagesBnb3Unselected = 'assets/change_voice/images/bnb3_unselected.png'; + static const String imagesBnb4Selected = 'assets/change_voice/images/bnb4_selected.png'; + static const String imagesBnb4Unselected = 'assets/change_voice/images/bnb4_unselected.png'; + static const String imagesChangeVoiceBg = 'assets/change_voice/images/change_voice_bg.png'; + static const String imagesChangeVoiceItemFrame = 'assets/change_voice/images/change_voice_item_frame.png'; + static const String imagesChangeVoiceItemSelected = 'assets/change_voice/images/change_voice_item_selected.png'; + static const String imagesFavorite = 'assets/change_voice/images/favorite.png'; + static const String imagesHomeBg = 'assets/change_voice/images/home_bg.png'; + static const String imagesHomeBnbBg = 'assets/change_voice/images/home_bnb_bg.png'; + static const String imagesIconChevronRight = 'assets/change_voice/images/icon_chevron_right.png'; + static const String imagesImgError = 'assets/change_voice/images/img_error.png'; + static const String imagesImgPlaceholder = 'assets/change_voice/images/img_placeholder.png'; + static const String imagesMore = 'assets/change_voice/images/more.png'; + static const String imagesNotFavorite = 'assets/change_voice/images/not_favorite.png'; + static const String imagesNotPlayed = 'assets/change_voice/images/not_played.png'; + static const String imagesNotPlayed1 = 'assets/change_voice/images/not_played1.png'; + static const String imagesPlaySoundBg = 'assets/change_voice/images/play_sound_bg.png'; + static const String imagesPlaying = 'assets/change_voice/images/playing.png'; + static const String imagesPlaying1 = 'assets/change_voice/images/playing1.png'; + static const String imagesPrivacy = 'assets/change_voice/images/privacy.png'; + static const String imagesSettingsBg = 'assets/change_voice/images/settings_bg.png'; + static const String imagesSubtract = 'assets/change_voice/images/subtract.png'; + static const String imagesTheMonster = 'assets/change_voice/images/the_monster.png'; + static const String imagesTheMonster1 = 'assets/change_voice/images/the_monster1.png'; + static const String imagesTheMonster2 = 'assets/change_voice/images/the_monster2.png'; + static const String imagesUploadMethodBg = 'assets/change_voice/images/upload_method_bg.png'; + static const String imagesUploadPick = 'assets/change_voice/images/upload_pick.png'; + static const String imagesUploadRecordSound = 'assets/change_voice/images/upload_record_sound.png'; + static const String imagesUserAgreement = 'assets/change_voice/images/user_agreement.png'; + static const String imagesVoiceChange = 'assets/change_voice/images/voice_change.png'; + static const String imagesVoiceDefault = 'assets/change_voice/images/voice_default.png'; + +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..4450d82 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,76 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/data/storage/hive_storage.dart'; +import 'package:tone_snap/res/themes/app_themes.dart'; +import 'package:tone_snap/res/values/strings.dart'; +import 'package:tone_snap/routes/app_pages.dart'; +import 'package:tone_snap/routes/app_routes.dart'; +import 'package:tone_snap/utils/file_util.dart'; +import 'package:tone_snap/utils/local_path_util.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // 初始化Hive + await initHive(); + + // 竖屏 + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + + runApp(const MyApp()); + + if (Platform.isAndroid) { + SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + systemNavigationBarColor: Colors.black, + systemNavigationBarIconBrightness: Brightness.light, + )); + } + + // 删除缓存文件 + FileUtil.deleteAllFilesInDirectory(await LocalPathUtil.getRecordingsDir()); + FileUtil.deleteAllFilesInDirectory(await LocalPathUtil.getAssetsDir()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + final easyLoading = EasyLoading.init(); + return ScreenUtilInit( + // 以设计稿的尺寸为基准进行适配 + designSize: const Size(375, 812), + minTextAdapt: true, + builder: (context, child) { + return GetMaterialApp( + title: appName, + debugShowCheckedModeBanner: false, + theme: appTheme, + darkTheme: appTheme, + themeMode: ThemeMode.light, + getPages: AppPages.routes, + initialRoute: AppRoutes.initial, + builder: (context, widget) { + BaseEasyLoading.initGlobalConfig(); + widget = easyLoading(context, widget); + return MediaQuery( + // 设置文字大小不随系统设置改变 + data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling), + child: widget, + ); + }, + ); + }, + ); + } +} diff --git a/lib/modules/splash/splash_binding.dart b/lib/modules/splash/splash_binding.dart new file mode 100644 index 0000000..d89f924 --- /dev/null +++ b/lib/modules/splash/splash_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/splash/splash_controller.dart'; + +class SplashBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SplashController()); + } +} diff --git a/lib/modules/splash/splash_controller.dart b/lib/modules/splash/splash_controller.dart new file mode 100644 index 0000000..bb7e30e --- /dev/null +++ b/lib/modules/splash/splash_controller.dart @@ -0,0 +1,43 @@ +import 'dart:async'; +import 'package:get/get.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class SplashController extends GetxController { + Timer? _timer; + int _timeCount = 1; + + @override + void onInit() { + super.onInit(); + _startTimer(); + } + + @override + void onClose() { + _stopTimer(); + super.onClose(); + } + + /// 开始定时器 + void _startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { + if (_timeCount <= 0) { + _openInitial(); + return; + } + _timeCount--; + }); + } + + /// 停止定时器 + void _stopTimer() { + _timer?.cancel(); + _timer = null; + } + + /// 打开初始页面 + void _openInitial() { + _stopTimer(); + Get.offNamed(AppRoutes.initial); + } +} diff --git a/lib/modules/splash/splash_view.dart b/lib/modules/splash/splash_view.dart new file mode 100644 index 0000000..cf491e7 --- /dev/null +++ b/lib/modules/splash/splash_view.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/splash/splash_controller.dart'; +import 'package:tone_snap/res/values/strings.dart'; + +class SplashView extends StatelessWidget { + SplashView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + Get.find(); + return Scaffold( + body: Stack( + children: [ + _buildIconName(), + _buildProgress(), + ], + ), + ); + } + + Widget _buildIconName() { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + Assets.iconAppIcon, + width: 96.w, + height: 96.w, + ), + SizedBox(height: 20.h), + Text( + appName, + style: TextStyle( + color: Colors.white, + fontSize: 22.sp, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + Widget _buildProgress() { + return Container( + alignment: Alignment.center, + margin: const EdgeInsets.only(bottom: 60).h, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + width: 0.5.sw, + child: LinearProgressIndicator( + backgroundColor: Colors.white, + valueColor: const AlwaysStoppedAnimation(Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + ), + SizedBox(height: 14.h), + Text( + 'Resource Loading...', + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/voice/about/about_binding.dart b/lib/modules/voice/about/about_binding.dart new file mode 100644 index 0000000..52fdf9c --- /dev/null +++ b/lib/modules/voice/about/about_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'about_controller.dart'; + +class AboutBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => AboutController()); + } +} diff --git a/lib/modules/voice/about/about_controller.dart b/lib/modules/voice/about/about_controller.dart new file mode 100644 index 0000000..a939f51 --- /dev/null +++ b/lib/modules/voice/about/about_controller.dart @@ -0,0 +1,18 @@ +import 'package:get/get.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class AboutController extends GetxController { + var versionName = ''.obs; + + @override + void onReady() { + super.onReady(); + _getVersion(); + } + + /// 获取版本号 + void _getVersion() async { + final packageInfo = await PackageInfo.fromPlatform(); + versionName.value = 'App Version:${packageInfo.version}'; + } +} diff --git a/lib/modules/voice/about/about_view.dart b/lib/modules/voice/about/about_view.dart new file mode 100644 index 0000000..af37a10 --- /dev/null +++ b/lib/modules/voice/about/about_view.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/navigation_bar/base_appbar.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/voice/about/about_controller.dart'; +import 'package:tone_snap/res/values/strings.dart'; + +class AboutView extends StatelessWidget { + AboutView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const BaseAppBar('About'), + body: Container( + alignment: Alignment.topCenter, + child: Column( + children: [ + SizedBox(height: 60.h), + Image.asset( + Assets.iconAppIcon, + width: 120.w, + height: 120.w, + ), + SizedBox(height: 20.h), + Obx(() { + return RichText( + maxLines: 2, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + text: TextSpan( + text: appName, + style: TextStyle( + color: Colors.white, + fontSize: 22.sp, + fontWeight: FontWeight.w600, + ), + children: [ + TextSpan( + text: '\n${controller.versionName.value}', + style: TextStyle( + color: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.w600, + ), + ), + WidgetSpan( + child: SizedBox(height: 20.h), + ), + ], + ), + ); + }), + ], + ), + ), + ); + } +} diff --git a/lib/modules/voice/change_voice/change_voice_binding.dart b/lib/modules/voice/change_voice/change_voice_binding.dart new file mode 100644 index 0000000..137acc7 --- /dev/null +++ b/lib/modules/voice/change_voice/change_voice_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/voice/change_voice/change_voice_controller.dart'; + +class ChangeVoiceBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => ChangeVoiceController()); + } +} diff --git a/lib/modules/voice/change_voice/change_voice_controller.dart b/lib/modules/voice/change_voice/change_voice_controller.dart new file mode 100644 index 0000000..dfe3cfc --- /dev/null +++ b/lib/modules/voice/change_voice/change_voice_controller.dart @@ -0,0 +1,155 @@ +import 'package:ffmpeg_kit_flutter_audio/ffmpeg_kit.dart'; +import 'package:ffmpeg_kit_flutter_audio/ffmpeg_session.dart'; +import 'package:ffmpeg_kit_flutter_audio/return_code.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/controllers/player_controller.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/data/storage/my_voice_data.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/voice/initial/initial_controller.dart'; +import 'package:tone_snap/routes/app_routes.dart'; +import 'package:tone_snap/utils/date_util.dart'; +import 'package:tone_snap/utils/file_util.dart'; +import 'package:tone_snap/utils/local_path_util.dart'; +import 'package:tone_snap/utils/log_print.dart'; +import 'package:tone_snap/utils/num_util.dart'; + +class ChangeVoiceController extends GetxController { + var timberList = [ + Timber('Child', Assets.changeVoiceAudioChild, 1.3, 1.1), + Timber('Boy', Assets.changeVoiceAudioBoy, 1.2, 1.1), + Timber('Man', Assets.changeVoiceAudioMan, 0.9, 1.1), + Timber('Elder', Assets.changeVoiceAudioElder, 0.8, 1.1), + // Timber('Rapper', Assets.changeVoiceAudioRapper, 1.0, 1.0), + Timber('Robot', Assets.changeVoiceAudioRobot, 1.0, 1.5), + Timber('Girl', Assets.changeVoiceAudioGirl, 1.5, 1.1), + Timber('Sexy', Assets.changeVoiceAudioSexy, 0.85, 1.1), + Timber('Woman', Assets.changeVoiceAudioWoman, 1.1, 1.1), + Timber('Monsters', Assets.changeVoiceAudioMonsters, 0.7, 1.1), + ].obs; + final playerController = PlayerController.to; + var toneValue = 1.0.obs; + var soundSpeedValue = 1.0.obs; + late String filePath; + + @override + void onInit() { + super.onInit(); + filePath = Get.arguments; + playerController.setFilePath(filePath); + } + + @override + void onClose() { + playerController.setSpeed(1.0); + playerController.setPitch(1.0); + playerController.stopPlay(); + super.onClose(); + } + + void onTapItem(Timber item, int index) { + for (var e in timberList) { + e == item ? item.check = !item.check : e.check = false; + } + toneValue.value = item.check ? item.tone : 1.0; + soundSpeedValue.value = item.check ? item.soundSpeed : 1.0; + playerController.setPitch(toneValue.value); + playerController.setSpeed(soundSpeedValue.value); + timberList.refresh(); + } + + /// 开始/暂停播放 + Future togglePlayback() async { + if (playerController.isPlaying.value) { + await playerController.pausePlay(); + } else { + await playerController.startPlay(); + } + } + + void onChanged(double value, int i) { + var item = timberList.firstWhereOrNull((e) => e.check); + if (i == 0) { + toneValue.value = value; + playerController.setPitch(toneValue.value); + if (item?.tone.toString() != NumUtil.formatNum(value)) { + item?.check = false; + timberList.refresh(); + } + } else { + soundSpeedValue.value = value; + playerController.setSpeed(soundSpeedValue.value); + if (item?.soundSpeed.toString() != NumUtil.formatNum(value)) { + item?.check = false; + timberList.refresh(); + } + } + } + + /// 保存 + Future save() async { + // 停止播放 + if (playerController.isPlaying.value) playerController.stopPlay(); + BaseEasyLoading.loading(); + + try { + // 若是assets路径,转换为文件路径 + if (filePath.contains('assets')) { + filePath = await FileUtil.getAssetsToFilePath(filePath); + } + + // 输出目录 + final outputDir = await LocalPathUtil.getVoiceChangeOutputDir(); + String fileName = '${DateUtil.getNowTimeStr().replaceAll(' ', '_')}_output.mp3'; + String outputPath = '${outputDir.path}/$fileName'; + + var timber = timberList.firstWhereOrNull((e) => e.check); + var filter = ""; + if (timber != null) { + int index = timberList.indexOf(timber); + // Rapper + // filter = ",aecho=0.8:0.88:60:0.4,areverb=50:50:100:100:0.5:0.5"; + + filter = index == 4 ? ",afftdn=nf=-30" : ",aresample=44100"; + } + + // 构建 FFmpeg 命令 + final String command = '-i $filePath -af "asetrate=44100*${toneValue.value},atempo=${soundSpeedValue.value}$filter" $outputPath'; + + // 执行 FFmpeg 命令 + FFmpegSession session = await FFmpegKit.execute(command); + + // 检查执行结果 + ReturnCode? returnCode = await session.getReturnCode(); + if (ReturnCode.isSuccess(returnCode)) { + LogPrint.d('Audio processing successful'); + try { + await MyVoiceData().addData(VoiceModel(name: fileName, path: outputPath)); + BaseEasyLoading.toast('Save successful'); + + // 回到首页-我的页面 + Get.until((route) => route.settings.name == AppRoutes.initial); + InitialController.to.onBottomAppBarItemChanged(2); + } catch (e) { + BaseEasyLoading.toast('Save failed'); + } + } else { + LogPrint.d('Audio processing failed'); + BaseEasyLoading.toast('Audio processing failed'); + } + } catch (e) { + BaseEasyLoading.toast('Audio processing failed'); + } + } +} + +class Timber { + late final String name; + late final String cover; + late final double tone; + late final double soundSpeed; + bool check; + + Timber(this.name, this.cover, this.tone, this.soundSpeed, {this.check = false}); +} \ No newline at end of file diff --git a/lib/modules/voice/change_voice/change_voice_view.dart b/lib/modules/voice/change_voice/change_voice_view.dart new file mode 100644 index 0000000..847f831 --- /dev/null +++ b/lib/modules/voice/change_voice/change_voice_view.dart @@ -0,0 +1,318 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/my_marquee_text.dart'; +import 'package:tone_snap/components/navigation_bar/base_appbar.dart'; +import 'package:tone_snap/components/round_rect_slider_thumb_shape.dart'; +import 'package:tone_snap/components/round_rect_slider_track_shape.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/voice/change_voice/change_voice_controller.dart'; +import 'package:tone_snap/utils/num_util.dart'; + +class ChangeVoiceView extends StatelessWidget { + ChangeVoiceView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Image.asset( + Assets.imagesChangeVoiceBg, + width: 1.sw, + height: 1.sh, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + appBar: const BaseAppBar('Change voice', backgroundColor: Colors.transparent), + body: Column( + children: [ + Expanded( + child: _buildTimber(), + ), + SizedBox(height: 20.h), + _buildSlider(context, 0), + SizedBox(height: 20.h), + _buildSlider(context, 1), + SizedBox(height: 20.h), + _buildPlayer(), + _buildSave(), + ], + ), + ), + ], + ); + } + + Widget _buildTimber() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 12.h), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24).w, + child: Text( + 'Timber', + style: TextStyle( + color: Colors.white, + fontSize: 20.sp, + fontWeight: FontWeight.w700, + ), + ), + ), + SizedBox(height: 16.h), + Obx(() { + return Expanded( + child: GridView.builder( + itemCount: controller.timberList.length, + padding: EdgeInsets.symmetric(horizontal: 24.w), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, + mainAxisSpacing: 10.h, + crossAxisSpacing: 0.w, + childAspectRatio: 102/130, + ), + itemBuilder: (context, index) { + return _buildItem(controller.timberList[index], index); + }, + ), + ); + }), + ], + ); + } + + Widget _buildItem(Timber item, int index) { + return GestureDetector( + onTap: () => controller.onTapItem(item, index), + child: Column( + children: [ + Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + Visibility( + visible: item.check, + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: Image.asset( + Assets.imagesChangeVoiceItemFrame, + width: 78.w, + height: 78.w, + fit: BoxFit.fill, + ), + ), + ClipRRect( + borderRadius: BorderRadius.circular(64.w/2), + child: Image.asset( + item.cover, + width: 64.w, + height: 64.w, + fit: BoxFit.cover, + ), + ), + Visibility( + visible: item.check, + child: Positioned( + right: 0.w, + bottom: 0.h, + child: Image.asset( + Assets.imagesChangeVoiceItemSelected, + width: 23.w, + height: 23.w, + fit: BoxFit.fill, + ), + ), + ), + ], + ), + SizedBox(height: 4.h), + Expanded( + child: MyMarqueeText( + text: item.name.toString(), + textStyle: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + } + + Widget _buildSlider(BuildContext context, int i) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 24).w, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + i == 0 ? 'Tone' : 'Sound speed', + style: TextStyle( + color: Colors.white, + fontSize: 20.sp, + fontWeight: FontWeight.w700, + ), + ), + SizedBox(height: 16.h), + Row( + children: [ + Expanded( + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: const Color(0x33000000), + inactiveTrackColor: const Color(0x33000000), + trackShape: RoundRectSliderTrackShape( + trackHeight: 20.h, + trackRadius: 12.r, + ), + thumbColor: Colors.black, + thumbShape: RoundRectSliderThumbShape( + thumbWidth: 32.w, + thumbHeight: 44.h, + thumbRadius: 12.r, + ), + ), + child: Obx(() { + return Slider( + min: 0.5, + max: 2, + value: i == 0 ? controller.toneValue.value : controller.soundSpeedValue.value, + onChanged: (value) => controller.onChanged(value, i), + ); + }), + ), + ), + SizedBox(width: 16.w), + Obx(() { + return Text( + i == 0 ? NumUtil.formatNum(controller.toneValue.value) : NumUtil.formatNum(controller.soundSpeedValue.value), + style: TextStyle( + color: const Color(0x73000000), + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ); + }), + ], + ), + ], + ), + ); + } + + Widget _buildPlayer() { + return Row( + children: [ + SizedBox(width: 24.w), + GestureDetector( + onTap: controller.togglePlayback, + child: ClipOval( + child: Container( + width: 54.w, + height: 54.w, + color: Colors.black, + child: Center( + child: Obx(() { + return Image.asset( + controller.playerController.isPlaying.value ? Assets.imagesPlaying1 : Assets.imagesNotPlayed1, + width: 29.w, + height: 29.w, + ); + }), + ), + ), + ), + ), + SizedBox(width: 16.w), + Expanded( + child: Column( + children: [ + SizedBox(height: 17.h), + ClipRRect( + borderRadius: BorderRadius.circular(6.5), + child: Obx(() { + return LinearProgressIndicator( + value: controller.playerController.positionValue.value, + valueColor: const AlwaysStoppedAnimation(Colors.black), + backgroundColor: const Color(0x40000000), + borderRadius: BorderRadius.circular(6.5), + minHeight: 12.h, + ); + }), + ), + SizedBox(height: 8.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Obx(() { + return Text( + controller.playerController.getPositionDuration(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0x73000000), + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ); + }), + ), + Flexible( + child: Obx(() { + return Text( + controller.playerController.getDuration(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0x73000000), + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ); + }), + ), + ], + ), + ], + ), + ), + SizedBox(width: 34.w), + ], + ); + } + + Widget _buildSave() { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 24).h, + child: ClipRRect( + borderRadius: BorderRadius.circular(26.5), + child: Material( + color: Colors.black, + child: InkWell( + onTap: controller.save, + child: Container( + width: 229.w, + height: 53.h, + alignment: Alignment.center, + child: Text( + 'Save', + style: TextStyle( + color: Colors.white, + fontSize: 24.sp, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/modules/voice/favourite/favourite_binding.dart b/lib/modules/voice/favourite/favourite_binding.dart new file mode 100644 index 0000000..ac01b4a --- /dev/null +++ b/lib/modules/voice/favourite/favourite_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'favourite_controller.dart'; + +class FavouriteBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => FavouriteController()); + } +} diff --git a/lib/modules/voice/favourite/favourite_controller.dart b/lib/modules/voice/favourite/favourite_controller.dart new file mode 100644 index 0000000..8892e06 --- /dev/null +++ b/lib/modules/voice/favourite/favourite_controller.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/components/dialog/remind_dialog.dart'; +import 'package:tone_snap/components/dialog/rename_dialog.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/data/storage/favorite_data.dart'; +import 'package:tone_snap/modules/voice/initial/initial_controller.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class FavouriteController extends GetxController { + static FavouriteController get to => Get.find(); + var scrollController = ScrollController(); + var voiceList = [].obs; + var viewState = ViewState.loading.obs; + + @override + void onReady() { + super.onReady(); + getData(); + } + + void getData() { + voiceList.value = FavoriteData().getList().reversed.toList(); + viewState.value = voiceList.isNotEmpty ? ViewState.normal : ViewState.empty; + voiceList.refresh(); + } + + void onTapItem(VoiceModel item) { + InitialController.to.currentPlayVoiceModel.value = item; + Get.toNamed(AppRoutes.playSound, arguments: item); + } + + void onTapReName(VoiceModel item) { + Get.dialog( + barrierDismissible: true, + RenameDialog( + name: item.name, + confirmOnTap: (value) { + item.name = value; + item.save(); + voiceList.refresh(); + + // 若 item 和当前正播放的 item 是同个对象,则需要同步修改 + if (identical(item, InitialController.to.currentPlayVoiceModel.value)) { + InitialController.to.currentPlayVoiceModel.update((e) => e?.name = value); + } + }, + ), + ); + } + + void onTapDelete(VoiceModel item) { + Get.dialog( + barrierDismissible: false, + RemindDialog( + content: 'Are you sure to delete it?', + confirmOnTap: () async { + BaseEasyLoading.loading(); + await item.delete(); + voiceList.remove(item); + voiceList.refresh(); + BaseEasyLoading.toast('Removed'); + + // 若 item 和当前正播放的 item 是同个对象,则需要同步修改 + if (identical(item, InitialController.to.currentPlayVoiceModel.value)) { + InitialController.to.isFavourite.value = false; + } + }, + ), + ); + } +} diff --git a/lib/modules/voice/favourite/favourite_view.dart b/lib/modules/voice/favourite/favourite_view.dart new file mode 100644 index 0000000..e364fa8 --- /dev/null +++ b/lib/modules/voice/favourite/favourite_view.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/private/my_voice_item.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/modules/voice/favourite/favourite_controller.dart'; + +class FavouriteView extends GetView { + const FavouriteView({super.key}); + + @override + Widget build(BuildContext context) { + Get.find(); + return Obx(() { + return ViewStateWidget( + viewState: controller.viewState.value, + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: Scrollbar( + controller: controller.scrollController, + child: ListView.builder( + controller: controller.scrollController, + itemCount: controller.voiceList.length, + padding: EdgeInsets.symmetric(vertical: 16.h), + itemBuilder: (context, index) { + var item = controller.voiceList[index]; + return MyVoiceItem( + item: item, + onTapItem: () => controller.onTapItem(item), + onReName: () => controller.onTapReName(item), + onDelete: () => controller.onTapDelete(item), + ); + }, + ), + ), + ), + ); + }); + } +} diff --git a/lib/modules/voice/home/home_binding.dart b/lib/modules/voice/home/home_binding.dart new file mode 100644 index 0000000..dd3bed3 --- /dev/null +++ b/lib/modules/voice/home/home_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/voice/home/home_controller.dart'; + +class HomeBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => HomeController()); + } +} diff --git a/lib/modules/voice/home/home_controller.dart b/lib/modules/voice/home/home_controller.dart new file mode 100644 index 0000000..73c48b8 --- /dev/null +++ b/lib/modules/voice/home/home_controller.dart @@ -0,0 +1,48 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/controllers/player_controller.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/voice/initial/initial_controller.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class HomeController extends GetxController { + static HomeController get to => Get.find(); + var playerController = PlayerController.to; + var voiceList = [ + VoiceModel(name: 'Child', path: Assets.audioChild, cover: Assets.changeVoiceAudioChild), + VoiceModel(name: 'Boy', path: Assets.audioBoy, cover: Assets.changeVoiceAudioBoy), + VoiceModel(name: 'Man', path: Assets.audioMan, cover: Assets.changeVoiceAudioMan), + VoiceModel(name: 'Elder', path: Assets.audioElder, cover: Assets.changeVoiceAudioElder), + // VoiceModel(name: 'Rapper', path: Assets.audioRapper, cover: Assets.changeVoiceAudioRapper), + VoiceModel(name: 'Robot', path: Assets.audioRobot, cover: Assets.changeVoiceAudioRobot), + VoiceModel(name: 'Girl', path: Assets.audioGirl, cover: Assets.changeVoiceAudioGirl), + VoiceModel(name: 'Sexy', path: Assets.audioSexy, cover: Assets.changeVoiceAudioSexy), + VoiceModel(name: 'Woman', path: Assets.audioWoman, cover: Assets.changeVoiceAudioWoman), + VoiceModel(name: 'Monsters', path: Assets.audioMonsters, cover: Assets.changeVoiceAudioMonsters), + ]; + + void onTapItem(VoiceModel item) { + InitialController.to.currentPlayVoiceModel.value = item; + Get.toNamed(AppRoutes.playSound, arguments: item); + } + + Future onTapPlayBarPlay(VoiceModel item) async { + if (isPlayItem(item)) { + await playerController.pausePlay(); + } else { + if (!identical(item, InitialController.to.currentPlayVoiceModel.value)) { + BaseEasyLoading.loading(); + await playerController.setFilePath(item.path); + BaseEasyLoading.dismiss(); + InitialController.to.currentPlayVoiceModel.value = item; + InitialController.to.isFavourite.value = InitialController.to.getIsFavouriteModel() != null; + } + await playerController.startPlay(); + } + } + + bool isPlayItem(VoiceModel item) { + return playerController.isPlaying.value && identical(InitialController.to.currentPlayVoiceModel.value, item); + } +} \ No newline at end of file diff --git a/lib/modules/voice/home/home_view.dart b/lib/modules/voice/home/home_view.dart new file mode 100644 index 0000000..c907683 --- /dev/null +++ b/lib/modules/voice/home/home_view.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/circular_notch_clipper.dart'; +import 'package:tone_snap/components/my_marquee_text.dart'; +import 'package:tone_snap/components/private/head_label.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/voice/home/home_controller.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class HomeView extends GetView { + const HomeView({super.key}); + + @override + Widget build(BuildContext context) { + Get.find(); + return Column( + children: [ + HeadLabel( + assets: Assets.imagesTheMonster, + width: 208.w, + height: 43.h, + ), + _buildGrid(context), + ], + ); + } + + Widget _buildGrid(BuildContext context) { + return Expanded( + child: GridView.builder( + padding: EdgeInsets.symmetric(horizontal: 24.w, vertical: 6.h), + itemCount: controller.voiceList.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + mainAxisSpacing: 20.h, + crossAxisSpacing: 15.w, + childAspectRatio: 102/130, + ), + itemBuilder: (context, index) { + return _buildItem(controller.voiceList[index], index); + }, + ), + ); + } + + Widget _buildItem(VoiceModel item, int index) { + return Column( + children: [ + GestureDetector( + onTap: () => controller.onTapItem(item), + child: Stack( + alignment: Alignment.center, + children: [ + Obx(() { + return Visibility( + visible: controller.isPlayItem(item), + maintainState: true, + maintainAnimation: true, + maintainSize: true, + child: Image.asset( + Assets.imagesSubtract, + width: 100.w, + height: 100.w, + fit: BoxFit.fill, + ), + ); + }), + Stack( + clipBehavior: Clip.none, + children: [ + Visibility( + visible: ObjUtil.isNotEmptyStr(item.cover), + child: ClipPath( + clipper: CircularNotchClipper( + notchRadius: 20, + ), + child: ClipRRect( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(85.w/2), + topRight: Radius.circular(85.w/2), + bottomLeft: Radius.circular(85.w/2), + bottomRight: const Radius.circular(2), + ), + child: Image.asset( + '${item.cover}', + width: 85.w, + height: 85.w, + fit: BoxFit.cover, + ), + ), + ), + ), + Positioned( + right: -10.w, + bottom: -10.h, + child: Obx(() { + return GestureDetector( + onTap: () => controller.onTapPlayBarPlay(item), + child: Image.asset( + controller.isPlayItem(item) ? Assets.imagesPlaying : Assets.imagesNotPlayed, + width: 30.w, + height: 30.w, + ), + ); + }), + ), + ], + ), + ], + ), + ), + SizedBox(height: 8.h), + Expanded( + child: MyMarqueeText( + text: item.name, + textStyle: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ); + } +} diff --git a/lib/modules/voice/initial/initial_binding.dart b/lib/modules/voice/initial/initial_binding.dart new file mode 100644 index 0000000..b32ef02 --- /dev/null +++ b/lib/modules/voice/initial/initial_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/voice/initial/initial_controller.dart'; + +class InitialBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => InitialController()); + } +} diff --git a/lib/modules/voice/initial/initial_controller.dart b/lib/modules/voice/initial/initial_controller.dart new file mode 100644 index 0000000..3d01cd0 --- /dev/null +++ b/lib/modules/voice/initial/initial_controller.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/controllers/player_controller.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/data/storage/favorite_data.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/voice/favourite/favourite_controller.dart'; +import 'package:tone_snap/modules/voice/home/home_view.dart'; +import 'package:tone_snap/modules/voice/me/me_controller.dart'; +import 'package:tone_snap/modules/voice/me/me_view.dart'; +import 'package:tone_snap/modules/voice/my_voice/my_voice_controller.dart'; +import 'package:tone_snap/modules/voice/settings/settings_view.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class InitialController extends GetxController { + static InitialController get to => Get.find(); + late PageController pageController; + final pages = [ + PageItem([Assets.imagesBnb1Selected, Assets.imagesBnb1Unselected], const HomeView()), + PageItem([Assets.imagesBnb2Selected, Assets.imagesBnb2Unselected], Container()), + PageItem([Assets.imagesBnb3Selected, Assets.imagesBnb3Unselected], const MeView()), + PageItem([Assets.imagesBnb4Selected, Assets.imagesBnb4Unselected], const SettingsView()), + ]; + var currentIndex = 0.obs; + Rx currentPlayVoiceModel = Rx(null); + var playerController = PlayerController.to; + + /// 是否加入喜欢列表 + var isFavourite = false.obs; + + @override + void onInit() { + super.onInit(); + pageController = PageController(initialPage: currentIndex.value); + } + + @override + void onClose() { + pageController.dispose(); + super.onClose(); + } + + void onBottomAppBarItemChanged(int index) { + if (index == 1) { + Get.toNamed(AppRoutes.uploadMethod); + } else { + if (index == 2) _refreshMe(); + currentIndex.value = index; + pageController.jumpToPage(index); + } + } + + void onTapPlayBar() { + Get.toNamed(AppRoutes.playSound, arguments: currentPlayVoiceModel.value); + } + + Future togglePlayback() async { + playerController.isPlaying.value + ? await playerController.pausePlay() + : await playerController.startPlay(); + } + + VoiceModel? getIsFavouriteModel() { + final list = FavoriteData().getList(); + return list.firstWhereOrNull((e) => e.path == currentPlayVoiceModel.value?.path); + } + + Future onTapFavourite() async { + if (currentPlayVoiceModel.value != null) { + if (isFavourite.value) { + getIsFavouriteModel()?.delete(); + isFavourite.value = false; + } else { + await FavoriteData().addData(currentPlayVoiceModel.value!.copyWith()); + isFavourite.value = true; + } + _refreshMe(); + } + } + + /// 刷新我的页面 + void _refreshMe() { + if (Get.isRegistered()) { + if (Get.isRegistered()) MyVoiceController.to.getData(); + if (Get.isRegistered()) FavouriteController.to.getData(); + } + } +} + +class PageItem { + late final List icons; + late final StatelessWidget widget; + + PageItem(this.icons, this.widget); +} diff --git a/lib/modules/voice/initial/initial_view.dart b/lib/modules/voice/initial/initial_view.dart new file mode 100644 index 0000000..1d9bf91 --- /dev/null +++ b/lib/modules/voice/initial/initial_view.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/keep_alive_wrapper.dart'; +import 'package:tone_snap/components/my_marquee_text.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/voice/initial/initial_controller.dart'; + +class InitialView extends StatelessWidget { + InitialView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Obx(() { + return IndexedStack( + index: controller.currentIndex.value, + children: [ + Stack( + children: [ + Image.asset( + Assets.imagesHomeBg, + width: 1.sw, + height: 1.sh, + fit: BoxFit.fill, + ), + Positioned( + bottom: 0, + child: Image.asset( + Assets.imagesHomeBnbBg, + width: 1.sw, + height: 222.h, + fit: BoxFit.fill, + ), + ), + ], + ), + Container(), + Image.asset( + Assets.imagesUploadMethodBg, + width: 1.sw, + height: 1.sh, + fit: BoxFit.fill, + ), + Image.asset( + Assets.imagesSettingsBg, + width: 1.sw, + height: 1.sh, + fit: BoxFit.fill, + ), + ], + ); + }), + Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: Column( + children: [ + Expanded( + child: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: controller.pageController, + children: controller.pages.map((e) => KeepAliveWrapper(child: e.widget)).toList(), + ), + ), + _buildPlayBar(), + ], + ), + bottomNavigationBar: _buildBottomAppBar(), + ), + ], + ); + } + + Widget _buildPlayBar() { + return Obx(() { + return Visibility( + visible: controller.currentPlayVoiceModel.value != null && !controller.playerController.isCompleted.value, + child: GestureDetector( + onTap: controller.onTapPlayBar, + child: Container( + height: 72.h, + margin: EdgeInsets.fromLTRB(14.w, 0, 14.w, 7.h), + padding: const EdgeInsets.only(left: 34, right: 30).w, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(39), + ), + child: Row( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Obx(() { + return MyMarqueeText( + text: '${controller.currentPlayVoiceModel.value?.name}', + textStyle: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.bold, + ), + ); + }), + SizedBox(height: 4.h), + Obx(() { + return Text( + '${controller.playerController.getPositionDuration()}/${controller.playerController.getDuration()}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0x73FFFFFF), + fontSize: 12.sp, + ), + ); + }), + ], + ), + ), + SizedBox(width: 13.w), + Obx(() { + return GestureDetector( + onTap: controller.onTapFavourite, + child: Image.asset( + controller.isFavourite.value ? Assets.imagesFavorite : Assets.imagesNotFavorite, + width: 28.w, + height: 28.w, + ), + ); + }), + SizedBox(width: 13.w), + GestureDetector( + onTap: controller.togglePlayback, + child: Image.asset( + controller.playerController.isPlaying.value ? Assets.imagesPlaying1 : Assets.imagesNotPlayed1, + width: 38.w, + height: 38.w, + ), + ), + ], + ), + ), + ), + ); + }); + } + + BottomAppBar _buildBottomAppBar() { + return BottomAppBar( + height: kBottomNavigationBarHeight, + padding: EdgeInsets.symmetric(horizontal: (1.sw - (28.w * 4)) / 8), + color: Colors.transparent, + child: SizedBox( + height: double.infinity, + child: Row( + children: controller.pages.asMap().entries.map((e) { + return Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + child: Obx(() { + return Image.asset( + e.value.icons[controller.currentIndex.value == e.key ? 1 : 0], + width: 28.w, + height: 28.w, + color: controller.currentIndex.value == e.key ? const Color(0xFF8602ED) : null, + ); + }), + onTap: () => controller.onBottomAppBarItemChanged(e.key), + ), + ); + }).toList(), + ), + ), + ); + } +} diff --git a/lib/modules/voice/me/me_binding.dart b/lib/modules/voice/me/me_binding.dart new file mode 100644 index 0000000..37bd0ec --- /dev/null +++ b/lib/modules/voice/me/me_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/voice/me/me_controller.dart'; + +class MeBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => MeController()); + } +} diff --git a/lib/modules/voice/me/me_controller.dart b/lib/modules/voice/me/me_controller.dart new file mode 100644 index 0000000..15328e2 --- /dev/null +++ b/lib/modules/voice/me/me_controller.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/modules/voice/favourite/favourite_view.dart'; +import 'package:tone_snap/modules/voice/my_voice/my_voice_view.dart'; + +class MeController extends GetxController with GetTickerProviderStateMixin { + late TabController tabController; + final labels = ['My Voice', 'Favourite']; + final pages = [const MyVoiceView(), const FavouriteView()]; + + @override + void onInit() { + super.onInit(); + tabController = TabController(length: labels.length, vsync: this); + } + + @override + void onClose() { + tabController.dispose(); + super.onClose(); + } +} diff --git a/lib/modules/voice/me/me_view.dart b/lib/modules/voice/me/me_view.dart new file mode 100644 index 0000000..84bc53a --- /dev/null +++ b/lib/modules/voice/me/me_view.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/keep_alive_wrapper.dart'; +import 'package:tone_snap/components/my_custom_indicator.dart'; +import 'package:tone_snap/components/private/head_label.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/voice/me/me_controller.dart'; + +class MeView extends GetView { + const MeView({super.key}); + + @override + Widget build(BuildContext context) { + Get.find(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HeadLabel( + assets: Assets.imagesTheMonster1, + width: 135.w, + height: 51.h, + ), + _buildTabBar(), + _buildTabBarView(), + ], + ); + } + + Widget _buildTabBar() { + return TabBar( + controller: controller.tabController, + tabAlignment: TabAlignment.center, + dividerHeight: 0, + padding: const EdgeInsets.symmetric(horizontal: 8).w, + labelPadding: const EdgeInsets.symmetric(horizontal: 16).w, + labelStyle: TextStyle( + color: Colors.white, + fontSize: 20.sp, + fontWeight: FontWeight.w600, + ), + unselectedLabelStyle: TextStyle( + color: const Color(0x4DFFFFFF), + fontSize: 20.sp, + fontWeight: FontWeight.w600, + ), + indicatorPadding: EdgeInsets.fromLTRB(0, 4.h, 0, 0), + indicator: MyCustomIndicator( + indWidth: 16.w, + indHeight: 4.h, + radius: 3.5.r, + ), + tabs: controller.labels.map((e) => Tab(text: e)).toList(), + ); + } + + Widget _buildTabBarView() { + return Expanded( + child: TabBarView( + controller: controller.tabController, + children: controller.pages.map((e) => KeepAliveWrapper(child: e)).toList(), + ), + ); + } +} diff --git a/lib/modules/voice/my_voice/my_voice_binding.dart b/lib/modules/voice/my_voice/my_voice_binding.dart new file mode 100644 index 0000000..2851544 --- /dev/null +++ b/lib/modules/voice/my_voice/my_voice_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'my_voice_controller.dart'; + +class MyVoiceBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => MyVoiceController()); + } +} diff --git a/lib/modules/voice/my_voice/my_voice_controller.dart b/lib/modules/voice/my_voice/my_voice_controller.dart new file mode 100644 index 0000000..4587034 --- /dev/null +++ b/lib/modules/voice/my_voice/my_voice_controller.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/components/dialog/remind_dialog.dart'; +import 'package:tone_snap/components/dialog/rename_dialog.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/data/storage/my_voice_data.dart'; +import 'package:tone_snap/modules/voice/initial/initial_controller.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class MyVoiceController extends GetxController { + static MyVoiceController get to => Get.find(); + var scrollController = ScrollController(); + var voiceList = [].obs; + var viewState = ViewState.loading.obs; + + @override + void onReady() { + super.onReady(); + getData(); + } + + void getData() { + voiceList.value = MyVoiceData().getList().reversed.toList(); + viewState.value = voiceList.isNotEmpty ? ViewState.normal : ViewState.empty; + voiceList.refresh(); + } + + void onTapItem(VoiceModel item) { + InitialController.to.currentPlayVoiceModel.value = item; + Get.toNamed(AppRoutes.playSound, arguments: item); + } + + void onTapReName(VoiceModel item) { + Get.dialog( + barrierDismissible: true, + RenameDialog( + name: item.name, + confirmOnTap: (value) { + item.name = value; + item.save(); + voiceList.refresh(); + + // 若 item 和当前正播放的 item 是同个对象,则需要同步改名 + if (identical(item, InitialController.to.currentPlayVoiceModel.value)) { + InitialController.to.currentPlayVoiceModel.update((e) => e?.name = value); + } + }, + ), + ); + } + + void onTapDelete(VoiceModel item) { + Get.dialog( + barrierDismissible: false, + RemindDialog( + content: 'Are you sure to delete it?', + confirmOnTap: () async { + BaseEasyLoading.loading(); + await item.delete(); + voiceList.remove(item); + voiceList.refresh(); + BaseEasyLoading.toast('Removed'); + }, + ), + ); + } +} diff --git a/lib/modules/voice/my_voice/my_voice_view.dart b/lib/modules/voice/my_voice/my_voice_view.dart new file mode 100644 index 0000000..63d3362 --- /dev/null +++ b/lib/modules/voice/my_voice/my_voice_view.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/private/my_voice_item.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/modules/voice/my_voice/my_voice_controller.dart'; + +class MyVoiceView extends GetView { + const MyVoiceView({super.key}); + + @override + Widget build(BuildContext context) { + Get.find(); + return Obx(() { + return ViewStateWidget( + viewState: controller.viewState.value, + child: MediaQuery.removePadding( + context: context, + removeTop: true, + child: Scrollbar( + controller: controller.scrollController, + child: ListView.builder( + controller: controller.scrollController, + itemCount: controller.voiceList.length, + padding: EdgeInsets.symmetric(vertical: 16.h), + itemBuilder: (context, index) { + var item = controller.voiceList[index]; + return MyVoiceItem( + item: item, + onTapItem: () => controller.onTapItem(item), + onReName: () => controller.onTapReName(item), + onDelete: () => controller.onTapDelete(item), + ); + }, + ), + ), + ), + ); + }); + } +} diff --git a/lib/modules/voice/play_sound/play_sound_binding.dart b/lib/modules/voice/play_sound/play_sound_binding.dart new file mode 100644 index 0000000..ed6dde1 --- /dev/null +++ b/lib/modules/voice/play_sound/play_sound_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/voice/play_sound/play_sound_controller.dart'; + +class PlaySoundBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => PlaySoundController()); + } +} diff --git a/lib/modules/voice/play_sound/play_sound_controller.dart b/lib/modules/voice/play_sound/play_sound_controller.dart new file mode 100644 index 0000000..9252329 --- /dev/null +++ b/lib/modules/voice/play_sound/play_sound_controller.dart @@ -0,0 +1,38 @@ +import 'dart:async'; + +import 'package:get/get.dart'; +import 'package:tone_snap/controllers/player_controller.dart'; +import 'package:tone_snap/data/models/voice_model.dart'; +import 'package:tone_snap/modules/voice/initial/initial_controller.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class PlaySoundController extends GetxController { + var playerController = PlayerController.to; + late VoiceModel voiceModel; + + @override + void onInit() { + super.onInit(); + voiceModel = Get.arguments; + } + + @override + void onReady() async { + super.onReady(); + InitialController.to.isFavourite.value = InitialController.to.getIsFavouriteModel() != null; + if (playerController.filePath != voiceModel.path || playerController.isCompleted.value) { + await playerController.setFilePath(voiceModel.path); + await playerController.startPlay(); + } + } + + Future togglePlayback() async { + playerController.isPlaying.value + ? await playerController.pausePlay() + : await playerController.startPlay(); + } + + void goChangeVoice() async { + Get.toNamed(AppRoutes.changeVoice, arguments: voiceModel.path); + } +} diff --git a/lib/modules/voice/play_sound/play_sound_view.dart b/lib/modules/voice/play_sound/play_sound_view.dart new file mode 100644 index 0000000..d94d63e --- /dev/null +++ b/lib/modules/voice/play_sound/play_sound_view.dart @@ -0,0 +1,230 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/my_marquee_text.dart'; +import 'package:tone_snap/components/navigation_bar/base_appbar.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/voice/initial/initial_controller.dart'; +import 'package:tone_snap/modules/voice/play_sound/play_sound_controller.dart'; +import 'package:tone_snap/utils/obj_util.dart'; + +class PlaySoundView extends StatelessWidget { + PlaySoundView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + Image.asset( + Assets.imagesPlaySoundBg, + width: 1.sw, + height: 1.sh, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + appBar: BaseAppBar('', backWidget: _buildBackWidget(), backgroundColor: Colors.transparent), + body: SizedBox( + width: 1.sw, + child: Column( + children: [ + _buildCover(), + SizedBox(height: 60.h), + _buildVoiceName(), + SizedBox(height: 32.h), + _buildProgressBar(), + SizedBox(height: 50.h), + _buildControlMenu(), + SizedBox(height: 50.h), + ], + ), + ), + ), + ], + ); + } + + Widget _buildBackWidget() { + return ClipOval( + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: Get.back, + child: Padding( + padding: const EdgeInsets.all(10).w, + child: Image.asset( + Assets.imagesArrowDownBack, + width: 24.w, + height: 24.w, + ), + ), + ), + ), + ); + } + + Widget _buildCover() { + return Expanded( + child: Visibility( + visible: ObjUtil.isNotEmptyStr(controller.voiceModel.cover), + replacement: SizedBox( + width: 296.w, + height: 296.w, + child: FittedBox( + fit: BoxFit.none, + child: Image.asset( + Assets.imagesVoiceDefault, + width: 200.w, + height: 200.w, + fit: BoxFit.contain, + ), + ), + ), + child: FittedBox( + fit: BoxFit.none, + child: ClipRRect( + borderRadius: BorderRadius.circular(31), + child: Image.asset( + '${controller.voiceModel.cover}', + width: 296.w, + height: 296.w, + fit: BoxFit.cover, + ), + ), + ), + ), + ); + } + + Widget _buildVoiceName() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16).w, + child: MyMarqueeText( + text: controller.voiceModel.name, + textStyle: TextStyle( + color: Colors.black, + fontSize: 24.sp, + fontWeight: FontWeight.bold, + ), + ), + ); + } + + Widget _buildProgressBar() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16).w, + child: Column( + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(6.5), + child: Obx(() { + return LinearProgressIndicator( + value: controller.playerController.positionValue.value, + valueColor: const AlwaysStoppedAnimation(Colors.black), + backgroundColor: const Color(0x40000000), + borderRadius: BorderRadius.circular(6.5), + minHeight: 12.h, + ); + }), + ), + SizedBox(height: 6.h), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Obx(() { + return Text( + controller.playerController.getPositionDuration(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0x73000000), + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ); + }), + ), + Flexible( + child: Obx(() { + return Text( + controller.playerController.getDuration(), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: const Color(0x73000000), + fontSize: 14.sp, + fontWeight: FontWeight.w500, + ), + ); + }), + ), + ], + ), + ], + ), + ); + } + + Widget _buildControlMenu() { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + GestureDetector( + onTap: InitialController.to.onTapFavourite, + child: ClipOval( + child: Container( + width: 52.w, + height: 52.w, + color: Colors.black, + child: Obx(() { + return Image.asset( + InitialController.to.isFavourite.value ? Assets.imagesFavorite : Assets.imagesNotFavorite, + width: 28.w, + height: 28.w, + ); + }), + ), + ), + ), + SizedBox(width: 30.w), + GestureDetector( + onTap: controller.togglePlayback, + child: ClipOval( + child: Container( + width: 76.w, + height: 76.w, + color: Colors.black, + child: Obx(() { + return Image.asset( + controller.playerController.isPlaying.value ? Assets.imagesPlaying1 : Assets.imagesNotPlayed1, + width: 38.w, + height: 38.w, + ); + }), + ), + ), + ), + SizedBox(width: 30.w), + GestureDetector( + onTap: controller.goChangeVoice, + child: ClipOval( + child: Container( + width: 52.w, + height: 52.w, + color: Colors.black, + child: Image.asset( + Assets.imagesVoiceChange, + width: 28.w, + height: 28.w, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/modules/voice/record_sound/record_sound_binding.dart b/lib/modules/voice/record_sound/record_sound_binding.dart new file mode 100644 index 0000000..008e1f1 --- /dev/null +++ b/lib/modules/voice/record_sound/record_sound_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/voice/record_sound/record_sound_controller.dart'; + +class RecordSoundBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => RecordSoundController()); + } +} diff --git a/lib/modules/voice/record_sound/record_sound_controller.dart b/lib/modules/voice/record_sound/record_sound_controller.dart new file mode 100644 index 0000000..8ec2736 --- /dev/null +++ b/lib/modules/voice/record_sound/record_sound_controller.dart @@ -0,0 +1,133 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter_sound/flutter_sound.dart'; +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:tone_snap/routes/app_routes.dart'; +import 'package:tone_snap/utils/date_util.dart'; +import 'package:tone_snap/utils/local_path_util.dart'; +import 'package:tone_snap/utils/permission_util.dart'; + +class RecordSoundController extends GetxController { + final _recorder = FlutterSoundRecorder(); + + /// isStopped:0, isPaused:1, isRecording:2 + var recorderState = 0.obs; + + /// 录音文件路径 + var _filePath = ''; + + /// 计时器 + Timer? _timer; + var recordDuration = 0.obs; + + @override + void onInit() async { + super.onInit(); + await _recorder.openRecorder(); + } + + @override + void onClose() { + _stopTimer(); + _recorder.closeRecorder(); + super.onClose(); + } + + void onTapRecord() { + recorderState.value == 2 ? pauseRecording() : (recorderState.value == 1 ? resumeRecording() : startRecording()); + } + + /// 开始录音 + Future startRecording() async { + bool result = await PermissionUtil.checkPermission([Permission.microphone]); + if (!result) return; + + Directory dir = await LocalPathUtil.getRecordingsDir(); + String path = '${dir.path}/${DateUtil.getNowTimestamp()}${ext[Codec.pcm16WAV.index]}'; + + recordDuration.value = 0; + await _recorder.startRecorder( + toFile: path, + codec: Codec.pcm16WAV, + bitRate: 16000, + numChannels: 1, + sampleRate: 44100, + ); + _startTimer(); + recorderState.value = 2; + _filePath = path; + } + + /// 恢复暂停的录音 + Future resumeRecording() async { + await _recorder.resumeRecorder(); + _startTimer(); + recorderState.value = 2; + } + + /// 暂停录音 + Future pauseRecording() async { + await _recorder.pauseRecorder(); + _stopTimer(); + recorderState.value = 1; + } + + /// 停止录音 + Future stopRecording() async { + await _recorder.stopRecorder(); + _stopTimer(); + recorderState.value = 0; + } + + /// 开始定时器 + void _startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (Timer t) { + recordDuration.value++; + // 设置最大录音时长 + if (recordDuration.value >= 3600000) { + stopRecording(); + return; + } + }); + } + + /// 停止定时器 + void _stopTimer() { + _timer?.cancel(); + _timer = null; + } + + /// 获取录制时长 + String getRecordDuration() { + final duration = Duration(seconds: recordDuration.value); + final minutes = duration.inMinutes.remainder(60); + final secs = duration.inSeconds.remainder(60); + return [ + if (minutes < 10) '0$minutes' else '$minutes', + if (secs < 10) '0$secs' else '$secs' + ].join(':'); + } + + String getRecordText() { + if (recorderState.value == 0) { + return 'Click to start'; + } else if (recorderState.value == 1) { + return 'Click to continue'; + } else if (recorderState.value == 2) { + return 'Click to pause'; + } + return ''; + } + + bool showChangeVoice() { + return (recorderState.value == 0 || recorderState.value == 1) && recordDuration.value > 0; + } + + Future goChangeVoice() async { + _stopTimer(); + _recorder.closeRecorder(); + Get.offNamed(AppRoutes.changeVoice, arguments: _filePath); + } +} \ No newline at end of file diff --git a/lib/modules/voice/record_sound/record_sound_view.dart b/lib/modules/voice/record_sound/record_sound_view.dart new file mode 100644 index 0000000..63d3cc2 --- /dev/null +++ b/lib/modules/voice/record_sound/record_sound_view.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/navigation_bar/base_appbar.dart'; +import 'package:tone_snap/modules/voice/record_sound/record_sound_controller.dart'; + +class RecordSoundView extends StatelessWidget { + RecordSoundView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const BaseAppBar('Record Sound'), + body: SingleChildScrollView( + child: Center( + child: Column( + children: [ + SizedBox(height: 167.h), + _buildTime(), + SizedBox(height: 200.h), + Obx(() { + return _buildBtn( + controller.getRecordText(), + controller.onTapRecord, + ); + }), + Obx(() { + return Visibility( + visible: controller.recorderState.value == 2, + child: Padding( + padding: const EdgeInsets.only(top: 20).h, + child: _buildBtn('Click to stop', controller.stopRecording), + ), + ); + }), + Obx(() { + return Visibility( + visible: controller.showChangeVoice(), + child: Padding( + padding: const EdgeInsets.only(top: 20).h, + child: _buildBtn('Change voice', controller.goChangeVoice), + ), + ); + }), + ], + ), + ), + ), + ); + } + + Widget _buildTime() { + return Obx(() { + return Text( + controller.getRecordDuration(), + style: TextStyle( + color: Colors.white, + fontSize: 48.sp, + fontWeight: FontWeight.bold, + ), + ); + }); + } + + Widget _buildBtn(String text, Function() onTap) { + return ClipRRect( + borderRadius: BorderRadius.circular(22.5), + child: Material( + color: const Color(0xFF191919), + child: InkWell( + onTap: onTap, + child: Container( + width: 184.w, + height: 43.h, + alignment: Alignment.center, + child: Text( + text, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/voice/settings/settings_binding.dart b/lib/modules/voice/settings/settings_binding.dart new file mode 100644 index 0000000..3dcae3b --- /dev/null +++ b/lib/modules/voice/settings/settings_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/voice/settings/settings_controller.dart'; + +class SettingsBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => SettingsController()); + } +} diff --git a/lib/modules/voice/settings/settings_controller.dart b/lib/modules/voice/settings/settings_controller.dart new file mode 100644 index 0000000..d42c227 --- /dev/null +++ b/lib/modules/voice/settings/settings_controller.dart @@ -0,0 +1,24 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class SettingsController extends GetxController { + final options = ['About', 'Privacy Policy', 'User Agreement']; + final optionIcons = [Assets.imagesAbout, Assets.imagesPrivacy, Assets.imagesUserAgreement]; + + void onTapItem(int index) async { + if (index == 0) { + Get.toNamed(AppRoutes.about); + } else if (index == 1) { + Get.toNamed(AppRoutes.privacy, arguments: { + 'title': options[index], + 'url': 'https://tonesnap-privacy.mystrikingly.com', + }); + } else if (index == 2) { + Get.toNamed(AppRoutes.terms, arguments: { + 'title': options[index], + 'url': 'https://tonesnap-terms.mystrikingly.com' + }); + } + } +} diff --git a/lib/modules/voice/settings/settings_view.dart b/lib/modules/voice/settings/settings_view.dart new file mode 100644 index 0000000..d36830a --- /dev/null +++ b/lib/modules/voice/settings/settings_view.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/private/head_label.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/voice/settings/settings_controller.dart'; +import 'package:tone_snap/res/themes/app_sizes.dart'; + +class SettingsView extends GetView{ + const SettingsView({super.key}); + + @override + Widget build(BuildContext context) { + Get.find(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + HeadLabel( + assets: Assets.imagesTheMonster2, + width: 155.w, + height: 51.h, + ), + Expanded( + child: ListView.separated( + padding: EdgeInsets.zero, + itemCount: controller.options.length, + itemBuilder: (context, index) { + return _buildOptionItem(index); + }, + separatorBuilder: (context, index) { + return SizedBox(height: 10.h); + }, + ), + ), + ], + ); + } + + Widget _buildOptionItem(index) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => controller.onTapItem(index), + child: Container( + height: 56.h, + padding: const EdgeInsets.only(left: 24, right: 32).w, + child: Row( + children: [ + Image.asset(controller.optionIcons[index]), + SizedBox(width: 12.w), + Expanded( + child: Text( + controller.options[index], + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w500, + ), + ), + ), + Image.asset(Assets.imagesIconChevronRight), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/voice/upload_method/upload_mothod_binding.dart b/lib/modules/voice/upload_method/upload_mothod_binding.dart new file mode 100644 index 0000000..da164e4 --- /dev/null +++ b/lib/modules/voice/upload_method/upload_mothod_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import 'package:tone_snap/modules/voice/upload_method/upload_mothod_controller.dart'; + +class UploadMethodBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => UploadMethodController()); + } +} diff --git a/lib/modules/voice/upload_method/upload_mothod_controller.dart b/lib/modules/voice/upload_method/upload_mothod_controller.dart new file mode 100644 index 0000000..f9ccd58 --- /dev/null +++ b/lib/modules/voice/upload_method/upload_mothod_controller.dart @@ -0,0 +1,47 @@ +import 'dart:io'; + +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:tone_snap/components/base_easyloading.dart'; +import 'package:tone_snap/controllers/player_controller.dart'; +import 'package:tone_snap/routes/app_routes.dart'; +import 'package:tone_snap/utils/permission_util.dart'; + +class UploadMethodController extends GetxController { + + Future goRecordSound() async { + await PlayerController.to.pausePlay(); + Get.toNamed(AppRoutes.recordSound); + } + + void openFilePicker() async { + Permission permission; + if (Platform.isAndroid) { + DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfoPlugin.androidInfo; + if (androidInfo.version.sdkInt >= 33) { + permission = Permission.audio; + } else { + permission = Permission.storage; + } + } else { + permission = Permission.mediaLibrary; + } + bool result = await PermissionUtil.checkPermission([permission]); + if (!result) return; + + FilePickerResult? filePickerResult = await FilePicker.platform.pickFiles( + type: FileType.audio, + onFileLoading: (FilePickerStatus status) { + if (status == FilePickerStatus.picking) BaseEasyLoading.loading(); + if (status == FilePickerStatus.done) BaseEasyLoading.dismiss(); + }, + ); + if (filePickerResult != null) { + PlatformFile file = filePickerResult.files.first; + Get.toNamed(AppRoutes.changeVoice, arguments: file.path); + } + } +} diff --git a/lib/modules/voice/upload_method/upload_mothod_view.dart b/lib/modules/voice/upload_method/upload_mothod_view.dart new file mode 100644 index 0000000..9102121 --- /dev/null +++ b/lib/modules/voice/upload_method/upload_mothod_view.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/navigation_bar/base_appbar.dart'; +import 'package:tone_snap/generated/assets.dart'; +import 'package:tone_snap/modules/voice/upload_method/upload_mothod_controller.dart'; + +class UploadMethodView extends StatelessWidget { + UploadMethodView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + Get.put(UploadMethodController()); + final labelTextStyle = TextStyle( + color: Colors.white, + fontSize: 20.sp, + fontWeight: FontWeight.bold, + ); + return Stack( + children: [ + Image.asset( + Assets.imagesUploadMethodBg, + width: 1.sw, + height: 1.sh, + fit: BoxFit.fill, + ), + Scaffold( + backgroundColor: Colors.transparent, + appBar: const BaseAppBar('Upload method', backgroundColor: Colors.transparent), + body: SingleChildScrollView( + child: Center( + child: Column( + children: [ + SizedBox(height: 88.h), + GestureDetector( + onTap: controller.goRecordSound, + child: Image.asset( + Assets.imagesUploadRecordSound, + width: 122.w, + height: 122.w, + ), + ), + SizedBox(height: 12.h), + Text( + 'Record Sound', + style: labelTextStyle, + ), + SizedBox(height: 94.h), + GestureDetector( + onTap: controller.openFilePicker, + child: Image.asset( + Assets.imagesUploadPick, + width: 122.w, + height: 122.w, + ), + ), + SizedBox(height: 12.h), + Text( + 'Pick From Files', + style: labelTextStyle, + ), + ], + ), + ), + ), + ) + ], + ); + } +} diff --git a/lib/modules/web_page/web_page_binding.dart b/lib/modules/web_page/web_page_binding.dart new file mode 100644 index 0000000..b6fd67e --- /dev/null +++ b/lib/modules/web_page/web_page_binding.dart @@ -0,0 +1,10 @@ +import 'package:get/get.dart'; + +import 'web_page_controller.dart'; + +class WebPageBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => WebPageController()); + } +} diff --git a/lib/modules/web_page/web_page_controller.dart b/lib/modules/web_page/web_page_controller.dart new file mode 100644 index 0000000..bb5e76c --- /dev/null +++ b/lib/modules/web_page/web_page_controller.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class WebPageController extends GetxController { + var title = ''; + var url = ''; + late final WebViewController webViewController; + var viewState = ViewState.loading.obs; + + @override + void onInit() { + super.onInit(); + title = Get.arguments['title']; + url = Get.arguments['url'] ?? ''; + webViewController = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setBackgroundColor(Colors.white) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) { + // Update loading bar. + }, + onPageStarted: (String url) {}, + onPageFinished: (String url) { + viewState.value = ViewState.normal; + }, + onWebResourceError: (WebResourceError error) { + }, + onNavigationRequest: (NavigationRequest request) { + return NavigationDecision.navigate; + }, + ), + ) + ..loadRequest(Uri.parse(url)); + } +} diff --git a/lib/modules/web_page/web_page_view.dart b/lib/modules/web_page/web_page_view.dart new file mode 100644 index 0000000..272e287 --- /dev/null +++ b/lib/modules/web_page/web_page_view.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/components/navigation_bar/base_appbar.dart'; +import 'package:tone_snap/components/view_state_widget.dart'; +import 'package:tone_snap/modules/web_page/web_page_controller.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class WebPageView extends StatelessWidget { + WebPageView({super.key}); + + final controller = Get.find(); + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: BaseAppBar(controller.title), + body: Obx(() { + return ViewStateWidget( + viewState: controller.viewState.value, + child: WebViewWidget(controller: controller.webViewController), + ); + }), + ); + } +} diff --git a/lib/res/themes/app_colors.dart b/lib/res/themes/app_colors.dart new file mode 100644 index 0000000..776ada4 --- /dev/null +++ b/lib/res/themes/app_colors.dart @@ -0,0 +1,8 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 颜色 + +import 'package:flutter/material.dart'; + +/// 种子颜色 +const seedColor = Color(0xFF000000); \ No newline at end of file diff --git a/lib/res/themes/app_sizes.dart b/lib/res/themes/app_sizes.dart new file mode 100644 index 0000000..7d30358 --- /dev/null +++ b/lib/res/themes/app_sizes.dart @@ -0,0 +1,23 @@ +// Author: fengshengxiong +// Date: 2024/5/8 +// Description: 大小 + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:get/get.dart'; + +double get statusBarHeight => MediaQuery.of(Get.context!).padding.top; +double get bottomBarHeight => MediaQuery.of(Get.context!).padding.bottom; + +/// BaseAppBar高度 +double get baseAppBarHeight => statusBarHeight + kToolbarHeight; + +/// 有kBottomNavigationBarHeight的body高度 +double get bodyHeight => 1.sh - + statusBarHeight - + bottomBarHeight - + kToolbarHeight - + kBottomNavigationBarHeight; + +/// 没有kBottomNavigationBarHeight的body高度 +double get noBnbBodyHeight => 1.sh - statusBarHeight - bottomBarHeight - kToolbarHeight; diff --git a/lib/res/themes/app_themes.dart b/lib/res/themes/app_themes.dart new file mode 100644 index 0000000..1c613a8 --- /dev/null +++ b/lib/res/themes/app_themes.dart @@ -0,0 +1,19 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 主题 + +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:tone_snap/res/themes/app_colors.dart'; + +/// 主题 +ThemeData appTheme = ThemeData.dark(useMaterial3: true).copyWith( + colorScheme: ColorScheme.fromSeed(seedColor: seedColor, primary: seedColor), + appBarTheme: const AppBarTheme(color: seedColor), + scaffoldBackgroundColor: seedColor, + scrollbarTheme: ScrollbarThemeData( + thumbVisibility: WidgetStateProperty.all(false), + thumbColor: WidgetStateProperty.all(Colors.white), + radius: Radius.circular(8.r), + ), +); \ No newline at end of file diff --git a/lib/res/values/strings.dart b/lib/res/values/strings.dart new file mode 100644 index 0000000..b5b976f --- /dev/null +++ b/lib/res/values/strings.dart @@ -0,0 +1,5 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 文本 + +const appName = 'ToneSnap'; \ No newline at end of file diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart new file mode 100644 index 0000000..22f6cd5 --- /dev/null +++ b/lib/routes/app_pages.dart @@ -0,0 +1,75 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 路由页面映射 + +import 'package:flutter/animation.dart'; +import 'package:get/get.dart'; +import 'package:tone_snap/modules/voice/about/about_binding.dart'; +import 'package:tone_snap/modules/voice/about/about_view.dart'; +import 'package:tone_snap/modules/voice/change_voice/change_voice_binding.dart'; +import 'package:tone_snap/modules/voice/change_voice/change_voice_view.dart'; +import 'package:tone_snap/modules/voice/favourite/favourite_binding.dart'; +import 'package:tone_snap/modules/voice/home/home_binding.dart'; +import 'package:tone_snap/modules/voice/initial/initial_binding.dart'; +import 'package:tone_snap/modules/voice/initial/initial_view.dart'; +import 'package:tone_snap/modules/voice/me/me_binding.dart'; +import 'package:tone_snap/modules/voice/my_voice/my_voice_binding.dart'; +import 'package:tone_snap/modules/voice/play_sound/play_sound_binding.dart'; +import 'package:tone_snap/modules/voice/play_sound/play_sound_view.dart'; +import 'package:tone_snap/modules/voice/record_sound/record_sound_binding.dart'; +import 'package:tone_snap/modules/voice/record_sound/record_sound_view.dart'; +import 'package:tone_snap/modules/voice/settings/settings_binding.dart'; +import 'package:tone_snap/modules/voice/upload_method/upload_mothod_binding.dart'; +import 'package:tone_snap/modules/voice/upload_method/upload_mothod_view.dart'; +import 'package:tone_snap/modules/web_page/web_page_binding.dart'; +import 'package:tone_snap/modules/web_page/web_page_view.dart'; +import 'package:tone_snap/routes/app_routes.dart'; + +class AppPages { + AppPages._(); + + static final routes = [ + GetPage( + name: AppRoutes.initial, + page: () => InitialView(), + bindings: [InitialBinding(), HomeBinding(), MeBinding(), SettingsBinding(), MyVoiceBinding(), FavouriteBinding()], + ), + GetPage( + name: AppRoutes.uploadMethod, + page: () => UploadMethodView(), + binding: UploadMethodBinding() + ), + GetPage( + name: AppRoutes.recordSound, + page: () => RecordSoundView(), + binding: RecordSoundBinding(), + ), + GetPage( + name: AppRoutes.changeVoice, + page: () => ChangeVoiceView(), + binding: ChangeVoiceBinding(), + ), + GetPage( + name: AppRoutes.playSound, + page: () => PlaySoundView(), + binding: PlaySoundBinding(), + transition: Transition.downToUp, + curve: Curves.easeIn, + ), + GetPage( + name: AppRoutes.about, + page: () => AboutView(), + binding: AboutBinding(), + ), + GetPage( + name: AppRoutes.privacy, + page: () => WebPageView(), + binding: WebPageBinding(), + ), + GetPage( + name: AppRoutes.terms, + page: () => WebPageView(), + binding: WebPageBinding(), + ), + ]; +} diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart new file mode 100644 index 0000000..80be0fa --- /dev/null +++ b/lib/routes/app_routes.dart @@ -0,0 +1,18 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 路由配置 + +class AppRoutes { + AppRoutes._(); + + // static const splash = '/'; + static const initial = '/'; + // static const initial = '/initial'; + static const uploadMethod = '/upload_method'; + static const recordSound = '/record_sound'; + static const changeVoice = '/change_voice'; + static const playSound = '/play_sound'; + static const about = '/about'; + static const privacy = '/privacy'; + static const terms = '/terms'; +} diff --git a/lib/utils/audio_util.dart b/lib/utils/audio_util.dart new file mode 100644 index 0000000..5be4b4f --- /dev/null +++ b/lib/utils/audio_util.dart @@ -0,0 +1,25 @@ +// Author: fengshengxiong +// Date: 2024/5/29 +// Description: 音频工具类 + +import 'package:audio_session/audio_session.dart'; + +class AudioUtil { + static Future configAudioSession() async { + final session = await AudioSession.instance; + await session.configure(AudioSessionConfiguration( + avAudioSessionCategory: AVAudioSessionCategory.playAndRecord, + avAudioSessionCategoryOptions: AVAudioSessionCategoryOptions.allowBluetooth | AVAudioSessionCategoryOptions.defaultToSpeaker, + avAudioSessionMode: AVAudioSessionMode.spokenAudio, + avAudioSessionRouteSharingPolicy: AVAudioSessionRouteSharingPolicy.defaultPolicy, + avAudioSessionSetActiveOptions: AVAudioSessionSetActiveOptions.none, + androidAudioAttributes: const AndroidAudioAttributes( + contentType: AndroidAudioContentType.speech, + flags: AndroidAudioFlags.none, + usage: AndroidAudioUsage.voiceCommunication, + ), + androidAudioFocusGainType: AndroidAudioFocusGainType.gain, + androidWillPauseWhenDucked: true, + )); + } +} \ No newline at end of file diff --git a/lib/utils/date_util.dart b/lib/utils/date_util.dart new file mode 100644 index 0000000..38cbf29 --- /dev/null +++ b/lib/utils/date_util.dart @@ -0,0 +1,296 @@ +// Author: fengshengxiong +// Date: 2024/5/11 +// Description: 日期工具类 + +/// 一些常用格式参照。可以自定义格式,例如:'yyyy/MM/dd HH:mm:ss','yyyy/M/d HH:mm:ss'。 +/// 格式要求 +/// year -> yyyy/yy month -> MM/M day -> dd/d +/// hour -> HH/H minute -> mm/m second -> ss/s +class DateFormats { + static String full = 'yyyy-MM-dd HH:mm:ss'; + static String yMoDHM = 'yyyy-MM-dd HH:mm'; + static String yMoD = 'yyyy-MM-dd'; + static String yMo = 'yyyy-MM'; + static String moD = 'MM-dd'; + static String moDHM = 'MM-dd HH:mm'; + static String hMS = 'HH:mm:ss'; + static String hM = 'HH:mm'; + + static String zhFull = 'yyyy年MM月dd日 HH时mm分ss秒'; + static String zhYMoDHM = 'yyyy年MM月dd日 HH时mm分'; + static String zhYMoD = 'yyyy年MM月dd日'; + static String zhYMo = 'yyyy年MM月'; + static String zhMoD = 'MM月dd日'; + static String zhMoDHM = 'MM月dd日 HH时mm分'; + static String zhHMS = 'HH时mm分ss秒'; + static String zhHM = 'HH时mm分'; +} + +class DateUtil { + /// 月->天数. + static Map monthDay = { + 1: 31, + 2: 28, + 3: 31, + 4: 30, + 5: 31, + 6: 30, + 7: 31, + 8: 31, + 9: 30, + 10: 31, + 11: 30, + 12: 31, + }; + + /// 获取当前时间,默认格式(yyyy-MM-dd HH:mm:ss) + static String getNowTimeStr({String? format}) { + return formatDate(DateTime.now(), format: format); + } + + /// 获取当前时间戳 + static int getNowTimestamp() { + return DateTime.now().millisecondsSinceEpoch; + } + + /// 以毫秒为单位格式化日期 + static String formatDateMs(int ms, {bool isUtc = false, String? format}) { + return formatDate(getDateTimeByMs(ms, isUtc: isUtc), format: format); + } + + /// 毫秒 转 DateTime + static DateTime getDateTimeByMs(int ms, {bool isUtc = false}) { + return DateTime.fromMillisecondsSinceEpoch(ms, isUtc: isUtc); + } + + /// 日期str 转 毫秒 + static int? getDateMsByTimeStr(String dateStr, {bool? isUtc}) { + DateTime? dateTime = getDateTime(dateStr, isUtc: isUtc); + return dateTime?.millisecondsSinceEpoch; + } + + /// 按日期str格式化日期 + static String formatDateStr(String dateStr, {bool? isUtc, String? format}) { + return formatDate(getDateTime(dateStr, isUtc: isUtc), format: format); + } + + /// 日期str 转 DateTime + static DateTime? getDateTime(String dateStr, {bool? isUtc}) { + DateTime? dateTime = DateTime.tryParse(dateStr); + if (isUtc != null) { + if (isUtc) { + dateTime = dateTime?.toUtc(); + } else { + dateTime = dateTime?.toLocal(); + } + } + return dateTime; + } + + /// 按DateTime格式化日期 + /// format 转换格式(已提供常用格式 DateFormats,可以自定义格式:'yyyy/MM/dd HH:mm:ss') + /// 格式要求 + /// year -> yyyy/yy month -> MM/M day -> dd/d + /// hour -> HH/H minute -> mm/m second -> ss/s + static String formatDate(DateTime? dateTime, {String? format}) { + if (dateTime == null) return ''; + format = format ?? DateFormats.full; + if (format.contains('yy')) { + String year = dateTime.year.toString(); + if (format.contains('yyyy')) { + format = format.replaceAll('yyyy', year); + } else { + format = format.replaceAll( + 'yy', year.substring(year.length - 2, year.length)); + } + } + + format = _comFormat(dateTime.month, format, 'M', 'MM'); + format = _comFormat(dateTime.day, format, 'd', 'dd'); + format = _comFormat(dateTime.hour, format, 'H', 'HH'); + format = _comFormat(dateTime.minute, format, 'm', 'mm'); + format = _comFormat(dateTime.second, format, 's', 'ss'); + format = _comFormat(dateTime.millisecond, format, 'S', 'SSS'); + + return format; + } + + static String _comFormat(int value, String format, String single, String full) { + if (format.contains(single)) { + if (format.contains(full)) { + format = format.replaceAll(full, value < 10 ? '0$value' : value.toString()); + } else { + format = format.replaceAll(single, value.toString()); + } + } + return format; + } + + /// 工作日 + /// dateTime + /// isUtc + /// languageCode zh or en + /// short + static String getWeekday(DateTime? dateTime, {String languageCode = 'zh', bool short = false}) { + if (dateTime == null) return ''; + String weekday = ''; + switch (dateTime.weekday) { + case 1: + weekday = languageCode == 'zh' ? '星期一' : 'Monday'; + break; + case 2: + weekday = languageCode == 'zh' ? '星期二' : 'Tuesday'; + break; + case 3: + weekday = languageCode == 'zh' ? '星期三' : 'Wednesday'; + break; + case 4: + weekday = languageCode == 'zh' ? '星期四' : 'Thursday'; + break; + case 5: + weekday = languageCode == 'zh' ? '星期五' : 'Friday'; + break; + case 6: + weekday = languageCode == 'zh' ? '星期六' : 'Saturday'; + break; + case 7: + weekday = languageCode == 'zh' ? '星期日' : 'Sunday'; + break; + default: + break; + } + return languageCode == 'zh' + ? (short ? weekday.replaceAll('星期', '周') : weekday) + : weekday.substring(0, short ? 3 : weekday.length); + } + + /// 以毫秒计算工作日 + static String getWeekdayByMs(int milliseconds, {bool isUtc = false, String languageCode = 'zh', bool short = false}) { + DateTime dateTime = getDateTimeByMs(milliseconds, isUtc: isUtc); + return getWeekday(dateTime, languageCode: languageCode, short: short); + } + + /// 在今年的第几天 + static int getDayOfYear(DateTime dateTime) { + int year = dateTime.year; + int month = dateTime.month; + int days = dateTime.day; + for (int i = 1; i < month; i++) { + days = days + monthDay[i]!; + } + if (isLeapYearByYear(year) && month > 2) { + days = days + 1; + } + return days; + } + + /// 在今年的第几天 + static int getDayOfYearByMs(int ms, {bool isUtc = false}) { + return getDayOfYear(DateTime.fromMillisecondsSinceEpoch(ms, isUtc: isUtc)); + } + + /// 是否是当天 + static bool isToday(int? milliseconds, {bool isUtc = false, int? locMs}) { + if (milliseconds == null || milliseconds == 0) return false; + DateTime old = + DateTime.fromMillisecondsSinceEpoch(milliseconds, isUtc: isUtc); + DateTime now; + if (locMs != null) { + now = DateUtil.getDateTimeByMs(locMs); + } else { + now = isUtc ? DateTime.now().toUtc() : DateTime.now().toLocal(); + } + return old.year == now.year && old.month == now.month && old.day == now.day; + } + + /// 是否是昨天. + static bool isYesterday(DateTime dateTime, DateTime locDateTime) { + if (yearIsEqual(dateTime, locDateTime)) { + int spDay = getDayOfYear(locDateTime) - getDayOfYear(dateTime); + return spDay == 1; + } else { + return ((locDateTime.year - dateTime.year == 1) && + dateTime.month == 12 && + locDateTime.month == 1 && + dateTime.day == 31 && + locDateTime.day == 1); + } + } + + /// 是否是昨天 + static bool isYesterdayByMs(int ms, int locMs) { + return isYesterday(DateTime.fromMillisecondsSinceEpoch(ms), + DateTime.fromMillisecondsSinceEpoch(locMs)); + } + + /// 是否是本周 + static bool isWeek(int? ms, {bool isUtc = false, int? locMs}) { + if (ms == null || ms <= 0) { + return false; + } + DateTime oldT = DateTime.fromMillisecondsSinceEpoch(ms, isUtc: isUtc); + DateTime nowT; + if (locMs != null) { + nowT = DateUtil.getDateTimeByMs(locMs, isUtc: isUtc); + } else { + nowT = isUtc ? DateTime.now().toUtc() : DateTime.now().toLocal(); + } + + DateTime old = + nowT.millisecondsSinceEpoch > oldT.millisecondsSinceEpoch ? oldT : nowT; + DateTime now = + nowT.millisecondsSinceEpoch > oldT.millisecondsSinceEpoch ? nowT : oldT; + return (now.weekday >= old.weekday) && + (now.millisecondsSinceEpoch - old.millisecondsSinceEpoch <= + 7 * 24 * 60 * 60 * 1000); + } + + /// 是否同年 + static bool yearIsEqual(DateTime dateTime, DateTime locDateTime) { + return dateTime.year == locDateTime.year; + } + + /// 是否同年 + static bool yearIsEqualByMs(int ms, int locMs) { + return yearIsEqual(DateTime.fromMillisecondsSinceEpoch(ms), + DateTime.fromMillisecondsSinceEpoch(locMs)); + } + + /// 是否是闰年 + static bool isLeapYear(DateTime dateTime) { + return isLeapYearByYear(dateTime.year); + } + + /// 是否是闰年 + static bool isLeapYearByYear(int year) { + return year % 4 == 0 && year % 100 != 0 || year % 400 == 0; + } + + /// 判断两个 DateTime 是否是同一天 + static bool isSameDay(String date1, String date2) { + final dateTime1 = getDateTime(date1); + final dateTime2 = getDateTime(date2); + if(dateTime1?.year == dateTime2?.year && dateTime1?.month == dateTime2?.month && dateTime1?.day == dateTime2?.day) { + return true; + } + return false; + } + + /// 如果如果当前月是12,下月年份需要+1 + static int getNextMonthToYear() { + DateTime dateTime = DateTime.now(); + if(dateTime.month == 12) { + return dateTime.year + 1; + } + return dateTime.year; + } + + /// 获取当前下月 + static int getNextMonth() { + DateTime dateTime = DateTime.now(); + if (dateTime.month == 12) { + return 1; + } + return dateTime.month + 1; + } +} \ No newline at end of file diff --git a/lib/utils/device_info_util.dart b/lib/utils/device_info_util.dart new file mode 100644 index 0000000..d8c3698 --- /dev/null +++ b/lib/utils/device_info_util.dart @@ -0,0 +1,21 @@ +// Author: fengshengxiong +// Date: 2024/5/11 +// Description: 设备信息 + +import 'package:device_info_plus/device_info_plus.dart'; + +class DeviceInfoUtil { + /// 获取当前Android设备的系统版本 + static Future getAndroidSystemVersion() async { + DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + AndroidDeviceInfo androidDeviceInfo = await deviceInfoPlugin.androidInfo; + return androidDeviceInfo.version.sdkInt; + } + + /// 获取当前iOS设备的系统版本 + static Future getIOSSystemVersion() async { + DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + IosDeviceInfo iosDeviceInfo = await deviceInfoPlugin.iosInfo; + return iosDeviceInfo.systemVersion; + } +} diff --git a/lib/utils/file_util.dart b/lib/utils/file_util.dart new file mode 100644 index 0000000..7db249c --- /dev/null +++ b/lib/utils/file_util.dart @@ -0,0 +1,40 @@ +// Author: fengshengxiong +// Date: 2024/6/4 +// Description: 文件工具类 + +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:tone_snap/utils/local_path_util.dart'; +import 'package:tone_snap/utils/log_print.dart'; + +class FileUtil { + /// Assets文件复制到getAssetsDir目录下 + static Future getAssetsToFilePath(String assets) async { + final byteData = await rootBundle.load(assets); + final file = File('${(await LocalPathUtil.getAssetsDir()).path}/${assets.split('/').last}'); + await file.create(recursive: true); + await file.writeAsBytes(byteData.buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes)); + return file.path; + } + + /// 删除目录中的所有文件 + static Future deleteAllFilesInDirectory(Directory directory) async { + if (await directory.exists()) { + final files = directory.listSync(); + + for (var file in files) { + if (file is File) { + try { + await file.delete(); + LogPrint.d('Deleted file: ${file.path}'); + } catch (e) { + LogPrint.d('Error deleting file: ${file.path}, Error: $e'); + } + } + } + } else { + LogPrint.d('Directory does not exist: ${directory.path}'); + } + } +} \ No newline at end of file diff --git a/lib/utils/filesize_util.dart b/lib/utils/filesize_util.dart new file mode 100644 index 0000000..4b6dcf9 --- /dev/null +++ b/lib/utils/filesize_util.dart @@ -0,0 +1,70 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 文件大小单位转换 + +class FileSizeUtil { + /// A method returns a human readable string representing a file fileSize + static String fileSize(dynamic size, [int round = 2]) { + /** + * [size] can be passed as number or as string + * + * the optional parameter [round] specifies the number + * of digits after comma/point (default is 2) + */ + var divider = 1024; + int fileSize; + try { + fileSize = int.parse(size.toString()); + } catch (e) { + throw ArgumentError('Can not parse the size parameter: $e'); + } + + if (fileSize < divider) { + return '$fileSize B'; + } + + if (fileSize < divider * divider && fileSize % divider == 0) { + return '${(fileSize / divider).toStringAsFixed(0)} KB'; + } + + if (fileSize < divider * divider) { + return '${(fileSize / divider).toStringAsFixed(round)} KB'; + } + + if (fileSize < divider * divider * divider && fileSize % divider == 0) { + return '${(fileSize / (divider * divider)).toStringAsFixed(0)} MB'; + } + + if (fileSize < divider * divider * divider) { + return '${(fileSize / divider / divider).toStringAsFixed(round)} MB'; + } + + if (fileSize < divider * divider * divider * divider && fileSize % divider == 0) { + return '${(fileSize / (divider * divider * divider)).toStringAsFixed(0)} GB'; + } + + if (fileSize < divider * divider * divider * divider) { + return '${(fileSize / divider / divider / divider).toStringAsFixed(round)} GB'; + } + + if (fileSize < divider * divider * divider * divider * divider && + fileSize % divider == 0) { + num r = fileSize / divider / divider / divider / divider; + return '${r.toStringAsFixed(0)} TB'; + } + + if (fileSize < divider * divider * divider * divider * divider) { + num r = fileSize / divider / divider / divider / divider; + return '${r.toStringAsFixed(round)} TB'; + } + + if (fileSize < divider * divider * divider * divider * divider * divider && + fileSize % divider == 0) { + num r = fileSize / divider / divider / divider / divider / divider; + return '${r.toStringAsFixed(0)} PB'; + } else { + num r = fileSize / divider / divider / divider / divider / divider; + return '${r.toStringAsFixed(round)} PB'; + } + } +} \ No newline at end of file diff --git a/lib/utils/local_path_util.dart b/lib/utils/local_path_util.dart new file mode 100644 index 0000000..54560e8 --- /dev/null +++ b/lib/utils/local_path_util.dart @@ -0,0 +1,65 @@ +// Author: fengshengxiong +// Date: 2024/5/10 +// Description: 本地路径 + +import 'dart:io'; +import 'package:path_provider/path_provider.dart'; + +class LocalPathUtil { + /// 获取临时目录 + /// `NSCachesDirectory` on iOS and macOS + /// `Context.getCacheDir` on Android. + static Future getTemporaryPath() async { + return await getTemporaryDirectory(); + } + + /// 获取应用支持目录 + /// `NSApplicationSupportDirectory` on iOS and macOS. + /// The Flutter engine's `PathUtils.getFilesDir` API on Android. + static Future getSupportPath() async { + return await getApplicationSupportDirectory(); + } + + /// 获取应用文档目录 + /// `NSDocumentDirectory` on iOS and macOS. + /// The Flutter engine's `PathUtils.getDataDirectory` API on Android. + static Future getDocumentsPath() async { + return await getApplicationDocumentsDirectory(); + } + + /// 获取录音文件保存目录 + static Future getRecordingsDir() async { + Directory cacheDir = await getTemporaryDirectory(); + Directory recordingsDir = Directory('${cacheDir.path}/recordings'); + bool exist = await recordingsDir.exists(); + if (!exist) { + // 若目录不存在,先创建 + await recordingsDir.create(); + } + return recordingsDir; + } + + /// 获取录音文件保存目录 + static Future getAssetsDir() async { + Directory cacheDir = await getTemporaryDirectory(); + Directory recordingsDir = Directory('${cacheDir.path}/assets'); + bool exist = await recordingsDir.exists(); + if (!exist) { + // 若目录不存在,先创建 + await recordingsDir.create(); + } + return recordingsDir; + } + + /// 获取音频文件变声后输出目录 + static Future getVoiceChangeOutputDir() async { + Directory cacheDir = await getTemporaryDirectory(); + Directory recordingsDir = Directory('${cacheDir.path}/change_voice'); + bool exist = await recordingsDir.exists(); + if (!exist) { + // 若目录不存在,先创建 + await recordingsDir.create(); + } + return recordingsDir; + } +} \ No newline at end of file diff --git a/lib/utils/log_print.dart b/lib/utils/log_print.dart new file mode 100644 index 0000000..8374c8b --- /dev/null +++ b/lib/utils/log_print.dart @@ -0,0 +1,50 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 日志打印 + +import 'package:logger/logger.dart'; + +final _logger = Logger( + printer: PrettyPrinter( + // 要显示的方法调用的数量 + methodCount: 0, + // 如果提供了stacktrace,则方法调用的数量 + errorMethodCount: 8, + // 输出的宽度 + lineLength: 120, + // 丰富多彩的日志消息 + colors: true, + // 是否打印表情符号 + printEmojis: true, + // 是否打印时间 + printTime: false, + ), +); + +class LogPrint { + static const String _tag = 'LogPrint'; + + LogPrint.t(dynamic msg, {String tag = _tag}) { + _logger.t('[$tag]: $msg'); + } + + LogPrint.d(dynamic msg, {String tag = _tag}) { + _logger.d('[$tag]: $msg'); + } + + LogPrint.i(dynamic msg, {String tag = _tag}) { + _logger.i('[$tag]: $msg'); + } + + LogPrint.w(dynamic msg, {String tag = _tag}) { + _logger.w('[$tag]: $msg'); + } + + LogPrint.e(dynamic msg, {String tag = _tag}) { + _logger.e('[$tag]: $msg'); + } + + LogPrint.f(dynamic msg, {String tag = _tag}) { + _logger.f('[$tag]: $msg'); + } +} \ No newline at end of file diff --git a/lib/utils/num_util.dart b/lib/utils/num_util.dart new file mode 100644 index 0000000..2cc1f4b --- /dev/null +++ b/lib/utils/num_util.dart @@ -0,0 +1,28 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 整数、浮点数工具类 + +class NumUtil { + static int getInt(num? value) { + if (value == null) return 0; + return value.toInt(); + } + + static double getDouble(num? value) { + if (value == null) return 0.0; + return value.toDouble(); + } + + static String formatNum(double? num, {int index = 2}){ + if (num == null) return '0'; + if((num.toString().length - num.toString().lastIndexOf('.') - 1) < index){ + return num.toStringAsFixed(index).substring(0, num.toString().lastIndexOf('.') + index + 1).toString(); + }else{ + return num.toString().substring(0, num.toString().lastIndexOf('.') + index + 1).toString(); + } + } + + static double strToDouble(String valueStr, {double defValue = 0.0}) { + return double.tryParse(valueStr) ?? defValue; + } +} \ No newline at end of file diff --git a/lib/utils/obj_util.dart b/lib/utils/obj_util.dart new file mode 100644 index 0000000..d1a6fae --- /dev/null +++ b/lib/utils/obj_util.dart @@ -0,0 +1,51 @@ +// Author: fengshengxiong +// Date: 2024/5/7 +// Description: 对象工具类 + +class ObjUtil { + static bool isNotEmptyStr(String? str) { + return str != null && str.trim().isNotEmpty; + } + + static String getStr(String? str) { + return isNotEmptyStr(str) ? str! : ''; + } + + static bool isNotEmptyList(Iterable? list) { + return list != null && list.isNotEmpty; + } + + static bool isNotEmptyMap(Map? map) { + return map != null && map.isNotEmpty; + } + + static bool isEmpty(Object? object) { + if (object == null) return true; + if (object is String && object.trim().isEmpty) { + return true; + } else if (object is Iterable && object.isEmpty) { + return true; + } else if (object is Map && object.isEmpty) { + return true; + } + return false; + } + + static bool isNotEmpty(Object? object) { + return !isEmpty(object); + } + + /// Returns true Two List Is Equal. + static bool twoListIsEqual(List? listA, List? listB) { + if (listA == listB) return true; + if (listA == null || listB == null) return false; + int length = listA.length; + if (length != listB.length) return false; + for (int i = 0; i < length; i++) { + if (!listA.contains(listB[i])) { + return false; + } + } + return true; + } +} \ No newline at end of file diff --git a/lib/utils/permission_util.dart b/lib/utils/permission_util.dart new file mode 100644 index 0000000..6f9c1a8 --- /dev/null +++ b/lib/utils/permission_util.dart @@ -0,0 +1,151 @@ +// Author: fengshengxiong +// Date: 2024/5/10 +// Description: 权限处理 + +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:tone_snap/components/dialog/remind_dialog.dart'; + +class PermissionUtil { + /// 检测是否有权限 + /// [permissionList] 权限申请列表 + static Future checkPermission(List permissionList) async { + // 一个新待申请权限列表 + List newPermissionList = []; + // 遍历当前权限申请列表 + for (Permission permission in permissionList) { + PermissionStatus status = await permission.status; + // 如果不是允许状态就添加到新的申请列表中 + if (!status.isGranted) { + newPermissionList.add(permission); + } + } + + // 如果需要重新申请的列表不是空的 + if (newPermissionList.isNotEmpty) { + PermissionStatus permissionStatus = await _requestPermission(newPermissionList); + switch (permissionStatus) { + // 拒绝状态 + case PermissionStatus.denied: + _showFailedDialog(newPermissionList); + return false; + // 允许状态 + case PermissionStatus.granted: + case PermissionStatus.limited: + case PermissionStatus.provisional: + return true; + // 永久拒绝 活动限制 + case PermissionStatus.restricted: + case PermissionStatus.permanentlyDenied: + _showFailedDialog(newPermissionList, isPermanentlyDenied: true); + break; + } + } else { + return true; + } + return false; + } + + /// 获取新列表中的权限 如果有一项不合格就返回false + static Future _requestPermission(List permissionList) async { + Map statuses = await permissionList.request(); + PermissionStatus currentPermissionStatus = PermissionStatus.granted; + statuses.forEach((key, value) { + if (!value.isGranted || !value.isLimited) { + currentPermissionStatus = value; + return; + } + }); + return currentPermissionStatus; + } + + static Future checkLocationAlways() async { + // 获取前置状态 + // Android没有这一步 ios会先访问这个再访问其他的 + PermissionStatus status = PermissionStatus.granted; + status = await _checkSinglePermission(Permission.locationWhenInUse); + + // 获取第二个状态 + PermissionStatus status2 = PermissionStatus.denied; + + // 如果前置状态为成功才能执行获取第二个状态 + if (status.isGranted) { + status2 = await _checkSinglePermission(Permission.locationAlways); + } + + // 如果两个都成功那么就返回成功 + if (status.isGranted && status2.isGranted) { + return true; + + // 如果有一个拒绝那么就失败了 + } else if (status.isDenied || status2.isDenied) { + _showFailedDialog([Permission.locationWhenInUse, Permission.locationAlways]); + } else { + _showFailedDialog([Permission.locationWhenInUse, Permission.locationAlways], + isPermanentlyDenied: true, + ); + } + return false; + } + + static _checkSinglePermission(Permission permission) async { + // 获取当前状态 + PermissionStatus status = await permission.status; + PermissionStatus currentPermissionStatus = PermissionStatus.granted; + + // 如果它状态不是允许那么就去获取 + if (!status.isGranted) { + currentPermissionStatus = await _requestPermission([permission]); + } + + // 返回最终状态 + return currentPermissionStatus; + } + + /// 权限拒绝后弹窗 + static _showFailedDialog(List permissionList, {bool isPermanentlyDenied = false}) async { + Get.dialog( + barrierDismissible: false, + RemindDialog( + content: await _getInstructions(permissionList), + confirmText: isPermanentlyDenied ? 'Go open' : 'Confirm', + confirmOnTap: () { + if (isPermanentlyDenied) { + openAppSettings(); + } else { + checkPermission(permissionList); + } + }, + ), + ); + } + + /// 获取权限使用说明 + static Future _getInstructions(List permissionList) async { + late Permission failedPermission; + + // 遍历当前权限申请列表 + for (Permission permission in permissionList) { + PermissionStatus status = await permission.status; + + // 如果不是允许状态就添加到新的申请列表中 + if (!status.isGranted || !status.isLimited) { + failedPermission = permission; + break; + } + } + String description = ''; + if (failedPermission == Permission.microphone) { + description = 'We need to access the microphone to record or select audio files.'; + } else if (failedPermission == Permission.storage) { + description = 'We need to access external storage to select audio files.'; + } else if (failedPermission == Permission.audio) { + description = 'We need to access external storage to select audio files.'; + } else if (failedPermission == Permission.photos) { + description = 'We need access to the photo library to pick audio files.'; + } else if (failedPermission == Permission.mediaLibrary) { + description = 'We need to access the device media library to select audio files.'; + } + return description; + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..064018f --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,154 @@ +name: tone_snap +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# 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 is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.2+4 + +environment: + sdk: '>=3.4.1 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +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: ^1.0.8 + + # 状态管理、路由、依赖注入 + get: ^4.6.6 + + # 屏幕和字体大小适配 + flutter_screenutil: ^5.9.3 + + # 下拉刷新、上拉加载 + easy_refresh: ^3.4.0 + + # 显示网络图像并将它们保存在缓存目录中 + cached_network_image: ^3.3.1 + + # 加载、进度、提示框 + flutter_easyloading: ^3.0.5 + + # 查找文件系统上的常用位置 + path_provider: ^2.1.3 + + # 权限处理 + permission_handler: ^11.3.1 + + # 纯 Dart 编写的轻量级且速度极快的键值数据库 + hive: ^2.2.3 + hive_flutter: ^1.1.0 + + # 获取当前设备信息 + device_info_plus: ^10.1.0 + + # 查询应用程序包信息 + package_info_plus: ^8.0.0 + + # 日志打印 + logger: ^2.3.0 + + # webview + webview_flutter: ^4.8.0 + + # 音频 + audio_session: ^0.1.19 + flutter_sound: ^9.4.18 + just_audio: ^0.9.38 +# ffmpeg_kit_flutter: ^6.0.3 + ffmpeg_kit_flutter_audio: 6.0.3-LTS + + # 文件选择 + file_picker: ^8.0.3 + + # 跑马灯 + widget_marquee: ^0.0.8 + +flutter_launcher_icons: + android: "launcher_icon" + ios: true + image_path: "assets/icon/app_icon.jpeg" + min_sdk_android: 23 # android min sdk min:16, default 21 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^4.0.0 + + hive_generator: ^2.0.1 + build_runner: ^2.4.10 + flutter_launcher_icons: ^0.13.1 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # 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: + - assets/icon/ + - assets/change_voice/ + - assets/change_voice/images/ + - assets/change_voice/images/2.0x/ + - assets/change_voice/images/3.0x/ + - assets/change_voice/audio/ + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/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.dev/custom-fonts/#from-packages \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..1e5e2fc --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,29 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. 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:tone_snap/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const 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); + }); +}