commit 91b7eebbf21c1364728202f6e7c071cbb90356c4 Author: fengshengxiong Date: Thu Jan 22 16:34:55 2026 +0800 接入TopON diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4386e7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ +*.md +*.txt +about.txt +about_zh.txt + + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..3d40ea9 --- /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: "d693b4b9dbac2acd4477aea4555ca6dcbea44ba2" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 + base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 + - platform: android + create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 + base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 + - platform: ios + create_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 + base_revision: d693b4b9dbac2acd4477aea4555ca6dcbea44ba2 + + # 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/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..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..43fa7a1 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +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") +} + +android { + namespace = "com.example.aesthetica_wallpaper" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.aesthetica_wallpaper" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +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..0a7b208 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/aesthetica_wallpaper/MainActivity.kt b/android/app/src/main/kotlin/com/example/aesthetica_wallpaper/MainActivity.kt new file mode 100644 index 0000000..cfbe573 --- /dev/null +++ b/android/app/src/main/kotlin/com/example/aesthetica_wallpaper/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.aesthetica_wallpaper + +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..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..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-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-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-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-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/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + 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..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + 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.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..b9b0d23 --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -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..ac3b479 --- /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-8.12-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..fb605bc --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + 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 "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/assets/images/abstract/abstract1.png b/assets/images/abstract/abstract1.png new file mode 100644 index 0000000..7c50531 Binary files /dev/null and b/assets/images/abstract/abstract1.png differ diff --git a/assets/images/abstract/abstract10.png b/assets/images/abstract/abstract10.png new file mode 100644 index 0000000..095549c Binary files /dev/null and b/assets/images/abstract/abstract10.png differ diff --git a/assets/images/abstract/abstract11.png b/assets/images/abstract/abstract11.png new file mode 100644 index 0000000..4498a85 Binary files /dev/null and b/assets/images/abstract/abstract11.png differ diff --git a/assets/images/abstract/abstract12.png b/assets/images/abstract/abstract12.png new file mode 100644 index 0000000..34ab7c6 Binary files /dev/null and b/assets/images/abstract/abstract12.png differ diff --git a/assets/images/abstract/abstract13.png b/assets/images/abstract/abstract13.png new file mode 100644 index 0000000..4929bd3 Binary files /dev/null and b/assets/images/abstract/abstract13.png differ diff --git a/assets/images/abstract/abstract14.png b/assets/images/abstract/abstract14.png new file mode 100644 index 0000000..3917dea Binary files /dev/null and b/assets/images/abstract/abstract14.png differ diff --git a/assets/images/abstract/abstract15.png b/assets/images/abstract/abstract15.png new file mode 100644 index 0000000..a6f1068 Binary files /dev/null and b/assets/images/abstract/abstract15.png differ diff --git a/assets/images/abstract/abstract2.png b/assets/images/abstract/abstract2.png new file mode 100644 index 0000000..5fe2e6e Binary files /dev/null and b/assets/images/abstract/abstract2.png differ diff --git a/assets/images/abstract/abstract3.png b/assets/images/abstract/abstract3.png new file mode 100644 index 0000000..9f9b577 Binary files /dev/null and b/assets/images/abstract/abstract3.png differ diff --git a/assets/images/abstract/abstract4.png b/assets/images/abstract/abstract4.png new file mode 100644 index 0000000..3ccebf2 Binary files /dev/null and b/assets/images/abstract/abstract4.png differ diff --git a/assets/images/abstract/abstract5.png b/assets/images/abstract/abstract5.png new file mode 100644 index 0000000..3f61f1a Binary files /dev/null and b/assets/images/abstract/abstract5.png differ diff --git a/assets/images/abstract/abstract6.png b/assets/images/abstract/abstract6.png new file mode 100644 index 0000000..d9b0df5 Binary files /dev/null and b/assets/images/abstract/abstract6.png differ diff --git a/assets/images/abstract/abstract7.png b/assets/images/abstract/abstract7.png new file mode 100644 index 0000000..ea8ddf9 Binary files /dev/null and b/assets/images/abstract/abstract7.png differ diff --git a/assets/images/abstract/abstract8.png b/assets/images/abstract/abstract8.png new file mode 100644 index 0000000..b5ff0c1 Binary files /dev/null and b/assets/images/abstract/abstract8.png differ diff --git a/assets/images/abstract/abstract9.png b/assets/images/abstract/abstract9.png new file mode 100644 index 0000000..f1e4346 Binary files /dev/null and b/assets/images/abstract/abstract9.png differ diff --git a/assets/images/aesthloco.png b/assets/images/aesthloco.png new file mode 100644 index 0000000..178efdc Binary files /dev/null and b/assets/images/aesthloco.png differ diff --git a/assets/images/animals/animals1.png b/assets/images/animals/animals1.png new file mode 100644 index 0000000..0a284c4 Binary files /dev/null and b/assets/images/animals/animals1.png differ diff --git a/assets/images/animals/animals2.png b/assets/images/animals/animals2.png new file mode 100644 index 0000000..cd54b04 Binary files /dev/null and b/assets/images/animals/animals2.png differ diff --git a/assets/images/animals/animals3.png b/assets/images/animals/animals3.png new file mode 100644 index 0000000..db7c793 Binary files /dev/null and b/assets/images/animals/animals3.png differ diff --git a/assets/images/animals/animals4.png b/assets/images/animals/animals4.png new file mode 100644 index 0000000..30031d9 Binary files /dev/null and b/assets/images/animals/animals4.png differ diff --git a/assets/images/animals/animals5.png b/assets/images/animals/animals5.png new file mode 100644 index 0000000..8b72182 Binary files /dev/null and b/assets/images/animals/animals5.png differ diff --git a/assets/images/animals/animals6.png b/assets/images/animals/animals6.png new file mode 100644 index 0000000..22f4890 Binary files /dev/null and b/assets/images/animals/animals6.png differ diff --git a/assets/images/animals/animals7.png b/assets/images/animals/animals7.png new file mode 100644 index 0000000..6203723 Binary files /dev/null and b/assets/images/animals/animals7.png differ diff --git a/assets/images/architecture/architecture1.png b/assets/images/architecture/architecture1.png new file mode 100644 index 0000000..43b9a35 Binary files /dev/null and b/assets/images/architecture/architecture1.png differ diff --git a/assets/images/architecture/architecture10.png b/assets/images/architecture/architecture10.png new file mode 100644 index 0000000..ecb89fb Binary files /dev/null and b/assets/images/architecture/architecture10.png differ diff --git a/assets/images/architecture/architecture2.png b/assets/images/architecture/architecture2.png new file mode 100644 index 0000000..701f3b4 Binary files /dev/null and b/assets/images/architecture/architecture2.png differ diff --git a/assets/images/architecture/architecture3.png b/assets/images/architecture/architecture3.png new file mode 100644 index 0000000..105bfd9 Binary files /dev/null and b/assets/images/architecture/architecture3.png differ diff --git a/assets/images/architecture/architecture4.png b/assets/images/architecture/architecture4.png new file mode 100644 index 0000000..d2cb160 Binary files /dev/null and b/assets/images/architecture/architecture4.png differ diff --git a/assets/images/architecture/architecture5.png b/assets/images/architecture/architecture5.png new file mode 100644 index 0000000..fb13f3e Binary files /dev/null and b/assets/images/architecture/architecture5.png differ diff --git a/assets/images/architecture/architecture6.png b/assets/images/architecture/architecture6.png new file mode 100644 index 0000000..ce885ec Binary files /dev/null and b/assets/images/architecture/architecture6.png differ diff --git a/assets/images/architecture/architecture7.png b/assets/images/architecture/architecture7.png new file mode 100644 index 0000000..87559bc Binary files /dev/null and b/assets/images/architecture/architecture7.png differ diff --git a/assets/images/architecture/architecture8.png b/assets/images/architecture/architecture8.png new file mode 100644 index 0000000..64aff4e Binary files /dev/null and b/assets/images/architecture/architecture8.png differ diff --git a/assets/images/architecture/architecture9.png b/assets/images/architecture/architecture9.png new file mode 100644 index 0000000..b69ef42 Binary files /dev/null and b/assets/images/architecture/architecture9.png differ diff --git a/assets/images/food/food1.png b/assets/images/food/food1.png new file mode 100644 index 0000000..bd02095 Binary files /dev/null and b/assets/images/food/food1.png differ diff --git a/assets/images/food/food10.png b/assets/images/food/food10.png new file mode 100644 index 0000000..1a5b023 Binary files /dev/null and b/assets/images/food/food10.png differ diff --git a/assets/images/food/food11.png b/assets/images/food/food11.png new file mode 100644 index 0000000..6d57cce Binary files /dev/null and b/assets/images/food/food11.png differ diff --git a/assets/images/food/food12.png b/assets/images/food/food12.png new file mode 100644 index 0000000..5ff17d7 Binary files /dev/null and b/assets/images/food/food12.png differ diff --git a/assets/images/food/food13.png b/assets/images/food/food13.png new file mode 100644 index 0000000..e5c5afb Binary files /dev/null and b/assets/images/food/food13.png differ diff --git a/assets/images/food/food2.png b/assets/images/food/food2.png new file mode 100644 index 0000000..ee97fb4 Binary files /dev/null and b/assets/images/food/food2.png differ diff --git a/assets/images/food/food3.png b/assets/images/food/food3.png new file mode 100644 index 0000000..85ce492 Binary files /dev/null and b/assets/images/food/food3.png differ diff --git a/assets/images/food/food4.png b/assets/images/food/food4.png new file mode 100644 index 0000000..8e395bc Binary files /dev/null and b/assets/images/food/food4.png differ diff --git a/assets/images/food/food5.png b/assets/images/food/food5.png new file mode 100644 index 0000000..e668278 Binary files /dev/null and b/assets/images/food/food5.png differ diff --git a/assets/images/food/food6.png b/assets/images/food/food6.png new file mode 100644 index 0000000..671f77c Binary files /dev/null and b/assets/images/food/food6.png differ diff --git a/assets/images/food/food7.png b/assets/images/food/food7.png new file mode 100644 index 0000000..51eacb7 Binary files /dev/null and b/assets/images/food/food7.png differ diff --git a/assets/images/food/food8.png b/assets/images/food/food8.png new file mode 100644 index 0000000..44f24db Binary files /dev/null and b/assets/images/food/food8.png differ diff --git a/assets/images/food/food9.png b/assets/images/food/food9.png new file mode 100644 index 0000000..f18e84b Binary files /dev/null and b/assets/images/food/food9.png differ diff --git a/assets/images/nature/nature1.png b/assets/images/nature/nature1.png new file mode 100644 index 0000000..d0216fa Binary files /dev/null and b/assets/images/nature/nature1.png differ diff --git a/assets/images/nature/nature2.png b/assets/images/nature/nature2.png new file mode 100644 index 0000000..c131948 Binary files /dev/null and b/assets/images/nature/nature2.png differ diff --git a/assets/images/nature/nature3.png b/assets/images/nature/nature3.png new file mode 100644 index 0000000..ffdaefe Binary files /dev/null and b/assets/images/nature/nature3.png differ diff --git a/assets/images/nature/nature4.png b/assets/images/nature/nature4.png new file mode 100644 index 0000000..c9732a5 Binary files /dev/null and b/assets/images/nature/nature4.png differ diff --git a/assets/images/nature/nature5.png b/assets/images/nature/nature5.png new file mode 100644 index 0000000..886935e Binary files /dev/null and b/assets/images/nature/nature5.png differ diff --git a/assets/images/nature/nature6.png b/assets/images/nature/nature6.png new file mode 100644 index 0000000..258f4de Binary files /dev/null and b/assets/images/nature/nature6.png differ diff --git a/assets/images/nature/nature7.png b/assets/images/nature/nature7.png new file mode 100644 index 0000000..464bb2a Binary files /dev/null and b/assets/images/nature/nature7.png differ diff --git a/assets/images/nature/nature8.png b/assets/images/nature/nature8.png new file mode 100644 index 0000000..e97dd00 Binary files /dev/null and b/assets/images/nature/nature8.png differ diff --git a/assets/images/nature/nature9.png b/assets/images/nature/nature9.png new file mode 100644 index 0000000..d1f77f1 Binary files /dev/null and b/assets/images/nature/nature9.png differ diff --git a/assets/images/travel/travel1.png b/assets/images/travel/travel1.png new file mode 100644 index 0000000..e78f8ed Binary files /dev/null and b/assets/images/travel/travel1.png differ diff --git a/assets/images/travel/travel2.png b/assets/images/travel/travel2.png new file mode 100644 index 0000000..c509253 Binary files /dev/null and b/assets/images/travel/travel2.png differ diff --git a/assets/images/travel/travel3.png b/assets/images/travel/travel3.png new file mode 100644 index 0000000..304ba96 Binary files /dev/null and b/assets/images/travel/travel3.png differ diff --git a/assets/images/travel/travel4.png b/assets/images/travel/travel4.png new file mode 100644 index 0000000..24c1fc6 Binary files /dev/null and b/assets/images/travel/travel4.png differ diff --git a/assets/images/travel/travel5.png b/assets/images/travel/travel5.png new file mode 100644 index 0000000..c33e179 Binary files /dev/null and b/assets/images/travel/travel5.png differ diff --git a/assets/images/travel/travel6.png b/assets/images/travel/travel6.png new file mode 100644 index 0000000..6022f51 Binary files /dev/null and b/assets/images/travel/travel6.png differ diff --git a/assets/images/travel/travel7.png b/assets/images/travel/travel7.png new file mode 100644 index 0000000..7b27975 Binary files /dev/null and b/assets/images/travel/travel7.png differ diff --git a/assets/images/travel/travel8.png b/assets/images/travel/travel8.png new file mode 100644 index 0000000..49871c0 Binary files /dev/null and b/assets/images/travel/travel8.png differ diff --git a/assets/images/travel/travel9.png b/assets/images/travel/travel9.png new file mode 100644 index 0000000..7cf4393 Binary files /dev/null and b/assets/images/travel/travel9.png differ diff --git a/assets/manifest.json b/assets/manifest.json new file mode 100644 index 0000000..e69de29 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..1dc6cf7 --- /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 + 13.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..3971c53 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,47 @@ +# Uncomment this line to define a global platform for your project +platform :ios, '15.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! + + 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| + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)','PERMISSION_PHOTOS=1',] + end + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..c69f688 --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,450 @@ +PODS: + - Ads-Global (7.7.0.4): + - Ads-Global/BUAdSDK (= 7.7.0.4) + - Ads-Global/BUAdSDK (7.7.0.4): + - Ads-Global/PangleSDK + - Ads-Global/TikTokBusinessSDK + - Ads-Global/PangleSDK (7.7.0.4) + - Ads-Global/TikTokBusinessSDK (7.7.0.4) + - app_tracking_transparency (0.0.1): + - Flutter + - AppLovinSDK (13.5.0) + - audioplayers_darwin (0.0.1): + - Flutter + - battery_plus (1.0.0): + - Flutter + - BidMachine (3.5.0): + - BidMachine/Static (= 3.5.0) + - BidMachine/Static (3.5.0): + - StackModules/Static (~> 3.4.0) + - BigoADS (5.0.0) + - ChartboostMediationAdapterChartboost (4.9.8.0.1): + - ChartboostMediationSDK (~> 4.0) + - ChartboostSDK (~> 9.8.0) + - ChartboostMediationSDK (4.9.0.1) + - ChartboostSDK (9.8.1) + - Firebase/CoreOnly (12.8.0): + - FirebaseCore (~> 12.8.0) + - Firebase/Crashlytics (12.8.0): + - Firebase/CoreOnly + - FirebaseCrashlytics (~> 12.8.0) + - firebase_analytics (12.1.1): + - firebase_core + - FirebaseAnalytics (= 12.8.0) + - Flutter + - firebase_core (4.4.0): + - Firebase/CoreOnly (= 12.8.0) + - Flutter + - firebase_crashlytics (5.0.7): + - Firebase/Crashlytics (= 12.8.0) + - firebase_core + - Flutter + - FirebaseAnalytics (12.8.0): + - FirebaseAnalytics/Default (= 12.8.0) + - FirebaseCore (~> 12.8.0) + - FirebaseInstallations (~> 12.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - FirebaseAnalytics/Default (12.8.0): + - FirebaseCore (~> 12.8.0) + - FirebaseInstallations (~> 12.8.0) + - GoogleAppMeasurement/Default (= 12.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - FirebaseCore (12.8.0): + - FirebaseCoreInternal (~> 12.8.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - FirebaseCoreExtension (12.8.0): + - FirebaseCore (~> 12.8.0) + - FirebaseCoreInternal (12.8.0): + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - FirebaseCrashlytics (12.8.0): + - FirebaseCore (~> 12.8.0) + - FirebaseInstallations (~> 12.8.0) + - FirebaseRemoteConfigInterop (~> 12.8.0) + - FirebaseSessions (~> 12.8.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/Environment (~> 8.1) + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - FirebaseInstallations (12.8.0): + - FirebaseCore (~> 12.8.0) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - PromisesObjC (~> 2.4) + - FirebaseRemoteConfigInterop (12.8.0) + - FirebaseSessions (12.8.0): + - FirebaseCore (~> 12.8.0) + - FirebaseCoreExtension (~> 12.8.0) + - FirebaseInstallations (~> 12.8.0) + - GoogleDataTransport (~> 10.1) + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/UserDefaults (~> 8.1) + - nanopb (~> 3.30910.0) + - PromisesSwift (~> 2.1) + - Flutter (1.0.0) + - Fyber_Marketplace_SDK (8.3.7) + - geolocator_apple (1.2.0): + - Flutter + - FlutterMacOS + - GoogleAdsOnDeviceConversion (3.2.0): + - GoogleUtilities/Environment (~> 8.1) + - GoogleUtilities/Logger (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/Core (12.8.0): + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/Default (12.8.0): + - GoogleAdsOnDeviceConversion (~> 3.2.0) + - GoogleAppMeasurement/Core (= 12.8.0) + - GoogleAppMeasurement/IdentitySupport (= 12.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleAppMeasurement/IdentitySupport (12.8.0): + - GoogleAppMeasurement/Core (= 12.8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 8.1) + - GoogleUtilities/MethodSwizzler (~> 8.1) + - GoogleUtilities/Network (~> 8.1) + - "GoogleUtilities/NSData+zlib (~> 8.1)" + - nanopb (~> 3.30910.0) + - GoogleDataTransport (10.1.0): + - nanopb (~> 3.30910.0) + - PromisesObjC (~> 2.4) + - GoogleUtilities/AppDelegateSwizzler (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (8.1.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Logger (8.1.0): + - GoogleUtilities/Environment + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/Network (8.1.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (8.1.0)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (8.1.0) + - GoogleUtilities/Reachability (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (8.1.0): + - GoogleUtilities/Logger + - GoogleUtilities/Privacy + - image_gallery_saver (2.0.2): + - Flutter + - InMobiSDK (10.8.6) + - IronSourceAdQualitySDK (7.26.2) + - IronSourceSDK (8.11.0.0): + - IronSourceSDK/AdQuality (= 8.11.0.0) + - IronSourceSDK/Ads (= 8.11.0.0) + - IronSourceSDK/AdQuality (8.11.0.0): + - IronSourceAdQualitySDK (~> 7.26.1) + - IronSourceSDK/Ads (8.11.0.0) + - kk_device_infos (0.0.1): + - Flutter + - MintegralAdSDK/All (7.7.9): + - MintegralAdSDK/BannerAd + - MintegralAdSDK/BidNativeAd + - MintegralAdSDK/InterstitialVideoAd + - MintegralAdSDK/NativeAd + - MintegralAdSDK/NativeAdvancedAd + - MintegralAdSDK/NewInterstitialAd + - MintegralAdSDK/RewardVideoAd + - MintegralAdSDK/SplashAd + - MintegralAdSDK/BannerAd (7.7.9): + - MintegralAdSDK/NativeAd + - MintegralAdSDK/BidNativeAd (7.7.9): + - MintegralAdSDK/NativeAd + - MintegralAdSDK/InterstitialVideoAd (7.7.9): + - MintegralAdSDK/NativeAd + - MintegralAdSDK/NativeAd (7.7.9) + - MintegralAdSDK/NativeAdvancedAd (7.7.9): + - MintegralAdSDK/NativeAd + - MintegralAdSDK/NewInterstitialAd (7.7.9): + - MintegralAdSDK/InterstitialVideoAd + - MintegralAdSDK/NativeAd + - MintegralAdSDK/RewardVideoAd (7.7.9): + - MintegralAdSDK/NativeAd + - MintegralAdSDK/SplashAd (7.7.9): + - MintegralAdSDK/NativeAd + - nanopb (3.30910.0): + - nanopb/decode (= 3.30910.0) + - nanopb/encode (= 3.30910.0) + - nanopb/decode (3.30910.0) + - nanopb/encode (3.30910.0) + - OMSDK_Appodeal/Static (1.6.0.3) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - permission_handler_apple (9.3.0): + - Flutter + - PromisesObjC (2.4.0) + - PromisesSwift (2.4.0): + - PromisesObjC (= 2.4.0) + - secmtp_sdk (1.0.8): + - Flutter + - TPNApplovinSDKAdapter (= 6.4.93.1) + - TPNBidMachineSDKAdapter (= 6.4.93.8) + - TPNBigoSDKAdapter (= 6.4.93.1) + - TPNChartboostSDKAdapter (= 6.4.93) + - TPNFyberSDKAdapter (= 6.4.93) + - TPNInmobiSDKAdapter (= 6.4.93) + - TPNiOS (= 6.4.93) + - TPNIronSourceSDKAdapter (= 6.4.93.1) + - TPNMintegralSDKAdapter (= 6.4.93) + - TPNPangleSDKAdapter (= 6.4.93.4) + - TPNStartAppSDKAdapter (= 6.4.93) + - TPNUnityAdsSDKAdapter (= 6.4.93) + - TPNVungleSDKAdapter (= 6.4.93) + - share_plus (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - StackModules/Core-Static (3.4.7) + - StackModules/ProductPresentation-Static (3.4.7): + - StackModules/Core-Static + - StackModules/Rendering-Static (3.4.7): + - OMSDK_Appodeal/Static (= 1.6.0.3) + - StackModules/Core-Static + - StackModules/ProductPresentation-Static + - StackModules/Static (3.4.7): + - StackModules/Core-Static + - StackModules/ProductPresentation-Static + - StackModules/Rendering-Static + - StartAppSDK (4.10.4) + - TPNApplovinSDKAdapter (6.4.93.1): + - AppLovinSDK (= 13.5.0) + - TPNiOS (= 6.4.93) + - TPNBidMachineSDKAdapter (6.4.93.8): + - BidMachine (= 3.5.0) + - TPNiOS (= 6.4.93) + - TPNBigoSDKAdapter (6.4.93.1): + - BigoADS (= 5.0.0) + - TPNiOS (= 6.4.93) + - TPNChartboostSDKAdapter (6.4.93): + - ChartboostMediationAdapterChartboost (= 4.9.8.0.1) + - ChartboostMediationSDK (= 4.9.0.1) + - ChartboostSDK (= 9.8.1) + - TPNiOS (= 6.4.93) + - TPNFyberSDKAdapter (6.4.93): + - Fyber_Marketplace_SDK (= 8.3.7) + - TPNiOS (= 6.4.93) + - TPNInmobiSDKAdapter (6.4.93): + - InMobiSDK (= 10.8.6) + - TPNiOS (= 6.4.93) + - TPNiOS (6.4.93): + - TPNiOS/TPNSDK (= 6.4.93) + - TPNiOS/TPNSDK (6.4.93) + - TPNIronSourceSDKAdapter (6.4.93.1): + - IronSourceSDK (= 8.11.0) + - TPNiOS (= 6.4.93) + - TPNMintegralSDKAdapter (6.4.93): + - MintegralAdSDK/All (= 7.7.9) + - TPNiOS (= 6.4.93) + - TPNPangleSDKAdapter (6.4.93.4): + - Ads-Global (= 7.7.0.4) + - TPNiOS (= 6.4.93) + - TPNStartAppSDKAdapter (6.4.93): + - StartAppSDK (= 4.10.4) + - TPNiOS (= 6.4.93) + - TPNUnityAdsSDKAdapter (6.4.93): + - TPNiOS (= 6.4.93) + - UnityAds (= 4.16.0) + - TPNVungleSDKAdapter (6.4.93): + - TPNiOS (= 6.4.93) + - VungleAds (= 7.5.3) + - UnityAds (4.16.0) + - url_launcher_ios (0.0.1): + - Flutter + - VungleAds (7.5.3) + +DEPENDENCIES: + - app_tracking_transparency (from `.symlinks/plugins/app_tracking_transparency/ios`) + - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`) + - battery_plus (from `.symlinks/plugins/battery_plus/ios`) + - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) + - firebase_core (from `.symlinks/plugins/firebase_core/ios`) + - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) + - Flutter (from `Flutter`) + - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) + - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) + - kk_device_infos (from `.symlinks/plugins/kk_device_infos/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - secmtp_sdk (from `.symlinks/plugins/secmtp_sdk/ios`) + - share_plus (from `.symlinks/plugins/share_plus/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + +SPEC REPOS: + trunk: + - Ads-Global + - AppLovinSDK + - BidMachine + - BigoADS + - ChartboostMediationAdapterChartboost + - ChartboostMediationSDK + - ChartboostSDK + - Firebase + - FirebaseAnalytics + - FirebaseCore + - FirebaseCoreExtension + - FirebaseCoreInternal + - FirebaseCrashlytics + - FirebaseInstallations + - FirebaseRemoteConfigInterop + - FirebaseSessions + - Fyber_Marketplace_SDK + - GoogleAdsOnDeviceConversion + - GoogleAppMeasurement + - GoogleDataTransport + - GoogleUtilities + - InMobiSDK + - IronSourceAdQualitySDK + - IronSourceSDK + - MintegralAdSDK + - nanopb + - OMSDK_Appodeal + - PromisesObjC + - PromisesSwift + - StackModules + - StartAppSDK + - TPNApplovinSDKAdapter + - TPNBidMachineSDKAdapter + - TPNBigoSDKAdapter + - TPNChartboostSDKAdapter + - TPNFyberSDKAdapter + - TPNInmobiSDKAdapter + - TPNiOS + - TPNIronSourceSDKAdapter + - TPNMintegralSDKAdapter + - TPNPangleSDKAdapter + - TPNStartAppSDKAdapter + - TPNUnityAdsSDKAdapter + - TPNVungleSDKAdapter + - UnityAds + - VungleAds + +EXTERNAL SOURCES: + app_tracking_transparency: + :path: ".symlinks/plugins/app_tracking_transparency/ios" + audioplayers_darwin: + :path: ".symlinks/plugins/audioplayers_darwin/ios" + battery_plus: + :path: ".symlinks/plugins/battery_plus/ios" + firebase_analytics: + :path: ".symlinks/plugins/firebase_analytics/ios" + firebase_core: + :path: ".symlinks/plugins/firebase_core/ios" + firebase_crashlytics: + :path: ".symlinks/plugins/firebase_crashlytics/ios" + Flutter: + :path: Flutter + geolocator_apple: + :path: ".symlinks/plugins/geolocator_apple/darwin" + image_gallery_saver: + :path: ".symlinks/plugins/image_gallery_saver/ios" + kk_device_infos: + :path: ".symlinks/plugins/kk_device_infos/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + secmtp_sdk: + :path: ".symlinks/plugins/secmtp_sdk/ios" + share_plus: + :path: ".symlinks/plugins/share_plus/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + +SPEC CHECKSUMS: + Ads-Global: 777ec343f5d0ca54bb0b4b74c285363065bb771a + app_tracking_transparency: 3d84f147f67ca82d3c15355c36b1fa6b66ca7c92 + AppLovinSDK: bf8974163120910e6b902e9610e7c5a2c0f577b6 + audioplayers_darwin: ccf9c770ee768abb07e26d90af093f7bab1c12ab + battery_plus: b42253f6d2dde71712f8c36fef456d99121c5977 + BidMachine: 7722254fdf6641a354ceef845bcb6cc3ff8dc0f3 + BigoADS: 789ca51394adb1adb405e7e01ec8e43d3dc14f15 + ChartboostMediationAdapterChartboost: 35f88bacf5669be31a354ccfac29e66f95258ec4 + ChartboostMediationSDK: e0976ce3765d6734b96b55cec00c653a38159f9d + ChartboostSDK: 5d387aeccf2748cd71c0b033e6c2cad4a558b308 + Firebase: 9a58fdbc9d8655ed7b79a19cf9690bb007d3d46d + firebase_analytics: b5a19eaf3e4bf4187b0815ef4850b8916e2bc549 + firebase_core: ee30637e6744af8e0c12a6a1e8a9718506ec2398 + firebase_crashlytics: 28b8f39df8104131376393e6af658b8b77dd120f + FirebaseAnalytics: f20bbad8cb7f65d8a5eaefeb424ae8800a31bdfc + FirebaseCore: 0dbad74bda10b8fb9ca34ad8f375fb9dd3ebef7c + FirebaseCoreExtension: 6605938d51f765d8b18bfcafd2085276a252bee2 + FirebaseCoreInternal: fe5fa466aeb314787093a7dce9f0beeaad5a2a21 + FirebaseCrashlytics: fb31c6907e5b52aa252668394d3f1ab326df1511 + FirebaseInstallations: 6a14ab3d694ebd9f839c48d330da5547e9ca9dc0 + FirebaseRemoteConfigInterop: 869ddca16614f979e5c931ece11fbb0b8729ed41 + FirebaseSessions: d614ca154c63dbbc6c10d6c38259c2162c4e7c9b + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + Fyber_Marketplace_SDK: 99ff1d44bf0c9b4e0aa3e2f2bd8c7956fc5c5675 + geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e + GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f + GoogleAppMeasurement: 72c9a682fec6290327ea5e3c4b829b247fcb2c17 + GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 + GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + image_gallery_saver: 14711d79da40581063e8842a11acf1969d781ed7 + InMobiSDK: a6e7bfbecb698c83183f42c5a3cfa1ac52f16bff + IronSourceAdQualitySDK: 03888a0ac60e1f24ff6277672c3184f00813af64 + IronSourceSDK: 32eeb199f608b590112a8263028a485e50f8d1b7 + kk_device_infos: 44a194dc1105227cc5c904a1b24ac375f3506870 + MintegralAdSDK: 054814f99bb7e967b8974fe635d9005b225cbe42 + nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + OMSDK_Appodeal: 907fdbc78ac0c6dfccb14b69a81302899dc536c9 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 + secmtp_sdk: 32d43e33e0d2024882ce10b0063260ed3ee2d258 + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + StackModules: 53abb3789ff65603857a7157efaac07e860cb34d + StartAppSDK: dacab2f76b584fa2c04c25b65fb7a84f394c7030 + TPNApplovinSDKAdapter: 84ae11eae89859cebc13fc7072b0a5e1e458553d + TPNBidMachineSDKAdapter: 306941ea610758ae6587f9278beba69423a835c3 + TPNBigoSDKAdapter: ed618310d108cd775425194743919fd6ba002391 + TPNChartboostSDKAdapter: 9b5e40753670c64483a81b554525985dfe4b20d0 + TPNFyberSDKAdapter: c543cb4b21ccfba36d3195fec99f75a8fcfaea65 + TPNInmobiSDKAdapter: 6942e9076af9e2e29321b675628ad60068692e3f + TPNiOS: 1c5f1a4a449e1e6483dffd3d6fef44dd41aa33e9 + TPNIronSourceSDKAdapter: 37ccf8b61c371ae7068132192937aec2d31b5f1e + TPNMintegralSDKAdapter: dbaa2e5a1a1aeca587821682488d7bce240e2c7b + TPNPangleSDKAdapter: 85bc0c1bc8e95ad24b721c6fc579a5334aed6393 + TPNStartAppSDKAdapter: 6a181c10be1c7e00c31e9b9bfe0a5e5eb3cf5fee + TPNUnityAdsSDKAdapter: 7c4c10fc95d580f067a664d5bdd8ccd14023980d + TPNVungleSDKAdapter: 89ac6e8d8ef77f1650af22d8781c549fb3bcc3c9 + UnityAds: 8c53660f12855f5f4da100f28709c3d2b4b83dbe + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + VungleAds: 3b273eba0219680dbef90d51e690d165422702d9 + +PODFILE CHECKSUM: 3e495731a82c0a856598430706848d68f801b8ce + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..788e818 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,789 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 29D577EF1C20787AFFBD55D2 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 139775EE4C0CCB8428AEC7C0 /* Pods_RunnerTests.framework */; }; + 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 */; }; + 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 */; }; + D3C4A78EE83796EF19A9A827 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D8FD41EB32E5520BE468F42A /* Pods_Runner.framework */; }; + D8DB3CDA2F15F3A90037F54E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D8DB3CD92F15F3A90037F54E /* GoogleService-Info.plist */; }; + D8DB3CDC2F1620070037F54E /* aesthloco.png in Resources */ = {isa = PBXBuildFile; fileRef = D8DB3CDB2F1620070037F54E /* aesthloco.png */; }; +/* 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 */ + 097693E14D036A1C9382946C /* 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 = ""; }; + 139775EE4C0CCB8428AEC7C0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 229421E64B47798CD5AD79C0 /* 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 = ""; }; + 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 = ""; }; + 4C30B3C60BDD9DEC6311372C /* 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 = ""; }; + 65DF362144A074F35EFF3C6C /* 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 = ""; }; + 8E3857F6557FD8D43FED4DE1 /* 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 = ""; }; + B2B3953A8FEAFCE12F7BFD32 /* 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 = ""; }; + D8DB3CD92F15F3A90037F54E /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; + D8DB3CDB2F1620070037F54E /* aesthloco.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = aesthloco.png; sourceTree = ""; }; + D8FD41EB32E5520BE468F42A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D3C4A78EE83796EF19A9A827 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D6D26CCCA4FFD767B9E02542 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 29D577EF1C20787AFFBD55D2 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0A98A17D3641BCB6A16E9C3F /* Frameworks */ = { + isa = PBXGroup; + children = ( + D8FD41EB32E5520BE468F42A /* Pods_Runner.framework */, + 139775EE4C0CCB8428AEC7C0 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + 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 */, + A966534FA742ACD17E020549 /* Pods */, + 0A98A17D3641BCB6A16E9C3F /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + D8DB3CDB2F1620070037F54E /* aesthloco.png */, + D8DB3CD92F15F3A90037F54E /* GoogleService-Info.plist */, + 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 = ""; + }; + A966534FA742ACD17E020549 /* Pods */ = { + isa = PBXGroup; + children = ( + B2B3953A8FEAFCE12F7BFD32 /* Pods-Runner.debug.xcconfig */, + 8E3857F6557FD8D43FED4DE1 /* Pods-Runner.release.xcconfig */, + 4C30B3C60BDD9DEC6311372C /* Pods-Runner.profile.xcconfig */, + 097693E14D036A1C9382946C /* Pods-RunnerTests.debug.xcconfig */, + 65DF362144A074F35EFF3C6C /* Pods-RunnerTests.release.xcconfig */, + 229421E64B47798CD5AD79C0 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + EF9AD35D40F6C6249921098C /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + D6D26CCCA4FFD767B9E02542 /* 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 = ( + 9617CB07735FE04E5F47B8C4 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 2982C83D1A861B4B4C173667 /* [CP] Embed Pods Frameworks */, + 40DADDD725C98DC4C3ECB1A5 /* [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" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + preferredProjectObjectVersion = 77; + 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 */, + D8DB3CDA2F15F3A90037F54E /* GoogleService-Info.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + D8DB3CDC2F1620070037F54E /* aesthloco.png in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2982C83D1A861B4B4C173667 /* [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; + }; + 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"; + }; + 40DADDD725C98DC4C3ECB1A5 /* [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; + }; + 9617CB07735FE04E5F47B8C4 /* [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; + }; + 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"; + }; + EF9AD35D40F6C6249921098C /* [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; + }; +/* 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 = 13.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*]" = 544LFS79WN; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.moodCanvasWalls.moodCanvasWalls; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "MoodCanvas:Walls"; + 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 = 097693E14D036A1C9382946C /* 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.example.aestheticaWallpaper.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 = 65DF362144A074F35EFF3C6C /* 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.example.aestheticaWallpaper.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 = 229421E64B47798CD5AD79C0 /* 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.example.aestheticaWallpaper.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 = 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; + 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 = 13.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 = 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 = 13.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*]" = 544LFS79WN; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.moodCanvasWalls.moodCanvasWalls; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "MoodCanvas:Walls"; + 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*]" = 544LFS79WN; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.moodCanvasWalls.moodCanvasWalls; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "MoodCanvas:Walls"; + 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..e3773d4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..6266644 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Flutter +import UIKit + +@main +@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..c68df94 --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,120 @@ +{ + "images": [ + { + "size": "20x20", + "idiom": "universal", + "filename": "icon-20@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "20x20", + "idiom": "universal", + "filename": "icon-20@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "29x29", + "idiom": "universal", + "filename": "icon-29@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "29x29", + "idiom": "universal", + "filename": "icon-29@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "38x38", + "idiom": "universal", + "filename": "icon-38@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "38x38", + "idiom": "universal", + "filename": "icon-38@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "40x40", + "idiom": "universal", + "filename": "icon-40@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "40x40", + "idiom": "universal", + "filename": "icon-40@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "60x60", + "idiom": "universal", + "filename": "icon-60@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "60x60", + "idiom": "universal", + "filename": "icon-60@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "64x64", + "idiom": "universal", + "filename": "icon-64@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "64x64", + "idiom": "universal", + "filename": "icon-64@3x.png", + "scale": "3x", + "platform": "ios" + }, + { + "size": "68x68", + "idiom": "universal", + "filename": "icon-68@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "76x76", + "idiom": "universal", + "filename": "icon-76@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "83.5x83.5", + "idiom": "universal", + "filename": "icon-83.5@2x.png", + "scale": "2x", + "platform": "ios" + }, + { + "size": "1024x1024", + "idiom": "universal", + "filename": "icon-1024.png", + "scale": "1x", + "platform": "ios" + } + ], + "info": { + "version": 1, + "author": "icon.wuruihong.com" + } +} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png new file mode 100644 index 0000000..ae43a52 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-1024.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png new file mode 100644 index 0000000..5309c72 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png new file mode 100644 index 0000000..8edb98d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png new file mode 100644 index 0000000..b66d069 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png new file mode 100644 index 0000000..8120404 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png new file mode 100644 index 0000000..64d5043 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png new file mode 100644 index 0000000..e92b098 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-38@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png new file mode 100644 index 0000000..f88fe1c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png new file mode 100644 index 0000000..eb101de Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png new file mode 100644 index 0000000..eb101de Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png new file mode 100644 index 0000000..e4dd8a8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png new file mode 100644 index 0000000..e0aab10 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png new file mode 100644 index 0000000..4519301 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-64@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png new file mode 100644 index 0000000..1da5d7e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-68@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png new file mode 100644 index 0000000..431d99b Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png new file mode 100644 index 0000000..79e3982 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/icon-83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..922d7f6 --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist new file mode 100644 index 0000000..5611534 --- /dev/null +++ b/ios/Runner/GoogleService-Info.plist @@ -0,0 +1,30 @@ + + + + + API_KEY + AIzaSyALflIdpAN8W4jdK73icGUFatyGH_cdOQo + GCM_SENDER_ID + 859494319187 + PLIST_VERSION + 1 + BUNDLE_ID + com.moodCanvasWalls.moodCanvasWalls + PROJECT_ID + moodcanvas-31621 + STORAGE_BUCKET + moodcanvas-31621.firebasestorage.app + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:859494319187:ios:59519b2bc6238b6c5572a6 + + \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..92e9417 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,852 @@ + + + + +LSApplicationQueriesSchemes + + taobao + pinduoduo + openapp.jdmobile + imeituan + iosamap + alipay + baiduboxapp + vipshop + tmall + meituanwaimai + kwai + eleme + xhsdiscover + ksnebula + sinaweibo + fleamarket + id6443575749 + com.pwrd.zhuxian2.zs + baiduboxlite + wireless1688 + iqiyi + weixin + taobaotravel + alipays + youku + + SKAdNetworkItems + + + SKAdNetworkIdentifier + 4w7y6s5ca2.skadnetwork + + + SKAdNetworkIdentifier + 44jx6755aq.skadnetwork + + + SKAdNetworkIdentifier + 9rd848q2bz.skadnetwork + + + SKAdNetworkIdentifier + tl55sbb4fm.skadnetwork + + + SKAdNetworkIdentifier + 9t245vhmpl.skadnetwork + + + SKAdNetworkIdentifier + a8cz6cu7e5.skadnetwork + + + SKAdNetworkIdentifier + 424m5254lk.skadnetwork + + + SKAdNetworkIdentifier + wg4vff78zm.skadnetwork + + + SKAdNetworkIdentifier + n9x2a789qt.skadnetwork + + + SKAdNetworkIdentifier + s39g8k73mm.skadnetwork + + + SKAdNetworkIdentifier + prcb7njmu6.skadnetwork + + + SKAdNetworkIdentifier + 238da6jt44.skadnetwork + + + SKAdNetworkIdentifier + mlmmfzh3r3.skadnetwork + + + SKAdNetworkIdentifier + mj797d8u6f.skadnetwork + + + SKAdNetworkIdentifier + vhf287vqwu.skadnetwork + + + SKAdNetworkIdentifier + 5l3tpt7t6e.skadnetwork + + + SKAdNetworkIdentifier + hs6bdukanm.skadnetwork + + + SKAdNetworkIdentifier + wzmmz9fp6w.skadnetwork + + + SKAdNetworkIdentifier + f38h382jlk.skadnetwork + + + SKAdNetworkIdentifier + glqzh8vgby.skadnetwork + + + SKAdNetworkIdentifier + 5a6flpkh64.skadnetwork + + + SKAdNetworkIdentifier + 294l99pt4k.skadnetwork + + + SKAdNetworkIdentifier + c6k4g5qg8m.skadnetwork + + + SKAdNetworkIdentifier + 4fzdc2evr5.skadnetwork + + + SKAdNetworkIdentifier + x44k69ngh6.skadnetwork + + + SKAdNetworkIdentifier + p78axxw29g.skadnetwork + + + SKAdNetworkIdentifier + k6y4y55b64.skadnetwork + + + SKAdNetworkIdentifier + 5f5u5tfb26.skadnetwork + + + SKAdNetworkIdentifier + 7ug5zh24hu.skadnetwork + + + SKAdNetworkIdentifier + f7s53z58qe.skadnetwork + + + SKAdNetworkIdentifier + 488r3q3dtq.skadnetwork + + + SKAdNetworkIdentifier + t38b2kh725.skadnetwork + + + SKAdNetworkIdentifier + e5fvkxwrpn.skadnetwork + + + SKAdNetworkIdentifier + 3qy4746246.skadnetwork + + + SKAdNetworkIdentifier + 32z4fx6l9h.skadnetwork + + + SKAdNetworkIdentifier + 9nlqeag3gk.skadnetwork + + + SKAdNetworkIdentifier + m8dbw4sv7c.skadnetwork + + + SKAdNetworkIdentifier + 97r2b46745.skadnetwork + + + SKAdNetworkIdentifier + 4pfyvq9l8r.skadnetwork + + + SKAdNetworkIdentifier + w9q455wk68.skadnetwork + + + SKAdNetworkIdentifier + yclnxrl5pm.skadnetwork + + + SKAdNetworkIdentifier + zmvfpc5aq8.skadnetwork + + + SKAdNetworkIdentifier + xga6mpmplv.skadnetwork + + + SKAdNetworkIdentifier + 5tjdwbrq8w.skadnetwork + + + SKAdNetworkIdentifier + cstr6suwn9.skadnetwork + + + SKAdNetworkIdentifier + 5lm9lj6jb7.skadnetwork + + + SKAdNetworkIdentifier + 22mmun2rn5.skadnetwork + + + SKAdNetworkIdentifier + ppxm28t8ap.skadnetwork + + + SKAdNetworkIdentifier + f73kdq92p3.skadnetwork + + + SKAdNetworkIdentifier + v9wttpbfk9.skadnetwork + + + SKAdNetworkIdentifier + 3sh42y64q3.skadnetwork + + + SKAdNetworkIdentifier + v79kvwwj4g.skadnetwork + + + SKAdNetworkIdentifier + ydx93a7ass.skadnetwork + + + SKAdNetworkIdentifier + k674qkevps.skadnetwork + + + SKAdNetworkIdentifier + av6w8kgt66.skadnetwork + + + SKAdNetworkIdentifier + 578prtvx9j.skadnetwork + + + SKAdNetworkIdentifier + uw77j35x4d.skadnetwork + + + SKAdNetworkIdentifier + 8s468mfl3y.skadnetwork + + + SKAdNetworkIdentifier + pwa73g5rt2.skadnetwork + + + SKAdNetworkIdentifier + 3rd42ekr43.skadnetwork + + + SKAdNetworkIdentifier + 4dzt52r2t5.skadnetwork + + + SKAdNetworkIdentifier + feyaarzu9v.skadnetwork + + + SKAdNetworkIdentifier + g6gcrrvk4p.skadnetwork + + + SKAdNetworkIdentifier + lr83yxwka7.skadnetwork + + + SKAdNetworkIdentifier + 2u9pt9hc89.skadnetwork + + + SKAdNetworkIdentifier + v72qych5uu.skadnetwork + + + SKAdNetworkIdentifier + mqn7fxpca7.skadnetwork + + + SKAdNetworkIdentifier + kbd757ywx3.skadnetwork + + + SKAdNetworkIdentifier + 2fnua5tdw4.skadnetwork + + + SKAdNetworkIdentifier + 6yxyv74ff7.skadnetwork + + + SKAdNetworkIdentifier + klf5c3l5u5.skadnetwork + + + SKAdNetworkIdentifier + mp6xlyr22a.skadnetwork + + + SKAdNetworkIdentifier + 4468km3ulz.skadnetwork + + + SKAdNetworkIdentifier + a2p9lx4jpn.skadnetwork + + + SKAdNetworkIdentifier + zq492l623r.skadnetwork + + + SKAdNetworkIdentifier + mls7yz5dvl.skadnetwork + + + SKAdNetworkIdentifier + cg4yq2srnc.skadnetwork + + + SKAdNetworkIdentifier + 737z793b9f.skadnetwork + + + SKAdNetworkIdentifier + 6xzpu9s2p8.skadnetwork + + + SKAdNetworkIdentifier + ludvb6z3bs.skadnetwork + + + SKAdNetworkIdentifier + 523jb4fst2.skadnetwork + + + SKAdNetworkIdentifier + ggvn48r87g.skadnetwork + + + SKAdNetworkIdentifier + 24t9a8vw3c.skadnetwork + + + SKAdNetworkIdentifier + cj5566h2ga.skadnetwork + + + SKAdNetworkIdentifier + 7rz58n8ntl.skadnetwork + + + SKAdNetworkIdentifier + ejvt5qm6ak.skadnetwork + + + SKAdNetworkIdentifier + dzg6xy7pwj.skadnetwork + + + SKAdNetworkIdentifier + y45688jllp.skadnetwork + + + SKAdNetworkIdentifier + hdw39hrw9y.skadnetwork + + + SKAdNetworkIdentifier + mtkv5xtk9e.skadnetwork + + + SKAdNetworkIdentifier + gta9lk7p23.skadnetwork + + + SKAdNetworkIdentifier + g28c52eehv.skadnetwork + + + SKAdNetworkIdentifier + su67r6k2v3.skadnetwork + + + SKAdNetworkIdentifier + rx5hdcabgc.skadnetwork + + + SKAdNetworkIdentifier + xy9t38ct57.skadnetwork + + + SKAdNetworkIdentifier + 54nzkqm89y.skadnetwork + + + SKAdNetworkIdentifier + 9b89h5y424.skadnetwork + + + SKAdNetworkIdentifier + 79pbpufp6p.skadnetwork + + + SKAdNetworkIdentifier + kbmxgpxpgc.skadnetwork + + + SKAdNetworkIdentifier + 275upjj5gd.skadnetwork + + + SKAdNetworkIdentifier + rvh3l7un93.skadnetwork + + + SKAdNetworkIdentifier + qqp299437r.skadnetwork + + + SKAdNetworkIdentifier + 74b6s63p6l.skadnetwork + + + SKAdNetworkIdentifier + 44n7hlldy6.skadnetwork + + + SKAdNetworkIdentifier + 6p4ks3rnbw.skadnetwork + + + SKAdNetworkIdentifier + 3qcr597p9d.skadnetwork + + + SKAdNetworkIdentifier + n6fk4nfna4.skadnetwork + + + SKAdNetworkIdentifier + b9bk5wbcq9.skadnetwork + + + SKAdNetworkIdentifier + 84993kbrcf.skadnetwork + + + SKAdNetworkIdentifier + 24zw6aqk47.skadnetwork + + + SKAdNetworkIdentifier + pwdxu55a5a.skadnetwork + + + SKAdNetworkIdentifier + cs644xg564.skadnetwork + + + SKAdNetworkIdentifier + 6964rsfnh4.skadnetwork + + + SKAdNetworkIdentifier + 9vvzujtq5s.skadnetwork + + + SKAdNetworkIdentifier + a7xqa6mtl2.skadnetwork + + + SKAdNetworkIdentifier + r45fhb6rf7.skadnetwork + + + SKAdNetworkIdentifier + c3frkrj4fj.skadnetwork + + + SKAdNetworkIdentifier + 6g9af3uyq4.skadnetwork + + + SKAdNetworkIdentifier + u679fj5vs4.skadnetwork + + + SKAdNetworkIdentifier + g2y4y55b64.skadnetwork + + + SKAdNetworkIdentifier + dbu4b84rxf.skadnetwork + + + SKAdNetworkIdentifier + ns5j362hk7.skadnetwork + + + SKAdNetworkIdentifier + 252b5q8x7y.skadnetwork + + + SKAdNetworkIdentifier + 7fmhfwg9en.skadnetwork + + + SKAdNetworkIdentifier + cwn433xbcr.skadnetwork + + + SKAdNetworkIdentifier + 7953jerfzd.skadnetwork + + + SKAdNetworkIdentifier + qu637u8glc.skadnetwork + + + SKAdNetworkIdentifier + 9yg77x724h.skadnetwork + + + SKAdNetworkIdentifier + n66cz3y3bx.skadnetwork + + + SKAdNetworkIdentifier + z959bm4gru.skadnetwork + + + SKAdNetworkIdentifier + s69wq72ugq.skadnetwork + + + SKAdNetworkIdentifier + x8uqf25wch.skadnetwork + + + SKAdNetworkIdentifier + bxvub5ada5.skadnetwork + + + SKAdNetworkIdentifier + krvm3zuq6h.skadnetwork + + + SKAdNetworkIdentifier + 8r8llnkz5a.skadnetwork + + + SKAdNetworkIdentifier + 47vhws6wlr.skadnetwork + + + SKAdNetworkIdentifier + 8c4e2ghe7u.skadnetwork + + + SKAdNetworkIdentifier + cp8zw746q7.skadnetwork + + + SKAdNetworkIdentifier + ecpz2srf59.skadnetwork + + + SKAdNetworkIdentifier + eh6m2bh4zr.skadnetwork + + + SKAdNetworkIdentifier + n38lu8286q.skadnetwork + + + SKAdNetworkIdentifier + v4nxqhlyqp.skadnetwork + + + SKAdNetworkIdentifier + vutu7akeur.skadnetwork + + + SKAdNetworkIdentifier + y5ghdn5j9k.skadnetwork + + + SKAdNetworkIdentifier + 3l6bd9hu43.skadnetwork + + + SKAdNetworkIdentifier + 52fl2v3hgk.skadnetwork + + + SKAdNetworkIdentifier + 6v7lgmsu45.skadnetwork + + + SKAdNetworkIdentifier + 89z7zv988g.skadnetwork + + + SKAdNetworkIdentifier + 8m87ys6875.skadnetwork + + + SKAdNetworkIdentifier + bvpn9ufa9b.skadnetwork + + + SKAdNetworkIdentifier + gvmwg8q7h5.skadnetwork + + + SKAdNetworkIdentifier + hb56zgv37p.skadnetwork + + + SKAdNetworkIdentifier + hjevpa356n.skadnetwork + + + SKAdNetworkIdentifier + m297p6643m.skadnetwork + + + SKAdNetworkIdentifier + m5mvw97r93.skadnetwork + + + SKAdNetworkIdentifier + nzq8sh4pbs.skadnetwork + + + SKAdNetworkIdentifier + pu4na253f3.skadnetwork + + + SKAdNetworkIdentifier + vcra2ehyfk.skadnetwork + + + SKAdNetworkIdentifier + yrqqpx2mcb.skadnetwork + + + SKAdNetworkIdentifier + z4gj7hsk7h.skadnetwork + + + SKAdNetworkIdentifier + 4mn522wn87.skadnetwork + + + SKAdNetworkIdentifier + dkc879ngq3.skadnetwork + + + SKAdNetworkIdentifier + gta8lk7p23.skadnetwork + + + SKAdNetworkIdentifier + x5l83yy675.skadnetwork + + + SKAdNetworkIdentifier + x8jxxk4ff5.skadnetwork + + + SKAdNetworkIdentifier + z24wtl6j62.skadnetwork + + + SKAdNetworkIdentifier + 9g2aggbj52.skadnetwork + + + SKAdNetworkIdentifier + h65wbv5k3f.skadnetwork + + + SKAdNetworkIdentifier + t6d3zquu66.skadnetwork + + + SKAdNetworkIdentifier + tvvz7th9br.skadnetwork + + + SKAdNetworkIdentifier + nu4557a4je.skadnetwork + + + SKAdNetworkIdentifier + 55644vm79v.skadnetwork + + + SKAdNetworkIdentifier + w7jznl3r6g.skadnetwork + + + SKAdNetworkIdentifier + 577p5t736z.skadnetwork + + + SKAdNetworkIdentifier + 6rd35atwn8.skadnetwork + + + SKAdNetworkIdentifier + 7bxrt786m8.skadnetwork + + + SKAdNetworkIdentifier + 7fbxrn65az.skadnetwork + + + SKAdNetworkIdentifier + ce8ybjwass.skadnetwork + + + SKAdNetworkIdentifier + dt3cjx1a9i.skadnetwork + + + SKAdNetworkIdentifier + fz2k2k5tej.skadnetwork + + + SKAdNetworkIdentifier + jk2fsx2rgz.skadnetwork + + + SKAdNetworkIdentifier + ln5gz23vtd.skadnetwork + + + SKAdNetworkIdentifier + r8lj5b58b5.skadnetwork + + + SKAdNetworkIdentifier + tmhh9296z4.skadnetwork + + + SKAdNetworkIdentifier + qwpu75vrh2.skadnetwork + + + SKAdNetworkIdentifier + 55y65gfgn7.skadnetwork + + + SKAdNetworkIdentifier + fq6vru337s.skadnetwork + + + SKAdNetworkIdentifier + 87u5trcl3r.skadnetwork + + + SKAdNetworkIdentifier + bd757ywx3.skadnetwork + + + SKAdNetworkIdentifier + 33r6p7g8nc.skadnetwork + + + SKAdNetworkIdentifier + g69uk9uh2b.skadnetwork + + + NSUserTrackingUsageDescription + Obtain tagging permissions to provide you with better, safer, personalized services and content. We will not use it for other purposes without consent. Once enabled, you can also go to the system Settings-Privacy; to turn it off at any time. + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + MoodCanvas + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + MoodCanvas + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.1 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + NSLocationAlwaysAndWhenInUseUsageDescription + This application needs to access your location to get local weather information. + NSLocationWhenInUseUsageDescription + This application needs to access your location to get local weather information. + NSPhotoLibraryAddUsageDescription + This application requires access to your photo album to save your edited wallpapers. + NSPhotoLibraryUsageDescription + This application requires access to your photo album to save your edited wallpapers. + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + + + 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/Runner/aesthloco.png b/ios/Runner/aesthloco.png new file mode 100644 index 0000000..178efdc Binary files /dev/null and b/ios/Runner/aesthloco.png differ 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/core/app.dart b/lib/core/app.dart new file mode 100644 index 0000000..414f924 --- /dev/null +++ b/lib/core/app.dart @@ -0,0 +1,46 @@ +import 'package:aesthetica_wallpaper/screens/home/gallery_screen.dart'; +import 'package:aesthetica_wallpaper/screens/editor/editor_screen.dart'; +import 'package:aesthetica_wallpaper/screens/recipe/recipe_screen.dart'; +import 'package:aesthetica_wallpaper/widgets/main_screen.dart'; +import 'package:flutter/material.dart'; + +import 'home_start_page.dart'; + +class AestheticaApp extends StatelessWidget { + final NavigatorObserver analyObserver; + const AestheticaApp({super.key,required this.analyObserver}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'MoodCanvas:Walls', + navigatorObservers: [analyObserver], + // App 主题,设置为深色模式 + theme: ThemeData( + brightness: Brightness.dark, + primaryColor: Colors.blueGrey[900], + scaffoldBackgroundColor: Colors.black, + fontFamily: 'Lato', + appBarTheme: AppBarTheme( + backgroundColor: Colors.grey[900], + elevation: 0, + ), + bottomNavigationBarTheme: BottomNavigationBarThemeData( + backgroundColor: Colors.black, + selectedItemColor: Colors.pinkAccent, + unselectedItemColor: Colors.grey[600], + type: BottomNavigationBarType.fixed, + ), + ), + debugShowCheckedModeBanner: false, + // App 路由 + initialRoute: '/', + routes: { + '/': (context) => HomeStartPage(), + '/gallery': (context) => const GalleryScreen(), + '/editor': (context) => const EditorScreen(), + '/recipes': (context) => const RecipeScreen(), + }, + ); + } +} diff --git a/lib/core/app_ads_tools.dart b/lib/core/app_ads_tools.dart new file mode 100644 index 0000000..1ce3487 --- /dev/null +++ b/lib/core/app_ads_tools.dart @@ -0,0 +1,256 @@ +import 'dart:async'; +import 'package:flutter/foundation.dart'; +import 'package:secmtp_sdk/at_init.dart'; +import 'package:secmtp_sdk/at_interstitial.dart'; +import 'package:secmtp_sdk/at_interstitial_response.dart'; +import 'package:secmtp_sdk/at_listener.dart'; + + +// --- 1. 使用枚举定义广告位,更安全、清晰 --- +enum AdPlacement { interstitial1, interstitial2, interstitial3 } + +/// 广告管理器 +class AppAdsTools { + // --- 单例实现 --- + static final AppAdsTools _instance = AppAdsTools._internal(); + + AppAdsTools._internal(); + + static AppAdsTools get instance => _instance; + + // --- 广告 ID 配置 --- + static const String _appId = 'h69718907a25ad'; + static const String _appKey = 'a5a35545d00d0e9c5471c354dd24edd03'; + + static final Map _adUnitIds = { + AdPlacement.interstitial1: "n6971892e19211", + AdPlacement.interstitial2: "n6971892f2da1f", + AdPlacement.interstitial3: "n697189366ab4f", + }; + + + // --- 内部状态变量 --- + bool _isSdkInitialized = false; + Completer? _initCompleter; + final Set _loadingAds = {}; + bool _isAdShowing = false; + final Map _adClosedCallbacks = {}; + + // --- 开屏广告专用回调和状态 --- + Function(AdPlacement adPlacement)? _onSplashAdReadyCallback; + Function()? _onSplashAdAllFailedCallback; + Set _splashAdsToLoad = {}; + int _splashAdsFailedCount = 0; + bool _isHandlingSplashAd = false; + + // 广告冷却逻辑 + final bool _useGlobalCooldown = true; + static const String _globalCooldownKey = 'any_interstitial_ad_was_shown'; + final Map _lastShownTimestamps = {}; + final int _adCooldownSeconds = 15; + + /// 初始化TopOn SDK和广告监听 + Future init() { + if (_initCompleter != null) { + return _initCompleter!.future; + } + _initCompleter = Completer(); + + debugPrint("【TopOnAdManager】SDK 开始初始化..."); + ATInitManger.setLogEnabled(logEnabled: false); + ATInitManger.initAnyThinkSDK(appidStr: _appId, appidkeyStr: _appKey).then(( + _, + ) { + debugPrint("【TopOnAdManager】SDK 初始化成功。"); + _isSdkInitialized = true; + _setupEventListeners(); + _initCompleter!.complete(); + }); + + return _initCompleter!.future; + } + + /// 统一配置插屏广告监听器 + void _setupEventListeners() { + ATListenerManager.interstitialEventHandler.listen((event) { + final placement = _getPlacementForAdUnitId(event.placementID); + if (placement == null) return; + + switch (event.interstatus) { + case InterstitialStatus.interstitialAdDidFinishLoading: + _loadingAds.remove(event.placementID); + debugPrint("【TopOnAdManager】✅ 广告加载成功: ${placement.name}"); + + if (_isHandlingSplashAd && + _splashAdsToLoad.contains(placement) && + _onSplashAdReadyCallback != null) { + _onSplashAdReadyCallback!(placement); + _clearSplashCallbacks(); + } + break; + + case InterstitialStatus.interstitialAdFailToLoadAD: + _loadingAds.remove(event.placementID); + debugPrint( + "【TopOnAdManager】❌ 广告加载失败: ${placement.name}, 原因: ${event.requestMessage}", + ); + + if (_isHandlingSplashAd && + _splashAdsToLoad.contains(placement) && + _onSplashAdAllFailedCallback != null) { + _splashAdsFailedCount++; + if (_splashAdsFailedCount >= _splashAdsToLoad.length) { + _onSplashAdAllFailedCallback!(); + _clearSplashCallbacks(); + } + } + break; + + case InterstitialStatus.interstitialAdDidClose: + case InterstitialStatus.interstitialFailedToShow: + debugPrint("【TopOnAdManager】广告已关闭或展示失败: ${placement.name}"); + _isAdShowing = false; + _adClosedCallbacks[event.placementID]?.call(); + _adClosedCallbacks.remove(event.placementID); + preloadAd(placement); + break; + + case InterstitialStatus.interstitialDidShowSucceed: + _isAdShowing = true; + debugPrint("【TopOnAdManager】广告开始显示: ${placement.name}"); + final keyToUpdate = _useGlobalCooldown + ? _globalCooldownKey + : event.placementID; + _lastShownTimestamps[keyToUpdate] = DateTime.now(); + debugPrint("【TopOnAdManager】冷却计时器已更新 (Key: $keyToUpdate)"); + break; + + default: + break; + } + }); + } + + /// 加载开屏广告的专用方法 + void loadInitialSplashAd({ + required Function(AdPlacement adPlacement) onAdReady, + required Function() onAllAdsFailed, + }) { + if (!_isSdkInitialized) { + debugPrint("【TopOnAdManager】SDK 未初始化,无法加载开屏广告。"); + onAllAdsFailed(); + return; + } + _onSplashAdReadyCallback = onAdReady; + _onSplashAdAllFailedCallback = onAllAdsFailed; + _splashAdsFailedCount = 0; + _isHandlingSplashAd = true; + + _splashAdsToLoad = { + AdPlacement.interstitial1, + AdPlacement.interstitial2, + AdPlacement.interstitial3, + }; + + debugPrint("【TopOnAdManager】开始并行加载初始开屏广告..."); + for (final placement in _splashAdsToLoad) { + _loadInterstitialAd(placement); + } + } + + /// 清除开屏广告回调的私有方法 + void _clearSplashCallbacks() { + _onSplashAdReadyCallback = null; + _onSplashAdAllFailedCallback = null; + _splashAdsToLoad.clear(); + _splashAdsFailedCount = 0; + _isHandlingSplashAd = false; + } + + /// 公共方法:展示插屏广告 + Future showAd(AdPlacement placement, {VoidCallback? onAdClosed}) async { + if (_isAdShowing) { + debugPrint("【TopOnAdManager】已有广告正在显示,无法展示新广告: ${placement.name}"); + return false; + } + + if (!_isSdkInitialized) { + debugPrint("【TopOnAdManager】SDK 未初始化,无法展示广告。"); + return false; + } + final adUnitId = _adUnitIds[placement]!; + + final keyToCheck = _useGlobalCooldown ? _globalCooldownKey : adUnitId; + final lastShown = _lastShownTimestamps[keyToCheck]; + if (lastShown != null && + DateTime.now().difference(lastShown) < + Duration(seconds: _adCooldownSeconds)) { + debugPrint("【TopOnAdManager】❌ 广告冷却中 (${_adCooldownSeconds}s)。放弃展示。"); + return false; + } + + bool isReady = await ATInterstitialManager.hasInterstitialAdReady( + placementID: adUnitId, + ); + debugPrint("【TopOnAdManager】检查广告就绪状态 ${placement.name}: $isReady"); + + if (isReady) { + if (onAdClosed != null) { + _adClosedCallbacks[adUnitId] = onAdClosed; + } + ATInterstitialManager.showSceneInterstitialAd( + placementID: adUnitId, + sceneID: _getSceneIdForPlacement(placement), + ); + return true; + } else { + debugPrint("【TopOnAdManager】广告 ${placement.name} 尚未准备好展示。"); + preloadAd(placement); // 第一次调用show时,如果没准备好,会自动触发加载 + return false; + } + } + + /// 预加载广告 + void preloadAd(AdPlacement placement) { + _loadInterstitialAd(placement); + } + + /// 内部方法:加载单个插屏广告 + void _loadInterstitialAd(AdPlacement placement) { + final adUnitId = _adUnitIds[placement]!; + if (!_isSdkInitialized) { + debugPrint("【TopOnAdManager】SDK 未初始化,无法加载广告: ${placement.name}"); + return; + } + if (_loadingAds.contains(adUnitId)) { + debugPrint("【TopOnAdManager】广告 ${placement.name} 正在加载中,跳过本次请求"); + return; + } + _loadingAds.add(adUnitId); + debugPrint("【TopOnAdManager】请求加载插屏广告: ${placement.name}"); + ATInterstitialManager.loadInterstitialAd( + placementID: adUnitId, + extraMap: const {}, + ); + } + + // --- 辅助方法 --- + AdPlacement? _getPlacementForAdUnitId(String adUnitId) { + for (var entry in _adUnitIds.entries) { + if (entry.value == adUnitId) return entry.key; + } + return null; + } + + String _getSceneIdForPlacement(AdPlacement placement) { + switch (placement) { + case AdPlacement.interstitial1: + return 'interstitial1'; + case AdPlacement.interstitial2: + return 'interstitial2'; + case AdPlacement.interstitial3: + return 'interstitial3'; + + } + } +} diff --git a/lib/core/firebase_optinons.dart b/lib/core/firebase_optinons.dart new file mode 100644 index 0000000..2dab2fb --- /dev/null +++ b/lib/core/firebase_optinons.dart @@ -0,0 +1,33 @@ +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' show defaultTargetPlatform, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + + default: + throw UnsupportedError('Unsupported platform'); + } + } + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'YOUR_ANDROID_API_KEY', + appId: 'YOUR_ANDROID_APP_ID', + messagingSenderId: 'YOUR_ANDROID_MESSAGING_SENDER_ID', + projectId: 'YOUR_PROJECT_ID', + storageBucket: 'YOUR_STORAGE_BUCKET', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyALflIdpAN8W4jdK73icGUFatyGH_cdOQo', + appId: '1:859494319187:ios:59519b2bc6238b6c5572a6', + messagingSenderId: '859494319187', + projectId: 'moodcanvas-31621', + storageBucket: 'moodcanvas-31621.firebasestorage.app', + iosBundleId: 'com.moodCanvasWalls.moodCanvasWalls', + ); +} diff --git a/lib/core/home_start_page.dart b/lib/core/home_start_page.dart new file mode 100644 index 0000000..ba943fd --- /dev/null +++ b/lib/core/home_start_page.dart @@ -0,0 +1,204 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:aesthetica_wallpaper/core/app_ads_tools.dart'; +import 'package:flutter/material.dart'; +import 'package:app_tracking_transparency/app_tracking_transparency.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; + +import '../widgets/main_screen.dart'; + + + + +class HomeStartPage extends StatefulWidget { + HomeStartPage({Key? key}) : super(key: key); + + @override + State createState() => _HomeStartPageState(); +} + +class _HomeStartPageState extends State with WidgetsBindingObserver { + + Timer? _timeoutTimer; + bool _isNavigating = false; + + // 使用这个标志位来防止并发请求,而不是彻底锁死 + bool _isRequestingATT = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((_) { + _initPermissionsAndSDK(); + }); + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + _timeoutTimer?.cancel(); + super.dispose(); + } + + /// 监听生命周期:这是通过审核的关键“补救”措施 + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state == AppLifecycleState.resumed) { + // 审核场景:审核员启动App -> 立即切后台 -> 再切回来 + // 此时如果 ATT 还没弹出来,我们需要检查并再次触发 + _checkATTOnResume(); + } + } + + /// 回到前台时的检查逻辑 + Future _checkATTOnResume() async { + if (Platform.isIOS) { + try { + // 只有当状态依然是“未决定”时,才去尝试请求 + // 如果用户已经点过允许或拒绝,这里就不会再打扰用户 + final status = await AppTrackingTransparency.trackingAuthorizationStatus; + if (status == TrackingStatus.notDetermined) { + debugPrint("App resumed and ATT is not determined. Retrying request..."); + await _requestATT(); + } + } catch (e) { + debugPrint("Error checking ATT on resume: $e"); + } + } + } + + Future _initPermissionsAndSDK() async { + // 1. 启动时的安全延迟,等待 LaunchScreen 消失 + await Future.delayed(const Duration(milliseconds: 500)); + + if (!mounted) return; + + // 2. iOS ATT 授权 + if (Platform.isIOS) { + await _requestATT(); + } + + // 3. 初始化 SDK + try { + await AppAdsTools.instance.init(); + debugPrint("✅ Max SDK Initialized"); + } catch (error) { + debugPrint("❌ SDK Initialization failed: $error"); + } + + // 4. 加载广告 + if (mounted) { + _startAdLoadingProcess(); + } + } + + /// 核心 ATT 请求逻辑(加入了防并发锁) + Future _requestATT() async { + // 如果当前正在请求中,直接返回,防止重复弹窗 + if (_isRequestingATT) return; + + _isRequestingATT = true; + + try { + TrackingStatus status = await AppTrackingTransparency.trackingAuthorizationStatus; + + // 只有未决定的状态才弹窗 + if (status == TrackingStatus.notDetermined) { + debugPrint("Requesting ATT Authorization..."); + // 请求弹窗 + status = await AppTrackingTransparency.requestTrackingAuthorization(); + debugPrint("ATT Status Result: $status"); + + // 只有当用户真正做出了选择(不再是 notDetermined),才延迟一下给动画时间 + if (status != TrackingStatus.notDetermined) { + await Future.delayed(const Duration(milliseconds: 500)); + } + } + } catch (e) { + debugPrint("ATT Request Error: $e"); + } finally { + // 无论成功失败,释放锁,允许后续重试(万一失败了) + _isRequestingATT = false; + } + } + + void _startAdLoadingProcess() { + debugPrint("开始加载开屏广告..."); + + // 如果之前已经有个定时器(比如因为生命周期变化重复触发),先取消掉 + _timeoutTimer?.cancel(); + + _timeoutTimer = Timer(const Duration(seconds: 15), () { + debugPrint("广告加载超时 (15秒)。"); + _navigateToHome(); + }); + + AppAdsTools.instance.loadInitialSplashAd( + onAdReady: (AdPlacement placement) { + debugPrint("开屏广告已就绪: ${placement.name}"); + _navigateToHome(adHomePlacement: placement); + }, + onAllAdsFailed: () { + debugPrint("所有开屏广告都加载失败。"); + _navigateToHome(); + }, + ); + } + + void _navigateToHome({AdPlacement? adHomePlacement}) { + if (_isNavigating) return; + _isNavigating = true; + _timeoutTimer?.cancel(); + + if (!mounted) return; + + Navigator.of(context).pushReplacement( + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => + MainScreen(adShowPlacement: adHomePlacement), + ), + ); + } + + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0xff1F2124), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Image.asset( + 'assets/images/aesthloco.png', + width: 100, + height: 100, + ), + const SizedBox(height: 5), + const Text( + '.Resource Loading.', + style: TextStyle( + fontSize: 14, + color: Color(0xff7D45E5), + fontWeight: FontWeight.w500, + decoration: TextDecoration.none, + ), + ), + const SizedBox(height: 5), + const SpinKitCubeGridIndicator(), + ], + ), + ), + ); + } +} + +class SpinKitCubeGridIndicator extends StatelessWidget { + const SpinKitCubeGridIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return const SpinKitThreeBounce(color: Color(0xff7D45E5), size: 23.0); + } +} \ No newline at end of file diff --git a/lib/generated/assets.dart b/lib/generated/assets.dart new file mode 100644 index 0000000..dc03c68 --- /dev/null +++ b/lib/generated/assets.dart @@ -0,0 +1,42 @@ +///This file is automatically generated. DO NOT EDIT, all your changes would be lost. +class Assets { + Assets._(); + + static const String abstractAbstract1 = 'assets/images/abstract/abstract1.png'; + static const String abstractAbstract10 = 'assets/images/abstract/abstract10.png'; + static const String abstractAbstract11 = 'assets/images/abstract/abstract11.png'; + static const String abstractAbstract12 = 'assets/images/abstract/abstract12.png'; + static const String abstractAbstract13 = 'assets/images/abstract/abstract13.png'; + static const String abstractAbstract14 = 'assets/images/abstract/abstract14.png'; + static const String abstractAbstract15 = 'assets/images/abstract/abstract15.png'; + static const String abstractAbstract2 = 'assets/images/abstract/abstract2.png'; + static const String abstractAbstract3 = 'assets/images/abstract/abstract3.png'; + static const String abstractAbstract4 = 'assets/images/abstract/abstract4.png'; + static const String abstractAbstract5 = 'assets/images/abstract/abstract5.png'; + static const String abstractAbstract6 = 'assets/images/abstract/abstract6.png'; + static const String abstractAbstract7 = 'assets/images/abstract/abstract7.png'; + static const String abstractAbstract8 = 'assets/images/abstract/abstract8.png'; + static const String abstractAbstract9 = 'assets/images/abstract/abstract9.png'; + static const String aestheticaWallpaperAssetsManifest = 'assets/manifest.json'; + static const String architectureArchitecture1 = 'assets/images/architecture/architecture1.png'; + static const String architectureArchitecture10 = 'assets/images/architecture/architecture10.png'; + static const String architectureArchitecture2 = 'assets/images/architecture/architecture2.png'; + static const String architectureArchitecture3 = 'assets/images/architecture/architecture3.png'; + static const String architectureArchitecture4 = 'assets/images/architecture/architecture4.png'; + static const String architectureArchitecture5 = 'assets/images/architecture/architecture5.png'; + static const String architectureArchitecture6 = 'assets/images/architecture/architecture6.png'; + static const String architectureArchitecture7 = 'assets/images/architecture/architecture7.png'; + static const String architectureArchitecture8 = 'assets/images/architecture/architecture8.png'; + static const String architectureArchitecture9 = 'assets/images/architecture/architecture9.png'; + static const String assetsManifest = 'assets/manifest.json'; + static const String natureNature1 = 'assets/images/nature/nature1.png'; + static const String natureNature2 = 'assets/images/nature/nature2.png'; + static const String natureNature3 = 'assets/images/nature/nature3.png'; + static const String natureNature4 = 'assets/images/nature/nature4.png'; + static const String natureNature5 = 'assets/images/nature/nature5.png'; + static const String natureNature6 = 'assets/images/nature/nature6.png'; + static const String natureNature7 = 'assets/images/nature/nature7.png'; + static const String natureNature8 = 'assets/images/nature/nature8.png'; + static const String natureNature9 = 'assets/images/nature/nature9.png'; + +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..80f8fea --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,114 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:aesthetica_wallpaper/providers/editor_provider.dart'; +import 'package:aesthetica_wallpaper/providers/recipe_provider.dart'; +import 'package:aesthetica_wallpaper/providers/weather_provider.dart'; +import 'package:aesthetica_wallpaper/providers/puzzle_provider.dart'; +import 'package:aesthetica_wallpaper/providers/drag_puzzle_provider.dart'; +import 'package:aesthetica_wallpaper/core/app.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:firebase_analytics/observer.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'core/firebase_optinons.dart'; + +bool isFirebaseInitialized = false; + +void main() async { + runZonedGuarded>(() async { + WidgetsFlutterBinding.ensureInitialized(); + + try { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ).timeout(const Duration(seconds: 10)); // 设置15秒超时 + print("✅ Firebase 初始化成功。"); + isFirebaseInitialized = true; // 成功后设置标志 + } on TimeoutException { + print("⚠️ Firebase 初始化超时 (超过15秒),应用将继续运行但相关功能将不可用。"); + } catch (error, stack) { + print("!!!!!!!!!! Firebase 初始化失败 !!!!!!!!!!: $error"); + FirebaseCrashlytics.instance.recordError(error, stack, fatal: false); + } + + // Firebase 成功初始化后,配置依赖它的服务 + if (isFirebaseInitialized) { + //设置全局错误处理器 + FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError; + PlatformDispatcher.instance.onError = (error, stack) { + FirebaseCrashlytics.instance.recordError(error, stack, fatal: true); + return true; + }; + // 使用安全服务来记录 App Open + AnalyticsService.instance.logAppOpen(); + } + + final analyObserver = SafeFirebaseAnalyticsObserver(); + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => EditorProvider()), + ChangeNotifierProvider( + create: (_) => RecipeProvider()..loadRecipes(), + ), + ChangeNotifierProvider(create: (_) => WeatherProvider()), + ChangeNotifierProvider(create: (_) => PuzzleProvider()), + ChangeNotifierProvider(create: (_) => DragPuzzleProvider()), + ], + child: AestheticaApp(analyObserver: analyObserver), + ), + ); + }, (e, s) {}); +} + +class AnalyticsService { + // 单例模式 + AnalyticsService._(); + static final instance = AnalyticsService._(); + final _analytics = FirebaseAnalytics.instance; + /// 安全地记录一个 App Open 事件。 + void logAppOpen() { + if (isFirebaseInitialized) { + _analytics.logAppOpen(); + } else { + print("[AnalyticsService] Skipped logAppOpen: Firebase not initialized."); + } + } +} + +class SafeFirebaseAnalyticsObserver extends NavigatorObserver { + late final FirebaseAnalyticsObserver _observer; + + SafeFirebaseAnalyticsObserver() { + if (isFirebaseInitialized) { + _observer = FirebaseAnalyticsObserver( + analytics: AnalyticsService.instance._analytics, + ); + } + } + + @override + void didPush(Route route, Route? previousRoute) { + if (isFirebaseInitialized) { + _observer.didPush(route, previousRoute); + } + } + + @override + void didPop(Route route, Route? previousRoute) { + if (isFirebaseInitialized) { + _observer.didPop(route, previousRoute); + } + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + if (isFirebaseInitialized) { + _observer.didReplace(newRoute: newRoute, oldRoute: oldRoute); + } + } +} diff --git a/lib/models/app_settings.dart b/lib/models/app_settings.dart new file mode 100644 index 0000000..8161f6a --- /dev/null +++ b/lib/models/app_settings.dart @@ -0,0 +1,31 @@ +class AppSettings { + final bool darkMode; + final String language; + final bool notifications; + final double imageQuality; + final bool autoSave; + + AppSettings({ + this.darkMode = true, + this.language = 'en', + this.notifications = true, + this.imageQuality = 1.0, + this.autoSave = true, + }); + + AppSettings copyWith({ + bool? darkMode, + String? language, + bool? notifications, + double? imageQuality, + bool? autoSave, + }) { + return AppSettings( + darkMode: darkMode ?? this.darkMode, + language: language ?? this.language, + notifications: notifications ?? this.notifications, + imageQuality: imageQuality ?? this.imageQuality, + autoSave: autoSave ?? this.autoSave, + ); + } +} diff --git a/lib/models/drag_puzzle_game.dart b/lib/models/drag_puzzle_game.dart new file mode 100644 index 0000000..0a95b3a --- /dev/null +++ b/lib/models/drag_puzzle_game.dart @@ -0,0 +1,162 @@ +import 'package:flutter/material.dart'; +import 'package:aesthetica_wallpaper/models/puzzle_game_interface.dart'; + +/// 拖拽式拼图块数据模型 +class DragPuzzlePiece { + final int id; // 唯一标识 + final int correctRow; // 正确的行位置 + final int correctCol; // 正确的列位置 + final ImageProvider image; // 图片块 + bool isPlaced; // 是否已放置到正确位置 + int? currentRow; // 当前行位置(如果已放置) + int? currentCol; // 当前列位置(如果已放置) + + DragPuzzlePiece({ + required this.id, + required this.correctRow, + required this.correctCol, + required this.image, + this.isPlaced = false, + this.currentRow, + this.currentCol, + }); + + bool get isCorrectlyPlaced => + isPlaced && currentRow == correctRow && currentCol == correctCol; + + DragPuzzlePiece copyWith({ + int? id, + int? correctRow, + int? correctCol, + ImageProvider? image, + bool? isPlaced, + int? currentRow, + int? currentCol, + }) { + return DragPuzzlePiece( + id: id ?? this.id, + correctRow: correctRow ?? this.correctRow, + correctCol: correctCol ?? this.correctCol, + image: image ?? this.image, + isPlaced: isPlaced ?? this.isPlaced, + currentRow: currentRow ?? this.currentRow, + currentCol: currentCol ?? this.currentCol, + ); + } +} + +/// 拖拽式拼图游戏状态 +class DragPuzzleGame implements IPuzzleGame { + final List pieces; // 所有拼图块 + final int gridRows; // 网格行数 + final int gridCols; // 网格列数 + final Duration elapsedTime; // 已用时间 + final int moves; // 移动次数 + final bool isComplete; // 是否完成 + final String imagePath; // 原图路径 + + DragPuzzleGame({ + required this.pieces, + required this.gridRows, + required this.gridCols, + this.elapsedTime = Duration.zero, + this.moves = 0, + this.isComplete = false, + required this.imagePath, + }); + + /// 获取未放置的拼图块 + List get unplacedPieces => + pieces.where((piece) => !piece.isPlaced).toList(); + + /// 获取已放置的拼图块 + List get placedPieces => + pieces.where((piece) => piece.isPlaced).toList(); + + /// 检查游戏是否完成 + bool checkComplete() { + return pieces.every((piece) => piece.isCorrectlyPlaced); + } + + /// 获取指定位置的拼图块 + DragPuzzlePiece? getPieceAt(int row, int col) { + try { + return pieces.firstWhere( + (piece) => + piece.isPlaced && + piece.currentRow == row && + piece.currentCol == col, + ); + } catch (e) { + return null; + } + } + + @override + String get difficultyLabel { + return '${gridRows}x$gridCols'; + } + + /// 获取星级评分 + @override + int getStarRating() { + if (!isComplete) return 0; + + final totalPieces = gridRows * gridCols; + final optimalMoves = totalPieces; // 理想情况下每个块只移动一次 + final optimalTime = Duration(minutes: totalPieces ~/ 4); // 理想时间 + + int stars = 1; + if (moves <= optimalMoves && elapsedTime <= optimalTime) { + stars = 3; + } else if (moves <= optimalMoves * 1.5 || + elapsedTime <= optimalTime * 1.5) { + stars = 2; + } + + return stars; + } + + DragPuzzleGame copyWith({ + List? pieces, + int? gridRows, + int? gridCols, + Duration? elapsedTime, + int? moves, + bool? isComplete, + String? imagePath, + }) { + return DragPuzzleGame( + pieces: pieces ?? this.pieces, + gridRows: gridRows ?? this.gridRows, + gridCols: gridCols ?? this.gridCols, + elapsedTime: elapsedTime ?? this.elapsedTime, + moves: moves ?? this.moves, + isComplete: isComplete ?? this.isComplete, + imagePath: imagePath ?? this.imagePath, + ); + } +} + +/// 拖拽式拼图难度 +enum DragPuzzleDifficulty { + beginner(2, 2, 'Beginner', '2x2'), + easy(3, 2, 'Easy', '3x2'), + medium(4, 3, 'Medium', '4x3'), + hard(5, 4, 'Hard', '5x4'), + expert(6, 5, 'Expert', '6x5'); + + final int rows; + final int cols; + final String label; + final String description; + + const DragPuzzleDifficulty( + this.rows, + this.cols, + this.label, + this.description, + ); + + int get totalPieces => rows * cols; +} diff --git a/lib/models/image_category.dart b/lib/models/image_category.dart new file mode 100644 index 0000000..30d3d93 --- /dev/null +++ b/lib/models/image_category.dart @@ -0,0 +1,24 @@ +class ImageCategory { + final String name; + final String folder; + final List images; + + ImageCategory({ + required this.name, + required this.folder, + required this.images, + }); + + // 获取第一张图片作为缩略图 + String getThumbnailPath() { + if (images.isNotEmpty) { + return 'assets/images/$folder/${images.first}'; + } + return 'assets/images/$folder/placeholder.png'; + } + + // 获取图片的完整路径 + String getImagePath(String imageName) { + return 'assets/images/$folder/$imageName'; + } +} diff --git a/lib/models/message.dart b/lib/models/message.dart new file mode 100644 index 0000000..76aa284 --- /dev/null +++ b/lib/models/message.dart @@ -0,0 +1,33 @@ +class Message { + final String id; + final String senderName; + final String content; + final DateTime timestamp; + final String avatarUrl; + final bool isRead; + + Message({ + required this.id, + required this.senderName, + required this.content, + required this.timestamp, + required this.avatarUrl, + this.isRead = false, + }); + + // 获取格式化的时间 + String get formattedTime { + final now = DateTime.now(); + final difference = now.difference(timestamp); + + if (difference.inDays > 0) { + return '${difference.inDays}d ago'; + } else if (difference.inHours > 0) { + return '${difference.inHours}h ago'; + } else if (difference.inMinutes > 0) { + return '${difference.inMinutes}m ago'; + } else { + return 'Just now'; + } + } +} diff --git a/lib/models/puzzle_game.dart b/lib/models/puzzle_game.dart new file mode 100644 index 0000000..6751b33 --- /dev/null +++ b/lib/models/puzzle_game.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:aesthetica_wallpaper/models/puzzle_game_interface.dart'; + +/// 拼图块数据模型 +class PuzzlePiece { + final int id; // 唯一标识 + final int correctPosition; // 正确位置 + int currentPosition; // 当前位置 + final ImageProvider image; // 图片块 + + PuzzlePiece({ + required this.id, + required this.correctPosition, + required this.currentPosition, + required this.image, + }); + + bool get isCorrect => currentPosition == correctPosition; + + PuzzlePiece copyWith({ + int? id, + int? correctPosition, + int? currentPosition, + ImageProvider? image, + }) { + return PuzzlePiece( + id: id ?? this.id, + correctPosition: correctPosition ?? this.correctPosition, + currentPosition: currentPosition ?? this.currentPosition, + image: image ?? this.image, + ); + } +} + +/// 游戏模式 +enum GameMode { + classic, // 经典模式(滑动) + challenge, // 挑战模式(限时) + casual, // 休闲模式(无限制) +} + +/// 游戏难度 +enum GameDifficulty { + easy(3, '简单', '3x3'), + medium(4, '中等', '4x4'), + hard(5, '困难', '5x5'), + expert(6, '专家', '6x6'); + + final int gridSize; + final String label; + final String description; + + const GameDifficulty(this.gridSize, this.label, this.description); +} + +/// 拼图游戏状态 +class PuzzleGame implements IPuzzleGame { + List pieces; + int moves; + Duration elapsedTime; + GameDifficulty difficulty; + GameMode mode; + bool isComplete; + int? emptyPosition; // 空格位置(仅滑动模式) + + PuzzleGame({ + required this.pieces, + this.moves = 0, + this.elapsedTime = Duration.zero, + required this.difficulty, + this.mode = GameMode.classic, + this.isComplete = false, + this.emptyPosition, + }); + + int get gridSize => difficulty.gridSize; + int get totalPieces => gridSize * gridSize; + + @override + String get difficultyLabel => difficulty.label; + + /// 检查游戏是否完成 + bool checkComplete() { + return pieces.every((piece) => piece.isCorrect); + } + + /// 获取星级评分(1-3星) + @override + int getStarRating() { + if (!isComplete) return 0; + + final optimalMoves = totalPieces * 2; // 理想步数 + final optimalTime = Duration(minutes: difficulty.gridSize); // 理想时间 + + int stars = 1; + if (moves <= optimalMoves && elapsedTime <= optimalTime) { + stars = 3; + } else if (moves <= optimalMoves * 1.5 || + elapsedTime <= optimalTime * 1.5) { + stars = 2; + } + + return stars; + } + + PuzzleGame copyWith({ + List? pieces, + int? moves, + Duration? elapsedTime, + GameDifficulty? difficulty, + GameMode? mode, + bool? isComplete, + int? emptyPosition, + }) { + return PuzzleGame( + pieces: pieces ?? this.pieces, + moves: moves ?? this.moves, + elapsedTime: elapsedTime ?? this.elapsedTime, + difficulty: difficulty ?? this.difficulty, + mode: mode ?? this.mode, + isComplete: isComplete ?? this.isComplete, + emptyPosition: emptyPosition ?? this.emptyPosition, + ); + } +} + +/// 游戏记录 +class GameRecord { + final String imageId; + final GameDifficulty difficulty; + final Duration time; + final int moves; + final int stars; + final DateTime completedAt; + + GameRecord({ + required this.imageId, + required this.difficulty, + required this.time, + required this.moves, + required this.stars, + required this.completedAt, + }); + + Map toJson() { + return { + 'imageId': imageId, + 'difficulty': difficulty.name, + 'time': time.inSeconds, + 'moves': moves, + 'stars': stars, + 'completedAt': completedAt.toIso8601String(), + }; + } + + factory GameRecord.fromJson(Map json) { + return GameRecord( + imageId: json['imageId'], + difficulty: GameDifficulty.values.firstWhere( + (d) => d.name == json['difficulty'], + ), + time: Duration(seconds: json['time']), + moves: json['moves'], + stars: json['stars'], + completedAt: DateTime.parse(json['completedAt']), + ); + } +} diff --git a/lib/models/puzzle_game_interface.dart b/lib/models/puzzle_game_interface.dart new file mode 100644 index 0000000..791ab83 --- /dev/null +++ b/lib/models/puzzle_game_interface.dart @@ -0,0 +1,13 @@ +/// 拼图游戏通用接口 +/// 用于统一不同类型的拼图游戏(传统滑块拼图、拖拽拼图等) +abstract class IPuzzleGame { + Duration get elapsedTime; + int get moves; + bool get isComplete; + + /// 获取星级评分(1-3星) + int getStarRating(); + + /// 获取难度标签 + String get difficultyLabel; +} diff --git a/lib/models/recipe.dart b/lib/models/recipe.dart new file mode 100644 index 0000000..375a94b --- /dev/null +++ b/lib/models/recipe.dart @@ -0,0 +1,132 @@ +// "配方"模型,定义了所有可编辑的参数 +class Recipe { + String id; + String baseImagePath; + double brightness; + double contrast; + double saturation; + double blur; + double pixelate; + String overlayText; + String fontFamily; + int textColor; // 存储为 ARGB 整数 + + Recipe({ + required this.id, + required this.baseImagePath, + this.brightness = 0.0, // 亮度 (-1 to 1, default 0) + this.contrast = 1.0, // 对比度 (0 to 4, default 1) + this.saturation = 1.0, // 饱和度 (0 to 4, default 1) + this.blur = 0.0, // 模糊 (0 to 20, default 0) + this.pixelate = 1.0, // 像素化 (1 to 30, default 1 = off) + this.overlayText = '', + this.fontFamily = 'Lato', + this.textColor = 0xFFFFFFFF, // 默认白色 + }); + + // 转换为 JSON (用于存储到 shared_preferences) + Map toJson() { + return { + 'id': id, + 'baseImagePath': baseImagePath, + 'brightness': brightness, + 'contrast': contrast, + 'saturation': saturation, + 'blur': blur, + 'pixelate': pixelate, + 'overlayText': overlayText, + 'fontFamily': fontFamily, + 'textColor': textColor, + }; + } + + // 从 JSON 恢复 (用于从 shared_preferences 读取) + factory Recipe.fromJson(Map json) { + return Recipe( + id: json['id'], + baseImagePath: json['baseImagePath'], + brightness: json['brightness'] ?? 0.0, + contrast: json['contrast'] ?? 1.0, + saturation: json['saturation'] ?? 1.0, + blur: json['blur'] ?? 0.0, + pixelate: json['pixelate'] ?? 1.0, + overlayText: json['overlayText'] ?? '', + fontFamily: json['fontFamily'] ?? 'Lato', + textColor: json['textColor'] ?? 0xFFFFFFFF, + ); + } + + // 辅助函数,用于快速序列化 + String toJsonString() { + // 简单的JSON序列化,不依赖dart:convert + return '{"id":"$id","baseImagePath":"$baseImagePath","brightness":$brightness,"contrast":$contrast,"saturation":$saturation,"blur":$blur,"pixelate":$pixelate,"overlayText":"$overlayText","fontFamily":"$fontFamily","textColor":$textColor}'; + } + + // 辅助函数,用于快速反序列化 + factory Recipe.fromJsonString(String str) { + // 简单的JSON解析,不依赖dart:convert + // 这里需要更复杂的解析逻辑,暂时返回默认值 + return Recipe(id: '', baseImagePath: ''); + } + + // 实现相等性比较,用于检测变化 + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! Recipe) return false; + + return id == other.id && + baseImagePath == other.baseImagePath && + brightness == other.brightness && + contrast == other.contrast && + saturation == other.saturation && + blur == other.blur && + pixelate == other.pixelate && + overlayText == other.overlayText && + fontFamily == other.fontFamily && + textColor == other.textColor; + } + + @override + int get hashCode { + return Object.hash( + id, + baseImagePath, + brightness, + contrast, + saturation, + blur, + pixelate, + overlayText, + fontFamily, + textColor, + ); + } + + // 创建副本的方法 + Recipe copyWith({ + String? id, + String? baseImagePath, + double? brightness, + double? contrast, + double? saturation, + double? blur, + double? pixelate, + String? overlayText, + String? fontFamily, + int? textColor, + }) { + return Recipe( + id: id ?? this.id, + baseImagePath: baseImagePath ?? this.baseImagePath, + brightness: brightness ?? this.brightness, + contrast: contrast ?? this.contrast, + saturation: saturation ?? this.saturation, + blur: blur ?? this.blur, + pixelate: pixelate ?? this.pixelate, + overlayText: overlayText ?? this.overlayText, + fontFamily: fontFamily ?? this.fontFamily, + textColor: textColor ?? this.textColor, + ); + } +} diff --git a/lib/models/weather.dart b/lib/models/weather.dart new file mode 100644 index 0000000..c35a22c --- /dev/null +++ b/lib/models/weather.dart @@ -0,0 +1,105 @@ +// 天气数据模型 +class Weather { + final String location; + final String country; + final double temperature; + final String condition; + final String conditionIcon; + final double feelsLike; + final int humidity; + final double windSpeed; + final String windDirection; + final double pressure; + final double visibility; + final int uvIndex; + final DateTime lastUpdated; + + Weather({ + required this.location, + required this.country, + required this.temperature, + required this.condition, + required this.conditionIcon, + required this.feelsLike, + required this.humidity, + required this.windSpeed, + required this.windDirection, + required this.pressure, + required this.visibility, + required this.uvIndex, + required this.lastUpdated, + }); + + factory Weather.fromJson(Map json) { + final location = json['location']; + final current = json['current']; + + return Weather( + location: location['name'] ?? '', + country: location['country'] ?? '', + temperature: (current['temp_c'] ?? 0.0).toDouble(), + condition: current['condition']['text'] ?? '', + conditionIcon: 'https:${current['condition']['icon'] ?? ''}', + feelsLike: (current['feelslike_c'] ?? 0.0).toDouble(), + humidity: (current['humidity'] ?? 0).toInt(), + windSpeed: (current['wind_kph'] ?? 0.0).toDouble(), + windDirection: current['wind_dir'] ?? '', + pressure: (current['pressure_mb'] ?? 0.0).toDouble(), + visibility: (current['vis_km'] ?? 0.0).toDouble(), + uvIndex: (current['uv'] ?? 0).toInt(), + lastUpdated: DateTime.parse( + current['last_updated'] ?? DateTime.now().toIso8601String(), + ), + ); + } + + Map toJson() { + return { + 'location': location, + 'country': country, + 'temperature': temperature, + 'condition': condition, + 'conditionIcon': conditionIcon, + 'feelsLike': feelsLike, + 'humidity': humidity, + 'windSpeed': windSpeed, + 'windDirection': windDirection, + 'pressure': pressure, + 'visibility': visibility, + 'uvIndex': uvIndex, + 'lastUpdated': lastUpdated.toIso8601String(), + }; + } +} + +// 天气预报数据模型 +class WeatherForecast { + final String date; + final double maxTemp; + final double minTemp; + final String condition; + final String conditionIcon; + final int chanceOfRain; + + WeatherForecast({ + required this.date, + required this.maxTemp, + required this.minTemp, + required this.condition, + required this.conditionIcon, + required this.chanceOfRain, + }); + + factory WeatherForecast.fromJson(Map json) { + final day = json['day']; + + return WeatherForecast( + date: json['date'] ?? '', + maxTemp: (day['maxtemp_c'] ?? 0.0).toDouble(), + minTemp: (day['mintemp_c'] ?? 0.0).toDouble(), + condition: day['condition']['text'] ?? '', + conditionIcon: 'https:${day['condition']['icon'] ?? ''}', + chanceOfRain: (day['daily_chance_of_rain'] ?? 0).toInt(), + ); + } +} diff --git a/lib/providers/drag_puzzle_provider.dart b/lib/providers/drag_puzzle_provider.dart new file mode 100644 index 0000000..45efe0c --- /dev/null +++ b/lib/providers/drag_puzzle_provider.dart @@ -0,0 +1,248 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui' as ui; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:aesthetica_wallpaper/models/drag_puzzle_game.dart'; + +class DragPuzzleProvider extends ChangeNotifier { + DragPuzzleGame? _currentGame; + Timer? _timer; + + DragPuzzleGame? get currentGame => _currentGame; + + /// 创建拖拽式拼图游戏 + Future createDragPuzzle({ + required String imagePath, + required DragPuzzleDifficulty difficulty, + }) async { + // 停止之前的计时器 + _timer?.cancel(); + + // 分割图片 + final pieces = await _splitImageForDrag( + imagePath, + difficulty.rows, + difficulty.cols, + ); + + // 打乱拼图块顺序 + pieces.shuffle(); + + // 创建游戏 + _currentGame = DragPuzzleGame( + pieces: pieces, + gridRows: difficulty.rows, + gridCols: difficulty.cols, + imagePath: imagePath, + ); + + // 开始计时 + _startTimer(); + + notifyListeners(); + } + + /// 分割图片为拖拽式拼图块 + Future> _splitImageForDrag( + String imagePath, + int rows, + int cols, + ) async { + final pieces = []; + + // 加载图片 + final ByteData data = await rootBundle.load(imagePath); + final codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); + final frame = await codec.getNextFrame(); + final image = frame.image; + + final pieceWidth = image.width ~/ cols; + final pieceHeight = image.height ~/ rows; + + // 分割图片 + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + final id = row * cols + col; + + // 创建图片块 + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + + canvas.drawImageRect( + image, + Rect.fromLTWH( + col * pieceWidth.toDouble(), + row * pieceHeight.toDouble(), + pieceWidth.toDouble(), + pieceHeight.toDouble(), + ), + Rect.fromLTWH(0, 0, pieceWidth.toDouble(), pieceHeight.toDouble()), + Paint(), + ); + + final picture = recorder.endRecording(); + final pieceImage = await picture.toImage(pieceWidth, pieceHeight); + + // 将图片转换为字节数据 + final byteData = await pieceImage.toByteData( + format: ui.ImageByteFormat.png, + ); + final uint8List = byteData!.buffer.asUint8List(); + + pieces.add( + DragPuzzlePiece( + id: id, + correctRow: row, + correctCol: col, + image: MemoryImage(uint8List), + ), + ); + } + } + + return pieces; + } + + /// 将拼图块放置到指定位置 + void placePiece(int pieceId, int targetRow, int targetCol) { + if (_currentGame == null || _currentGame!.isComplete) return; + + final pieces = List.from(_currentGame!.pieces); + final pieceIndex = pieces.indexWhere((p) => p.id == pieceId); + + if (pieceIndex == -1) return; + + final piece = pieces[pieceIndex]; + + // 检查目标位置是否已被占用 + final existingPiece = _currentGame!.getPieceAt(targetRow, targetCol); + if (existingPiece != null) { + // 如果目标位置有其他块,将其移回底部 + final existingIndex = pieces.indexWhere((p) => p.id == existingPiece.id); + pieces[existingIndex] = existingPiece.copyWith( + isPlaced: false, + currentRow: null, + currentCol: null, + ); + } + + // 放置当前块 + pieces[pieceIndex] = piece.copyWith( + isPlaced: true, + currentRow: targetRow, + currentCol: targetCol, + ); + + // 增加移动次数 + final newMoves = _currentGame!.moves + 1; + + // 更新游戏状态 + _currentGame = _currentGame!.copyWith(pieces: pieces, moves: newMoves); + + // 检查是否完成 + if (_currentGame!.checkComplete()) { + _completeGame(); + } + + notifyListeners(); + } + + /// 将拼图块移回底部 + void removePiece(int pieceId) { + if (_currentGame == null || _currentGame!.isComplete) return; + + final pieces = List.from(_currentGame!.pieces); + final pieceIndex = pieces.indexWhere((p) => p.id == pieceId); + + if (pieceIndex == -1) return; + + final piece = pieces[pieceIndex]; + + // 只有已放置的块才能移除 + if (!piece.isPlaced) return; + + pieces[pieceIndex] = piece.copyWith( + isPlaced: false, + currentRow: null, + currentCol: null, + ); + + _currentGame = _currentGame!.copyWith( + pieces: pieces, + moves: _currentGame!.moves + 1, + ); + + notifyListeners(); + } + + /// 开始计时 + void _startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_currentGame != null && !_currentGame!.isComplete) { + _currentGame = _currentGame!.copyWith( + elapsedTime: _currentGame!.elapsedTime + const Duration(seconds: 1), + ); + notifyListeners(); + } + }); + } + + /// 完成游戏 + void _completeGame() { + _timer?.cancel(); + _currentGame = _currentGame!.copyWith(isComplete: true); + } + + /// 重新开始游戏 + void restartGame() { + if (_currentGame == null) return; + + _timer?.cancel(); + + // 重置所有拼图块 + final pieces = _currentGame!.pieces + .map( + (piece) => piece.copyWith( + isPlaced: false, + currentRow: null, + currentCol: null, + ), + ) + .toList(); + + // 重新打乱 + pieces.shuffle(); + + _currentGame = _currentGame!.copyWith( + pieces: pieces, + elapsedTime: Duration.zero, + moves: 0, + isComplete: false, + ); + + _startTimer(); + notifyListeners(); + } + + /// 提示功能 + void showHint() { + if (_currentGame == null || _currentGame!.isComplete) return; + + // 找到第一个未正确放置的块 + final unplacedPieces = _currentGame!.unplacedPieces; + if (unplacedPieces.isNotEmpty) { + final hintPiece = unplacedPieces.first; + print( + '提示:将块 ${hintPiece.id} 放到位置 (${hintPiece.correctRow}, ${hintPiece.correctCol})', + ); + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } +} diff --git a/lib/providers/editor_provider.dart b/lib/providers/editor_provider.dart new file mode 100644 index 0000000..475d17b --- /dev/null +++ b/lib/providers/editor_provider.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; +import 'package:aesthetica_wallpaper/models/recipe.dart'; + +// EditorProvider 持有当前编辑器的所有状态 +class EditorProvider with ChangeNotifier { + // 从一个空白的 Recipe 开始 + Recipe _currentRecipe = Recipe(id: '', baseImagePath: ''); + + // 获取当前状态的 getter + Recipe get currentRecipe => _currentRecipe; + + // 动态模拟器状态 + bool _isTimeAware = false; + bool get isTimeAware => _isTimeAware; + + bool _isBatteryAware = false; + get isBatteryAware => _isBatteryAware; + + // 当进入编辑器时,重置或加载一个新的 Recipe + void startEditing(String baseImagePath) { + _currentRecipe = Recipe( + id: DateTime.now().millisecondsSinceEpoch.toString(), + baseImagePath: baseImagePath, + ); + // 重置动态开关 + _isTimeAware = false; + _isBatteryAware = false; + notifyListeners(); + } + + // 当从"我的配方"加载时 + void loadFromRecipe(Recipe recipe) { + _currentRecipe = recipe; + notifyListeners(); + } + + // 更新各种参数的方法 + // 每个方法都会更新值并通知监听器 (UI) 重建 + + void setBrightness(double value) { + _currentRecipe = _currentRecipe.copyWith(brightness: value); + notifyListeners(); + } + + void setContrast(double value) { + _currentRecipe = _currentRecipe.copyWith(contrast: value); + notifyListeners(); + } + + void setSaturation(double value) { + _currentRecipe = _currentRecipe.copyWith(saturation: value); + notifyListeners(); + } + + void setBlur(double value) { + _currentRecipe = _currentRecipe.copyWith(blur: value); + notifyListeners(); + } + + void setPixelate(double value) { + _currentRecipe = _currentRecipe.copyWith(pixelate: value); + notifyListeners(); + } + + void setText(String text) { + _currentRecipe = _currentRecipe.copyWith(overlayText: text); + notifyListeners(); + } + + void setFont(String font) { + _currentRecipe = _currentRecipe.copyWith(fontFamily: font); + notifyListeners(); + } + + void setTextColor(Color color) { + _currentRecipe = _currentRecipe.copyWith(textColor: color.toARGB32()); + notifyListeners(); + } + + void setTimeAware(bool value) { + _isTimeAware = value; + notifyListeners(); + } + + void setBatteryAware(bool value) { + _isBatteryAware = value; + notifyListeners(); + } +} diff --git a/lib/providers/puzzle_provider.dart b/lib/providers/puzzle_provider.dart new file mode 100644 index 0000000..407a45d --- /dev/null +++ b/lib/providers/puzzle_provider.dart @@ -0,0 +1,241 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'dart:ui' as ui; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:aesthetica_wallpaper/models/puzzle_game.dart'; + +class PuzzleProvider extends ChangeNotifier { + PuzzleGame? _currentGame; + Timer? _timer; + List _records = []; + + PuzzleGame? get currentGame => _currentGame; + List get records => _records; + + /// 创建新游戏 + Future createGame({ + required String imagePath, + required GameDifficulty difficulty, + GameMode mode = GameMode.classic, + }) async { + // 停止之前的计时器 + _timer?.cancel(); + + // 加载并分割图片 + final pieces = await _splitImage(imagePath, difficulty.gridSize); + + // 打乱拼图 + _shufflePieces(pieces, difficulty.gridSize); + + // 创建游戏 + _currentGame = PuzzleGame( + pieces: pieces, + difficulty: difficulty, + mode: mode, + emptyPosition: mode == GameMode.classic ? pieces.length - 1 : null, + ); + + // 开始计时 + _startTimer(); + + notifyListeners(); + } + + /// 分割图片为拼图块 + Future> _splitImage(String imagePath, int gridSize) async { + final pieces = []; + + // 加载图片 + final ByteData data = await rootBundle.load(imagePath); + final codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); + final frame = await codec.getNextFrame(); + final image = frame.image; + + final pieceWidth = image.width ~/ gridSize; + final pieceHeight = image.height ~/ gridSize; + + // 分割图片 + for (int row = 0; row < gridSize; row++) { + for (int col = 0; col < gridSize; col++) { + final position = row * gridSize + col; + + // 创建图片块 + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + + canvas.drawImageRect( + image, + Rect.fromLTWH( + col * pieceWidth.toDouble(), + row * pieceHeight.toDouble(), + pieceWidth.toDouble(), + pieceHeight.toDouble(), + ), + Rect.fromLTWH(0, 0, pieceWidth.toDouble(), pieceHeight.toDouble()), + Paint(), + ); + + final picture = recorder.endRecording(); + final pieceImage = await picture.toImage(pieceWidth, pieceHeight); + + pieces.add( + PuzzlePiece( + id: position, + correctPosition: position, + currentPosition: position, + image: MemoryImage( + (await pieceImage.toByteData( + format: ui.ImageByteFormat.png, + ))!.buffer.asUint8List(), + ), + ), + ); + } + } + + return pieces; + } + + /// 打乱拼图 + void _shufflePieces(List pieces, int gridSize) { + final random = math.Random(); + + // 执行多次随机交换 + for (int i = 0; i < pieces.length * 10; i++) { + final index1 = random.nextInt(pieces.length); + final index2 = random.nextInt(pieces.length); + + final temp = pieces[index1].currentPosition; + pieces[index1].currentPosition = pieces[index2].currentPosition; + pieces[index2].currentPosition = temp; + } + + // 确保拼图是可解的(对于滑动模式) + // TODO: 实现可解性检查 + } + + /// 移动拼图块 + void movePiece(int pieceIndex) { + if (_currentGame == null || _currentGame!.isComplete) return; + + final piece = _currentGame!.pieces[pieceIndex]; + final gridSize = _currentGame!.gridSize; + + if (_currentGame!.mode == GameMode.classic) { + // 滑动模式:只能移动到空格 + if (!_canMoveToEmpty(piece.currentPosition, gridSize)) return; + + final emptyPos = _currentGame!.emptyPosition!; + piece.currentPosition = emptyPos; + _currentGame = _currentGame!.copyWith(emptyPosition: pieceIndex); + } + + // 增加步数 + _currentGame = _currentGame!.copyWith(moves: _currentGame!.moves + 1); + + // 检查是否完成 + if (_currentGame!.checkComplete()) { + _completeGame(); + } + + notifyListeners(); + } + + /// 交换两个拼图块(交换模式) + void swapPieces(int index1, int index2) { + if (_currentGame == null || _currentGame!.isComplete) return; + if (_currentGame!.mode == GameMode.classic) return; + + final piece1 = _currentGame!.pieces[index1]; + final piece2 = _currentGame!.pieces[index2]; + + final temp = piece1.currentPosition; + piece1.currentPosition = piece2.currentPosition; + piece2.currentPosition = temp; + + _currentGame = _currentGame!.copyWith(moves: _currentGame!.moves + 1); + + if (_currentGame!.checkComplete()) { + _completeGame(); + } + + notifyListeners(); + } + + /// 检查是否可以移动到空格 + bool _canMoveToEmpty(int position, int gridSize) { + final emptyPos = _currentGame!.emptyPosition!; + final row = position ~/ gridSize; + final col = position % gridSize; + final emptyRow = emptyPos ~/ gridSize; + final emptyCol = emptyPos % gridSize; + + // 检查是否相邻 + return (row == emptyRow && (col - emptyCol).abs() == 1) || + (col == emptyCol && (row - emptyRow).abs() == 1); + } + + /// 开始计时 + void _startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_currentGame != null && !_currentGame!.isComplete) { + _currentGame = _currentGame!.copyWith( + elapsedTime: _currentGame!.elapsedTime + const Duration(seconds: 1), + ); + notifyListeners(); + } + }); + } + + /// 完成游戏 + void _completeGame() { + _timer?.cancel(); + _currentGame = _currentGame!.copyWith(isComplete: true); + + // 保存记录 + final record = GameRecord( + imageId: 'temp', // TODO: 使用实际图片ID + difficulty: _currentGame!.difficulty, + time: _currentGame!.elapsedTime, + moves: _currentGame!.moves, + stars: _currentGame!.getStarRating(), + completedAt: DateTime.now(), + ); + + _records.add(record); + // TODO: 持久化保存记录 + } + + /// 重新开始游戏 + void restartGame() { + if (_currentGame == null) return; + + _timer?.cancel(); + _shufflePieces(_currentGame!.pieces, _currentGame!.gridSize); + + _currentGame = _currentGame!.copyWith( + moves: 0, + elapsedTime: Duration.zero, + isComplete: false, + ); + + _startTimer(); + notifyListeners(); + } + + /// 使用提示 + void useHint() { + if (_currentGame == null || _currentGame!.isComplete) return; + + // TODO: 实现提示逻辑 + // 找到一个不在正确位置的块,并高亮显示其正确位置 + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } +} diff --git a/lib/providers/recipe_provider.dart b/lib/providers/recipe_provider.dart new file mode 100644 index 0000000..574c0b4 --- /dev/null +++ b/lib/providers/recipe_provider.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:aesthetica_wallpaper/models/recipe.dart'; + +class RecipeProvider with ChangeNotifier { + final String _storageKey = "aesthetica_recipes"; + List _recipes = []; + + List get recipes => _recipes; + + // 从 SharedPreferences 加载配方 + Future loadRecipes() async { + final prefs = await SharedPreferences.getInstance(); + final List recipeStrings = prefs.getStringList(_storageKey) ?? []; + _recipes = recipeStrings + .map((str) => Recipe.fromJsonString(str)) + .toList() + .reversed + .toList(); // 反转,让最新的在最前面 + notifyListeners(); + } + + // 保存所有配方到 SharedPreferences + Future _save() async { + final prefs = await SharedPreferences.getInstance(); + final List recipeStrings = _recipes + .map((r) => r.toJsonString()) + .toList(); + await prefs.setStringList(_storageKey, recipeStrings); + } + + // 添加一个新配方 + Future addRecipe(Recipe recipe) async { + // 确保 ID 是唯一的 + recipe.id = DateTime.now().millisecondsSinceEpoch.toString(); + _recipes.insert(0, recipe); // 插入到最前面 + await _save(); + notifyListeners(); + } + + // 删除一个配方 + Future deleteRecipe(String id) async { + _recipes.removeWhere((r) => r.id == id); + await _save(); + notifyListeners(); + } +} diff --git a/lib/providers/simple_puzzle_provider.dart b/lib/providers/simple_puzzle_provider.dart new file mode 100644 index 0000000..5af9310 --- /dev/null +++ b/lib/providers/simple_puzzle_provider.dart @@ -0,0 +1,251 @@ +import 'dart:async'; +import 'dart:math' as math; +import 'package:flutter/material.dart'; +import 'package:aesthetica_wallpaper/models/puzzle_game.dart'; + +/// 简化版拼图Provider,用于快速测试 +class SimplePuzzleProvider extends ChangeNotifier { + PuzzleGame? _currentGame; + Timer? _timer; + List _records = []; + + PuzzleGame? get currentGame => _currentGame; + List get records => _records; + + /// 创建简化版游戏(使用颜色块而不是图片) + void createSimpleGame({ + required GameDifficulty difficulty, + GameMode mode = GameMode.classic, + }) { + // 停止之前的计时器 + _timer?.cancel(); + + // 创建颜色块拼图 + final pieces = _createColorPieces(difficulty.gridSize); + + // 打乱拼图 + _shufflePieces(pieces, difficulty.gridSize); + + // 创建游戏 + _currentGame = PuzzleGame( + pieces: pieces, + difficulty: difficulty, + mode: mode, + emptyPosition: mode == GameMode.classic ? pieces.length - 1 : null, + ); + + // 开始计时 + _startTimer(); + + notifyListeners(); + } + + /// 创建颜色块拼图(用于测试) + List _createColorPieces(int gridSize) { + final pieces = []; + final colors = [ + Colors.red, + Colors.blue, + Colors.green, + Colors.orange, + Colors.purple, + Colors.pink, + Colors.teal, + Colors.amber, + Colors.indigo, + Colors.cyan, + Colors.lime, + Colors.brown, + Colors.grey, + Colors.deepOrange, + Colors.lightBlue, + Colors.lightGreen, + Colors.yellow, + Colors.deepPurple, + Colors.blueGrey, + Colors.redAccent, + Colors.greenAccent, + Colors.blueAccent, + Colors.orangeAccent, + Colors.purpleAccent, + Colors.pinkAccent, + Colors.tealAccent, + Colors.amberAccent, + Colors.indigoAccent, + Colors.cyanAccent, + Colors.limeAccent, + Colors.black87, + Colors.black54, + Colors.black45, + Colors.black38, + Colors.black26, + Colors.black12, + ]; + + for (int i = 0; i < gridSize * gridSize; i++) { + pieces.add( + PuzzlePiece( + id: i, + correctPosition: i, + currentPosition: i, + image: _createColorImage(colors[i % colors.length], i + 1), + ), + ); + } + + return pieces; + } + + /// 创建颜色图片 + ImageProvider _createColorImage(Color color, int number) { + // 这里返回一个简单的颜色图片 + // 实际实现中可以使用Canvas绘制带数字的颜色块 + return NetworkImage( + 'https://via.placeholder.com/100x100/${color.value.toRadixString(16).substring(2)}/FFFFFF?text=$number', + ); + } + + /// 打乱拼图 + void _shufflePieces(List pieces, int gridSize) { + final random = math.Random(); + + // 执行多次随机交换 + for (int i = 0; i < pieces.length * 5; i++) { + final index1 = random.nextInt(pieces.length); + final index2 = random.nextInt(pieces.length); + + final temp = pieces[index1].currentPosition; + pieces[index1].currentPosition = pieces[index2].currentPosition; + pieces[index2].currentPosition = temp; + } + } + + /// 移动拼图块 + void movePiece(int pieceIndex) { + if (_currentGame == null || _currentGame!.isComplete) return; + + final piece = _currentGame!.pieces[pieceIndex]; + final gridSize = _currentGame!.gridSize; + + if (_currentGame!.mode == GameMode.classic) { + // 滑动模式:只能移动到空格 + if (!_canMoveToEmpty(piece.currentPosition, gridSize)) return; + + final emptyPos = _currentGame!.emptyPosition!; + piece.currentPosition = emptyPos; + _currentGame = _currentGame!.copyWith(emptyPosition: pieceIndex); + } + + // 增加步数 + _currentGame = _currentGame!.copyWith(moves: _currentGame!.moves + 1); + + // 检查是否完成 + if (_currentGame!.checkComplete()) { + _completeGame(); + } + + notifyListeners(); + } + + /// 交换两个拼图块(交换模式) + void swapPieces(int index1, int index2) { + if (_currentGame == null || _currentGame!.isComplete) return; + if (_currentGame!.mode == GameMode.classic) return; + + final piece1 = _currentGame!.pieces[index1]; + final piece2 = _currentGame!.pieces[index2]; + + final temp = piece1.currentPosition; + piece1.currentPosition = piece2.currentPosition; + piece2.currentPosition = temp; + + _currentGame = _currentGame!.copyWith(moves: _currentGame!.moves + 1); + + if (_currentGame!.checkComplete()) { + _completeGame(); + } + + notifyListeners(); + } + + /// 检查是否可以移动到空格 + bool _canMoveToEmpty(int position, int gridSize) { + final emptyPos = _currentGame!.emptyPosition!; + final row = position ~/ gridSize; + final col = position % gridSize; + final emptyRow = emptyPos ~/ gridSize; + final emptyCol = emptyPos % gridSize; + + // 检查是否相邻 + return (row == emptyRow && (col - emptyCol).abs() == 1) || + (col == emptyCol && (row - emptyRow).abs() == 1); + } + + /// 开始计时 + void _startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_currentGame != null && !_currentGame!.isComplete) { + _currentGame = _currentGame!.copyWith( + elapsedTime: _currentGame!.elapsedTime + const Duration(seconds: 1), + ); + notifyListeners(); + } + }); + } + + /// 完成游戏 + void _completeGame() { + _timer?.cancel(); + _currentGame = _currentGame!.copyWith(isComplete: true); + + // 保存记录 + final record = GameRecord( + imageId: 'color_test', + difficulty: _currentGame!.difficulty, + time: _currentGame!.elapsedTime, + moves: _currentGame!.moves, + stars: _currentGame!.getStarRating(), + completedAt: DateTime.now(), + ); + + _records.add(record); + } + + /// 重新开始游戏 + void restartGame() { + if (_currentGame == null) return; + + _timer?.cancel(); + _shufflePieces(_currentGame!.pieces, _currentGame!.gridSize); + + _currentGame = _currentGame!.copyWith( + moves: 0, + elapsedTime: Duration.zero, + isComplete: false, + ); + + _startTimer(); + notifyListeners(); + } + + /// 使用提示 + void useHint() { + if (_currentGame == null || _currentGame!.isComplete) return; + + // 简单提示:找到第一个不在正确位置的块 + for (int i = 0; i < _currentGame!.pieces.length; i++) { + final piece = _currentGame!.pieces[i]; + if (!piece.isCorrect) { + // 这里可以添加高亮显示逻辑 + print('提示:块 ${piece.id} 应该在位置 ${piece.correctPosition}'); + break; + } + } + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } +} diff --git a/lib/providers/weather_provider.dart b/lib/providers/weather_provider.dart new file mode 100644 index 0000000..750d222 --- /dev/null +++ b/lib/providers/weather_provider.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/weather.dart'; +import '../services/weather_service.dart'; + +class WeatherProvider with ChangeNotifier { + final WeatherService _weatherService = WeatherService(); + + Weather? _currentWeather; + List _forecast = []; + bool _isLoading = false; + String? _error; + String _selectedLocation = ''; + List _favoriteLocations = []; + + // Getters + Weather? get currentWeather => _currentWeather; + List get forecast => _forecast; + bool get isLoading => _isLoading; + String? get error => _error; + String get selectedLocation => _selectedLocation; + List get favoriteLocations => _favoriteLocations; + + WeatherProvider() { + _loadFavoriteLocations(); + refreshWeather(); + } + + // 刷新天气数据 + Future refreshWeather({String? location}) async { + _setLoading(true); + _error = null; + + try { + // 获取当前天气 + final weather = await _weatherService.getCurrentWeather( + location: location, + ); + if (weather != null) { + _currentWeather = weather; + _selectedLocation = weather.location; + + // 获取预报 + final forecastData = await _weatherService.getWeatherForecast( + location: location, + ); + _forecast = forecastData; + + // 保存最后选择的位置 + await _saveLastLocation(_selectedLocation); + } else { + _error = 'Unable to get weather data, please check network connection'; + } + } catch (e) { + _error = 'Failed to get weather data: $e'; + } finally { + _setLoading(false); + } + } + + // 根据城市名获取天气 + Future getWeatherByCity(String cityName) async { + await refreshWeather(location: cityName); + } + + // 搜索城市 + Future>> searchCities(String query) async { + if (query.isEmpty) return []; + + try { + return await _weatherService.searchLocations(query); + } catch (e) { + debugPrint('搜索城市失败: $e'); + return []; + } + } + + // 添加到收藏位置 + Future addToFavorites(String location) async { + if (!_favoriteLocations.contains(location)) { + _favoriteLocations.add(location); + await _saveFavoriteLocations(); + notifyListeners(); + } + } + + // 从收藏位置移除 + Future removeFromFavorites(String location) async { + _favoriteLocations.remove(location); + await _saveFavoriteLocations(); + notifyListeners(); + } + + // 检查是否为收藏位置 + bool isFavorite(String location) { + return _favoriteLocations.contains(location); + } + + // 获取收藏位置的天气 + Future> getFavoriteWeathers() async { + List weathers = []; + + for (String location in _favoriteLocations) { + try { + final weather = await _weatherService.getWeatherByCity(location); + if (weather != null) { + weathers.add(weather); + } + } catch (e) { + debugPrint('获取收藏位置天气失败: $location - $e'); + } + } + + return weathers; + } + + // 设置加载状态 + void _setLoading(bool loading) { + _isLoading = loading; + notifyListeners(); + } + + // 保存收藏位置 + Future _saveFavoriteLocations() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setStringList('favorite_locations', _favoriteLocations); + } + + // 加载收藏位置 + Future _loadFavoriteLocations() async { + final prefs = await SharedPreferences.getInstance(); + _favoriteLocations = prefs.getStringList('favorite_locations') ?? []; + notifyListeners(); + } + + // 保存最后选择的位置 + Future _saveLastLocation(String location) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('last_location', location); + } + + // 清除错误 + void clearError() { + _error = null; + notifyListeners(); + } + + // 获取天气图标对应的本地图标 + IconData getWeatherIcon(String condition) { + final lowerCondition = condition.toLowerCase(); + + if (lowerCondition.contains('sunny') || lowerCondition.contains('clear')) { + return Icons.wb_sunny; + } else if (lowerCondition.contains('cloud')) { + return Icons.cloud; + } else if (lowerCondition.contains('rain') || + lowerCondition.contains('drizzle')) { + return Icons.grain; + } else if (lowerCondition.contains('snow')) { + return Icons.ac_unit; + } else if (lowerCondition.contains('thunder') || + lowerCondition.contains('storm')) { + return Icons.flash_on; + } else if (lowerCondition.contains('fog') || + lowerCondition.contains('mist')) { + return Icons.blur_on; + } else if (lowerCondition.contains('wind')) { + return Icons.air; + } else { + return Icons.wb_cloudy; + } + } + + // 获取天气颜色主题 + Color getWeatherColor(String condition) { + final lowerCondition = condition.toLowerCase(); + + if (lowerCondition.contains('sunny') || lowerCondition.contains('clear')) { + return Colors.orange; + } else if (lowerCondition.contains('cloud')) { + return Colors.grey; + } else if (lowerCondition.contains('rain') || + lowerCondition.contains('drizzle')) { + return Colors.blue; + } else if (lowerCondition.contains('snow')) { + return Colors.lightBlue; + } else if (lowerCondition.contains('thunder') || + lowerCondition.contains('storm')) { + return Colors.purple; + } else if (lowerCondition.contains('fog') || + lowerCondition.contains('mist')) { + return Colors.blueGrey; + } else { + return Colors.grey; + } + } +} diff --git a/lib/screens/aigenerate/ai_generate.dart b/lib/screens/aigenerate/ai_generate.dart new file mode 100644 index 0000000..926fd71 --- /dev/null +++ b/lib/screens/aigenerate/ai_generate.dart @@ -0,0 +1,603 @@ +import 'dart:async'; +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +// 注意:运行前需要在 pubspec.yaml 添加 image_gallery_saver 和 permission_handler +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:permission_handler/permission_handler.dart'; + +import '../../core/app_ads_tools.dart'; + +/// 粒子效果类型枚举 +enum EffectType { + fireflies, // 萤火虫/漂浮粒子 + geometric, // 几何连线 + snow, // 飘雪 + galaxy, // 旋转星系 +} + +class ParticleHomePage extends StatefulWidget { + const ParticleHomePage({super.key}); + + @override + State createState() => _ParticleHomePageState(); +} + +class _ParticleHomePageState extends State + with SingleTickerProviderStateMixin { + // 用于动画循环的控制器 + late AnimationController _controller; + // 粒子列表 + final List _particles = []; + // 随机数生成器 + final Random _random = Random(); + // 当前选择的效果 + EffectType _currentEffect = EffectType.fireflies; + // 用于截屏的 Key + final GlobalKey _repaintKey = GlobalKey(); + // 屏幕尺寸缓存 + Size _screenSize = Size.zero; + + @override + void initState() { + super.initState(); + // 初始化动画控制器,无限循环 + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + )..repeat(); // 驱动界面刷新 + + // 监听动画帧,更新粒子状态 + _controller.addListener(_updateParticles); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + /// 初始化或重置粒子系统 + void _initParticles(Size size) { + _particles.clear(); + int count = 0; + + // 根据不同效果设置粒子数量 + switch (_currentEffect) { + case EffectType.fireflies: + count = 100; + break; + case EffectType.geometric: + count = 60; + break; + case EffectType.snow: + count = 150; + break; + case EffectType.galaxy: + count = 200; + break; + } + + for (int i = 0; i < count; i++) { + _particles.add(Particle.random(size, _random, _currentEffect)); + } + } + + /// 每一帧更新粒子位置 + void _updateParticles() { + if (_screenSize == Size.zero) return; + + for (var particle in _particles) { + particle.update(_screenSize, _currentEffect); + } + // 触发重绘 + setState(() {}); + } + + /// 切换效果 + void _switchEffect() { + setState(() { + int nextIndex = (_currentEffect.index + 1) % EffectType.values.length; + _currentEffect = EffectType.values[nextIndex]; + _initParticles(_screenSize); + }); + } + + /// 保存截图到相册 + Future _saveToGallery() async { + try { + // 1. 请求存储权限 + var status = await Permission.storage.request(); + // Android 13+ 可能需要 photos 权限,这里做简单处理,实际需更严谨判断 + if (status.isDenied) { + status = await Permission.photos.request(); + } + + if (status.isGranted || + await Permission.storage.isGranted || + await Permission.photos.isGranted) { + // 2. 获取 RenderRepaintBoundary + RenderRepaintBoundary? boundary = + _repaintKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + + if (boundary == null) return; + + // 3. 转换为图片数据 (高像素密度,保证壁纸清晰度) + ui.Image image = await boundary.toImage(pixelRatio: 3.0); + ByteData? byteData = await image.toByteData( + format: ui.ImageByteFormat.png, + ); + + if (byteData != null) { + final result = await ImageGallerySaver.saveImage( + byteData.buffer.asUint8List(), + quality: 100, + name: "particle_wallpaper_${DateTime.now().millisecondsSinceEpoch}", + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + result['isSuccess'] ? 'Saved to Gallery!' : 'Failed to save.', + ), + backgroundColor: result['isSuccess'] + ? Colors.green + : Colors.red, + ), + ); + } + } + } else { + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text('Permission denied.'))); + } + } + } catch (e) { + debugPrint(e.toString()); + if (mounted) { + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Error: $e'))); + } + } + } + + @override + Widget build(BuildContext context) { + // 获取屏幕尺寸并在第一次时初始化粒子 + final size = MediaQuery.of(context).size; + if (_screenSize != size) { + _screenSize = size; + _initParticles(size); + } + + return Scaffold( + body: Stack( + children: [ + // 1. 绘图层 (被 RepaintBoundary 包裹以用于截图) + RepaintBoundary( + key: _repaintKey, + child: Container( + color: Colors.black, // 背景色 + width: double.infinity, + height: double.infinity, + child: CustomPaint( + painter: ParticlePainter( + particles: _particles, + effect: _currentEffect, + ), + ), + ), + ), + + // 2. 左上角返回按钮 + Positioned( + top: MediaQuery.of(context).padding.top + 8, + left: 8, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.purple.withValues(alpha: 0.3), + Colors.blue.withValues(alpha: 0.3), + ], + ), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.purple.withValues(alpha: 0.3), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + child: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + tooltip: 'Back', + ), + ), + ), + + // 顶部标题卡片 + Positioned( + top: MediaQuery.of(context).padding.top + 16, + left: 0, + right: 0, + child: Center( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.purple.withValues(alpha: 0.3), + Colors.blue.withValues(alpha: 0.3), + ], + ), + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.purple.withValues(alpha: 0.2), + blurRadius: 20, + spreadRadius: 2, + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.auto_awesome, + color: Colors.white, + size: 20, + ), + ), + const SizedBox(width: 12), + const Text( + 'AI Generator', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ], + ), + ), + ), + ), + + // 3. UI 控制层 (半透明,不影响视觉) + Positioned( + bottom: 40, + left: 16, + right: 16, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 效果选择器 + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.purple.withValues(alpha: 0.3), + Colors.blue.withValues(alpha: 0.3), + ], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withValues(alpha: 0.2), + width: 1, + ), + boxShadow: [ + BoxShadow( + color: Colors.purple.withValues(alpha: 0.3), + blurRadius: 20, + spreadRadius: 2, + ), + ], + ), + child: Column( + children: [ + Row( + children: [ + const Icon( + Icons.palette, + color: Colors.white, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'Effect: ${_currentEffect.name.toUpperCase()}', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _buildActionButton( + icon: Icons.refresh, + label: 'Switch Effect', + onTap: _switchEffect, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildActionButton( + icon: Icons.download, + label: 'Save', + onTap: ()async{ + final bool adShown = await AppAdsTools.instance.showAd( + AdPlacement.interstitial3, + onAdClosed: () { + _saveToGallery(); + }, + ); + if (!adShown) { + _saveToGallery(); + } + }, + isPrimary: true, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildActionButton({ + required IconData icon, + required String label, + required VoidCallback onTap, + bool isPrimary = false, + }) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + gradient: isPrimary + ? LinearGradient( + colors: [Colors.purple.shade400, Colors.blue.shade400], + ) + : null, + color: isPrimary ? null : Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isPrimary + ? Colors.white.withValues(alpha: 0.3) + : Colors.white.withValues(alpha: 0.2), + width: 1, + ), + boxShadow: isPrimary + ? [ + BoxShadow( + color: Colors.purple.withValues(alpha: 0.4), + blurRadius: 12, + spreadRadius: 1, + ), + ] + : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, color: Colors.white, size: 20), + const SizedBox(width: 6), + Flexible( + child: Text( + label, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + fontSize: 13, + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + ), + ], + ), + ), + ); + } +} + +/// 粒子数据模型 +class Particle { + double x; + double y; + double vx; // X轴速度 + double vy; // Y轴速度 + double size; + Color color; + double life; // 生命周期 (0.0 - 1.0) + double angle; // 用于旋转效果 + + Particle({ + required this.x, + required this.y, + required this.vx, + required this.vy, + required this.size, + required this.color, + this.life = 1.0, + this.angle = 0.0, + }); + + /// 生成随机粒子 + factory Particle.random(Size screenSize, Random random, EffectType type) { + Color randomColor() { + final colors = [ + Colors.cyanAccent, + Colors.purpleAccent, + Colors.blueAccent, + Colors.pinkAccent, + Colors.tealAccent, + ]; + return colors[random.nextInt(colors.length)]; + } + + double x = random.nextDouble() * screenSize.width; + double y = random.nextDouble() * screenSize.height; + double vx = (random.nextDouble() - 0.5) * 2; + double vy = (random.nextDouble() - 0.5) * 2; + double size = random.nextDouble() * 3 + 1; + Color color = randomColor().withValues( + alpha: random.nextDouble() * 0.5 + 0.3, + ); + + if (type == EffectType.snow) { + vy = random.nextDouble() * 2 + 1; // 向下落 + vx = (random.nextDouble() - 0.5) * 0.5; // 轻微左右飘 + color = Colors.white.withValues(alpha: random.nextDouble() * 0.8 + 0.2); + } else if (type == EffectType.galaxy) { + // 这里的 x, y 会在 update 中根据中心点重新计算,只需初始化角度 + x = screenSize.width / 2; + y = screenSize.height / 2; + } + + return Particle( + x: x, + y: y, + vx: vx, + vy: vy, + size: size, + color: color, + angle: random.nextDouble() * pi * 2, + life: random.nextDouble(), + ); + } + + /// 更新粒子位置和状态 + void update(Size size, EffectType type) { + if (type == EffectType.galaxy) { + // 星系模式:围绕中心旋转 + double centerX = size.width / 2; + double centerY = size.height / 2; + angle += 0.01 * (vx.sign == 0 ? 1 : vx.sign); // 旋转速度 + double radius = this.size * 30 + (life * 150); // 半径 + x = centerX + cos(angle) * radius; + y = centerY + sin(angle) * radius; + return; + } + + x += vx; + y += vy; + + // 边界检测:超出屏幕则反弹或重置 + if (type == EffectType.snow) { + if (y > size.height) { + y = -10; + x = Random().nextDouble() * size.width; + } + } else { + if (x < 0 || x > size.width) vx = -vx; + if (y < 0 || y > size.height) vy = -vy; + } + } +} + +/// 核心绘制逻辑 +class ParticlePainter extends CustomPainter { + final List particles; + final EffectType effect; + + ParticlePainter({required this.particles, required this.effect}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..strokeCap = StrokeCap.round; + + for (var i = 0; i < particles.length; i++) { + var p = particles[i]; + + // 1. 绘制粒子本体 + paint.color = p.color; + paint.strokeWidth = p.size; + + // 不同的绘制形状 + if (effect == EffectType.snow) { + // 雪花带一点模糊 + paint.maskFilter = const MaskFilter.blur(BlurStyle.normal, 2); + canvas.drawCircle(Offset(p.x, p.y), p.size, paint); + } else { + canvas.drawCircle(Offset(p.x, p.y), p.size / 2, paint); + } + + // 2. 几何模式下的连线效果 + if (effect == EffectType.geometric) { + _drawConnections(canvas, p, i, size); + } + } + } + + /// 绘制连线:如果两个粒子距离够近,就画一条线 + void _drawConnections( + Canvas canvas, + Particle p1, + int currentIndex, + Size size, + ) { + Paint linePaint = Paint()..strokeWidth = 1.0; + double connectDistance = 100.0; // 连线阈值 + + for (var j = currentIndex + 1; j < particles.length; j++) { + var p2 = particles[j]; + double dx = p1.x - p2.x; + double dy = p1.y - p2.y; + double dist = sqrt(dx * dx + dy * dy); + + if (dist < connectDistance) { + // 距离越近,线条越不透明 + double opacity = 1.0 - (dist / connectDistance); + linePaint.color = Colors.cyanAccent.withValues(alpha: opacity * 0.5); + canvas.drawLine(Offset(p1.x, p1.y), Offset(p2.x, p2.y), linePaint); + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; // 总是重绘以实现动画 + } +} diff --git a/lib/screens/editor/direct_save_dialog.dart b/lib/screens/editor/direct_save_dialog.dart new file mode 100644 index 0000000..161eada --- /dev/null +++ b/lib/screens/editor/direct_save_dialog.dart @@ -0,0 +1,346 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; + +class DirectSaveDialog extends StatefulWidget { + final GlobalKey previewKey; // 编辑器预览的key + final GlobalKey pureImageKey; // 纯净图片的key + final String fileName; + + const DirectSaveDialog({ + super.key, + required this.previewKey, + required this.pureImageKey, + required this.fileName, + }); + + @override + State createState() => _DirectSaveDialogState(); +} + +class _DirectSaveDialogState extends State { + Uint8List? _previewImageBytes; + bool _isCapturing = true; + bool _isSaving = false; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _capturePreviewImage(); + } + + Future _capturePreviewImage() async { + try { + setState(() { + _isCapturing = true; + _errorMessage = null; + }); + + // 直接从编辑器预览中捕获图片,不需要等待渲染 + final boundary = + widget.previewKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + + if (boundary == null) { + throw Exception('无法获取预览图片'); + } + + // 立即捕获,因为预览已经是渲染好的 + final image = await boundary.toImage(pixelRatio: 1.5); // 适中的分辨率用于预览 + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + + if (byteData == null) { + throw Exception('无法生成预览图片'); + } + + setState(() { + _previewImageBytes = byteData.buffer.asUint8List(); + _isCapturing = false; + }); + } catch (e) { + debugPrint('捕获预览失败: $e'); + setState(() { + _isCapturing = false; + _errorMessage = e.toString(); + }); + } + } + + Future _saveImage() async { + setState(() { + _isSaving = true; + }); + + try { + // 使用纯净图片key进行高质量保存 + final boundary = + widget.pureImageKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + + if (boundary == null) { + throw Exception('无法获取保存图片'); + } + + // 直接捕获纯净图片 + final image = await boundary.toImage(pixelRatio: 3.0); // 高分辨率保存 + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + + if (byteData == null) { + throw Exception('无法生成保存图片'); + } + + final pngBytes = byteData.buffer.asUint8List(); + + // 直接保存字节数据到相册 + final result = await ImageGallerySaver.saveImage( + pngBytes, + name: widget.fileName, + quality: 100, + ); + + final success = result['isSuccess'] == true; + + if (mounted) { + Navigator.of(context).pop(); // 关闭对话框 + + // 显示结果 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success ? '✅ 壁纸已保存到相册!' : '❌ 保存失败,请重试'), + backgroundColor: success ? Colors.green : Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + } catch (e) { + debugPrint('保存失败: $e'); + setState(() { + _isSaving = false; + _errorMessage = '保存失败: $e'; + }); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.8, + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + // 标题栏 + Container( + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.pinkAccent, Colors.purpleAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + const Icon(Icons.save_alt, color: Colors.white, size: 24), + const SizedBox(width: 12), + const Expanded( + child: Text( + '保存确认', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + + // 预览区域 + Expanded( + child: Container( + margin: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[700]!, width: 2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: _buildPreviewContent(), + ), + ), + ), + + // 按钮区域 + Container( + padding: const EdgeInsets.all(20), + child: _buildButtons(), + ), + ], + ), + ), + ); + } + + Widget _buildPreviewContent() { + if (_isCapturing) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(color: Colors.pinkAccent, strokeWidth: 3), + SizedBox(height: 16), + Text( + '正在获取预览...', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ], + ), + ); + } + + if (_errorMessage != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 48), + const SizedBox(height: 16), + const Text( + '获取预览失败', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + _errorMessage!, + style: TextStyle(color: Colors.grey[400], fontSize: 14), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _capturePreviewImage, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.pinkAccent, + foregroundColor: Colors.white, + ), + child: const Text('重新获取'), + ), + ], + ), + ); + } + + if (_previewImageBytes != null) { + return Stack( + fit: StackFit.expand, + children: [ + Image.memory(_previewImageBytes!, fit: BoxFit.contain), + if (_isSaving) + Container( + color: Colors.black.withValues(alpha: 0.7), + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: Colors.pinkAccent, + strokeWidth: 3, + ), + SizedBox(height: 16), + Text( + '正在保存到相册...', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ], + ), + ), + ), + ], + ); + } + + return const SizedBox.shrink(); + } + + Widget _buildButtons() { + if (_errorMessage != null) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('取消', style: TextStyle(color: Colors.grey)), + ), + ], + ); + } + + if (_previewImageBytes == null) { + return const SizedBox.shrink(); + } + + return Row( + children: [ + Expanded( + child: TextButton( + onPressed: _isSaving ? null : () => Navigator.of(context).pop(), + child: const Text('取消', style: TextStyle(color: Colors.grey)), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: ElevatedButton( + onPressed: _isSaving ? null : _saveImage, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.pinkAccent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _isSaving + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text( + '保存到相册', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ), + ], + ); + } +} diff --git a/lib/screens/editor/editor_controls.dart b/lib/screens/editor/editor_controls.dart new file mode 100644 index 0000000..dfb5f67 --- /dev/null +++ b/lib/screens/editor/editor_controls.dart @@ -0,0 +1,221 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:aesthetica_wallpaper/providers/editor_provider.dart'; + +// ------------------------------------- +// --- 4. Editor Controls (EditorControls) --- +// ------------------------------------- +class EditorControls extends StatelessWidget { + const EditorControls({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.grey[900]!, Colors.grey[850]!], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Column( + children: [ + // Top decoration bar + Container( + width: 50, + height: 3, + margin: const EdgeInsets.only( + top: 6, + bottom: 8, + ), // Further reduced margins + decoration: BoxDecoration( + color: Colors.grey[600], + borderRadius: BorderRadius.circular(2), + ), + ), + // Display all controls directly, no tabs + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ), // Reduced horizontal padding + child: Consumer( + builder: (context, provider, child) { + return ListView( + children: [ + // Color controls + _buildSlider( + context, + label: 'Brightness', + value: provider.currentRecipe.brightness, + min: -1.0, + max: 1.0, + onChanged: (val) => provider.setBrightness(val), + icon: Icons.brightness_6, + color: Colors.yellow, + ), + _buildSlider( + context, + label: 'Contrast', + value: provider.currentRecipe.contrast, + min: 0.0, + max: 4.0, + onChanged: (val) => provider.setContrast(val), + icon: Icons.contrast, + color: Colors.orange, + ), + _buildSlider( + context, + label: 'Saturation', + value: provider.currentRecipe.saturation, + min: 0.0, + max: 4.0, + onChanged: (val) => provider.setSaturation(val), + icon: Icons.palette, + color: Colors.pink, + ), + // Filter controls + _buildSlider( + context, + label: 'Blur', + value: provider.currentRecipe.blur, + min: 0.0, + max: 20.0, + onChanged: (val) => provider.setBlur(val), + icon: Icons.blur_on, + color: Colors.blue, + ), + ], + ); + }, + ), + ), + ), + ], + ), + ); + } + + // Helper Widget for creating standard sliders + Widget _buildSlider( + BuildContext context, { + required String label, + required double value, + required double min, + required double max, + int? divisions, + required ValueChanged onChanged, + IconData? icon, + Color? color, + }) { + final accentColor = color ?? Colors.pinkAccent; + + return Container( + margin: const EdgeInsets.only( + bottom: 10, + ), // Further reduced from 16 to 10 + padding: const EdgeInsets.all(8), // Further reduced from 12 to 8 + decoration: BoxDecoration( + color: Colors.grey[850], + borderRadius: BorderRadius.circular(10), // Reduced corner radius + border: Border.all(color: accentColor.withValues(alpha: 0.3), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + if (icon != null) ...[ + Container( + padding: const EdgeInsets.all( + 3, + ), // Further reduced icon container + decoration: BoxDecoration( + color: accentColor.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(5), + ), + child: Icon( + icon, + color: accentColor, + size: 12, + ), // Further reduced icon size + ), + const SizedBox(width: 6), // Further reduced spacing + ], + Text( + label, + style: const TextStyle( + fontSize: 13, // Further reduced font size + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, // Further reduced padding + vertical: 3, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + accentColor.withValues(alpha: 0.8), + accentColor.withValues(alpha: 0.6), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular( + 6, + ), // Reduced corner radius + boxShadow: [ + BoxShadow( + color: accentColor.withValues(alpha: 0.3), + blurRadius: 2, // Further reduced shadow + offset: const Offset(0, 1), + ), + ], + ), + child: Text( + value.toStringAsFixed(1), + style: const TextStyle( + fontSize: 11, // Further reduced font size + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ], + ), + const SizedBox(height: 4), // Further reduced spacing from 8 to 4 + SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: accentColor, + inactiveTrackColor: Colors.grey[700], + thumbColor: accentColor, + overlayColor: accentColor.withValues(alpha: 0.2), + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 6, // Further reduced thumb size + ), + trackHeight: 3, // Further reduced track height + overlayShape: const RoundSliderOverlayShape( + overlayRadius: 12, // Further reduced overlay + ), + ), + child: Slider( + value: value, + min: min, + max: max, + divisions: divisions, + onChanged: onChanged, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/editor/editor_preview.dart b/lib/screens/editor/editor_preview.dart new file mode 100644 index 0000000..a16b48a --- /dev/null +++ b/lib/screens/editor/editor_preview.dart @@ -0,0 +1,345 @@ +import 'dart:async'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle, ByteData; +import 'package:provider/provider.dart'; +import 'package:battery_plus/battery_plus.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:aesthetica_wallpaper/models/recipe.dart'; +import 'package:aesthetica_wallpaper/providers/editor_provider.dart'; +import 'wallpaper_painter.dart'; + +// ------------------------------------- +// --- 2. 编辑器预览 (EditorPreview) --- +// ------------------------------------- +class EditorPreview extends StatefulWidget { + final GlobalKey? repaintBoundaryKey; + final GlobalKey? pureImageKey; // 新增:纯净图片的key + + const EditorPreview({super.key, this.repaintBoundaryKey, this.pureImageKey}); + + @override + State createState() => _EditorPreviewState(); +} + +class _EditorPreviewState extends State { + // State 变量,用于管理异步资源和流 + ui.Image? _loadedImage; + bool _isLoading = true; + String _currentImagePath = ''; + + // 动态效果的当前值 + Color _timeOverlayColor = Colors.transparent; + double _batterySaturationMod = 1.0; // 1.0 = 正常, 0.0 = 黑白 + + // 流订阅 + StreamSubscription? _timeSubscription; + StreamSubscription? _batterySubscription; + + // 异步加载 ui.Image + Future _loadImage(String path) async { + if (mounted) { + setState(() { + _isLoading = true; + }); + } + + try { + final ByteData data = await rootBundle.load(path); + final ui.Codec codec = await ui.instantiateImageCodec( + data.buffer.asUint8List(), + ); + final ui.FrameInfo fi = await codec.getNextFrame(); + + if (mounted) { + setState(() { + _loadedImage = fi.image; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + debugPrint("Error loading image: $e"); + } + } + + // 在 didChangeDependencies 中加载图片和订阅流 + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final provider = Provider.of(context); + final recipe = provider.currentRecipe; + + // 检查图片路径是否已更改 + if (recipe.baseImagePath != _currentImagePath && + recipe.baseImagePath.isNotEmpty) { + setState(() { + _isLoading = true; + _currentImagePath = recipe.baseImagePath; + }); + _loadImage(_currentImagePath); + } + + // 订阅或取消订阅流 + _subscribeToStreams(provider); + } + + // 管理流订阅 + void _subscribeToStreams(EditorProvider provider) { + // ---- 时间感知 ---- + _timeSubscription?.cancel(); + if (provider.isTimeAware) { + _timeSubscription = + Stream.periodic( + const Duration(minutes: 1), + (_) => DateTime.now(), + ).listen((time) { + if (mounted) { + setState(() => _timeOverlayColor = _getTimeAwareOverlay(time)); + } + }); + // 立即设置一次 + _timeOverlayColor = _getTimeAwareOverlay(DateTime.now()); + } else { + _timeOverlayColor = Colors.transparent; + } + + // ---- 电量感知 ---- + _batterySubscription?.cancel(); + if (provider.isBatteryAware) { + final battery = Battery(); + _batterySubscription = battery.onBatteryStateChanged.listen((_) async { + _updateBatteryEffect(battery); + }); + // 立即设置一次 + _updateBatteryEffect(battery); + } else { + _batterySaturationMod = 1.0; + } + } + + // (已修复) + Future _updateBatteryEffect(Battery battery) async { + try { + // 错误 1 修复: + // 'getBatteryLevel()' 不是一个方法。 + // 正确的属性是 '.batteryLevel',它是一个 Future。 + final level = await battery.batteryLevel; + final newState = (level < 20) ? 0.0 : 1.0; // 低于20%则变为黑白 + if (mounted && newState != _batterySaturationMod) { + setState(() => _batterySaturationMod = newState); + } + } catch (e) { + debugPrint("Error getting battery level: $e"); + } + } + + // 在 widget 销毁时取消所有订阅 + @override + void dispose() { + _timeSubscription?.cancel(); + _batterySubscription?.cancel(); + _loadedImage?.dispose(); + super.dispose(); + } + + // 帮助函数: 根据时间获取动态蒙版颜色 + Color _getTimeAwareOverlay(DateTime time) { + final hour = time.hour; + if (hour < 5 || hour > 20) { + // 夜晚 (8 PM - 5 AM) + return Colors.blue.withValues(alpha: 0.3); + } else if (hour < 10) { + // 早晨 (5 AM - 10 AM) + return Colors.yellow.withValues(alpha: 0.15); + } + return Colors.transparent; // 白天 + } + + @override + Widget build(BuildContext context) { + // 监听 EditorProvider 的变化以触发重建 + final provider = context.watch(); + final recipe = provider.currentRecipe; + + return Stack( + children: [ + // 纯净图片渲染区域(用于保存,使用Offstage隐藏) + Offstage( + offstage: true, // 隐藏但仍然渲染 + child: RepaintBoundary( + key: widget.pureImageKey, + child: (_isLoading || _loadedImage == null) + ? const SizedBox(width: 100, height: 100) // 占位符 + : _buildPureImageRenderer(recipe), + ), + ), + // 模拟手机屏幕预览 + _buildPhonePreview(recipe), + ], + ); + } + + // 构建纯净的图片渲染器(用于保存) + Widget _buildPureImageRenderer(Recipe recipe) { + // 获取原始图片尺寸 + final imageWidth = _loadedImage!.width.toDouble(); + final imageHeight = _loadedImage!.height.toDouble(); + + Widget canvasWidget = CustomPaint( + size: Size(imageWidth, imageHeight), + painter: WallpaperPainter( + image: _loadedImage!, + recipe: recipe, + timeOverlay: _timeOverlayColor, + batterySaturation: _batterySaturationMod, + ), + ); + + // 应用像素化效果 + if (recipe.pixelate > 1.0) { + final Matrix4 pixelMatrix = Matrix4.identity(); + final double scale = 1.0 / recipe.pixelate; + pixelMatrix.scaleByDouble(scale, scale, 1.0, 1.0); + + return ImageFiltered( + imageFilter: ui.ImageFilter.matrix( + pixelMatrix.storage, + filterQuality: ui.FilterQuality.none, + ), + child: canvasWidget, + ); + } + + return canvasWidget; + } + + // 构建手机预览界面 + Widget _buildPhonePreview(Recipe recipe) { + return Container( + margin: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.black, + border: Border.all(color: Colors.grey[700]!, width: 4), + borderRadius: BorderRadius.circular(40), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(36), + child: RepaintBoundary( + key: widget.repaintBoundaryKey, + child: Stack( + fit: StackFit.expand, + children: [ + // --- 核心渲染区 --- + (_isLoading || _loadedImage == null) + ? const Center(child: CircularProgressIndicator()) + : _buildCanvasRenderer(recipe), + + // --- 模拟手机 UI (保持在顶部) --- + _buildMockUI(context), + ], + ), + ), + ), + ); + } + + // (已修复) + Widget _buildCanvasRenderer(Recipe recipe) { + // 我们的 "像素化" 效果是一个特例 + // 它通过 ImageFiltered hack 实现,所以我们把它放在 CustomPaint 的 *外部* + + Widget canvasWidget = CustomPaint( + // 错误 1, 2, 3 修复: + // 确保所有参数都在 CustomPaint 构造函数内部 + painter: WallpaperPainter( + image: _loadedImage!, + recipe: recipe, + timeOverlay: _timeOverlayColor, + batterySaturation: _batterySaturationMod, + ), + // 必须有一个 child 才能让 CustomPaint 获得大小 + child: const SizedBox.expand(), + ); + + // 应用像素化 Hack + if (recipe.pixelate > 1.0) { + // 错误 4, 5, 6 修复: + // 确保 `Matrix4` 逻辑在 CustomPaint *外部* + final Matrix4 pixelMatrix = Matrix4.identity(); + final double scale = 1.0 / recipe.pixelate; + pixelMatrix.scaleByDouble(scale, scale, 1.0, 1.0); + + return ImageFiltered( + imageFilter: ui.ImageFilter.matrix( + pixelMatrix.storage, // 明确传递 .storage (一个 Float64List) + filterQuality: ui.FilterQuality.none, // 关键:使用最近邻插值 + ), + child: canvasWidget, + ); + } + + return canvasWidget; + } + + // 模拟手机UI + Widget _buildMockUI(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 模拟状态栏 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '9:41', + style: GoogleFonts.lato( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + const Row( + children: [ + Icon( + Icons.signal_cellular_alt, + color: Colors.white, + size: 16, + ), + SizedBox(width: 4), + Icon(Icons.wifi, color: Colors.white, size: 16), + SizedBox(width: 4), + Icon(Icons.battery_full, color: Colors.white, size: 16), + ], + ), + ], + ), + ), + // 模拟 DOCK 栏 + Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(24), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Icon(Icons.phone, color: Colors.white, size: 32), + Icon(Icons.message, color: Colors.white, size: 32), + Icon(Icons.camera_alt, color: Colors.white, size: 32), + Icon(Icons.music_note, color: Colors.white, size: 32), + ], + ), + ), + ], + ); + } +} diff --git a/lib/screens/editor/editor_screen.dart b/lib/screens/editor/editor_screen.dart new file mode 100644 index 0000000..e09732a --- /dev/null +++ b/lib/screens/editor/editor_screen.dart @@ -0,0 +1,272 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:aesthetica_wallpaper/models/recipe.dart'; +import 'package:aesthetica_wallpaper/providers/editor_provider.dart'; +import 'package:aesthetica_wallpaper/providers/recipe_provider.dart'; +import 'package:aesthetica_wallpaper/screens/preview/full_screen_preview_dialog.dart'; +import 'editor_preview.dart'; +import 'editor_controls.dart'; +import 'simple_save_dialog.dart'; + +// ------------------------------------ +// --- 1. Editor Screen (EditorScreen) --- +// ------------------------------------ +class EditorScreen extends StatelessWidget { + const EditorScreen({super.key}); + + @override + Widget build(BuildContext context) { + final editorProvider = Provider.of(context, listen: false); + final GlobalKey repaintBoundaryKey = GlobalKey(); + final GlobalKey pureImageKey = GlobalKey(); // Pure image key + + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: const Text( + 'Workshop', + style: TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: Colors.transparent, + elevation: 0, + iconTheme: const IconThemeData(color: Colors.white), + actions: [ + // Full screen button + Container( + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + icon: const Icon(Icons.fullscreen, color: Colors.white), + tooltip: 'Full Screen Preview', + onPressed: () => _showFullScreenPreview(context, editorProvider), + ), + ), + // Save button + Container( + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Colors.pinkAccent, Colors.purpleAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + icon: const Icon(Icons.download, color: Colors.white), + tooltip: 'Save to Gallery', + onPressed: () => _showSimpleSave(context), + ), + ), + // Share button + Container( + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + icon: const Icon(Icons.share, color: Colors.blue), + tooltip: 'Share Creation', + onPressed: () => _shareCreation(), + ), + ), + // Favorite button + Consumer( + builder: (context, recipeProvider, child) { + final isFavorited = recipeProvider.recipes.any( + (recipe) => + recipe.baseImagePath == + editorProvider.currentRecipe.baseImagePath, + ); + + return Container( + margin: const EdgeInsets.only(right: 8), + decoration: BoxDecoration( + color: isFavorited + ? Colors.pinkAccent.withValues(alpha: 0.2) + : Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + icon: Icon( + isFavorited ? Icons.favorite : Icons.favorite_border, + color: isFavorited ? Colors.pinkAccent : Colors.white, + ), + tooltip: isFavorited + ? 'Remove from Favorites' + : 'Add to Favorites', + onPressed: () { + final currentRecipe = editorProvider.currentRecipe; + final existingRecipe = recipeProvider.recipes.firstWhere( + (recipe) => + recipe.baseImagePath == currentRecipe.baseImagePath, + orElse: () => Recipe(id: '', baseImagePath: ''), + ); + + if (existingRecipe.id.isEmpty) { + recipeProvider.addRecipe(currentRecipe); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Row( + children: [ + Icon(Icons.favorite, color: Colors.white), + SizedBox(width: 8), + Text('Added to favorites!'), + ], + ), + backgroundColor: Colors.pinkAccent, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + duration: const Duration(seconds: 2), + ), + ); + } else { + recipeProvider.deleteRecipe(existingRecipe.id); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Row( + children: [ + Icon(Icons.heart_broken, color: Colors.white), + SizedBox(width: 8), + Text('Removed from favorites'), + ], + ), + backgroundColor: Colors.orange, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + duration: const Duration(seconds: 2), + ), + ); + } + }, + ), + ); + }, + ), + // Save recipe button + Container( + margin: const EdgeInsets.only(right: 16), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: IconButton( + icon: const Icon(Icons.save, color: Colors.green), + tooltip: 'Save Recipe', + onPressed: () { + final recipe = editorProvider.currentRecipe; + Provider.of( + context, + listen: false, + ).addRecipe(recipe); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: const Row( + children: [ + Icon(Icons.check_circle, color: Colors.white), + SizedBox(width: 8), + Text('Recipe Saved!'), + ], + ), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + ); + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + body: SafeArea( + child: Column( + children: [ + // Preview area (takes available space) + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + child: EditorPreview( + repaintBoundaryKey: repaintBoundaryKey, + pureImageKey: pureImageKey, + ), + ), + ), + // Minimal spacing between preview and controls + const SizedBox(height: 8), + // Control area (fixed height) + Container( + height: 160, + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 10, + spreadRadius: 2, + offset: const Offset(0, -2), + ), + ], + ), + child: const EditorControls(), + ), + ], + ), + ), + ); + } + + // Show full screen preview + void _showFullScreenPreview( + BuildContext context, + EditorProvider editorProvider, + ) { + showDialog( + context: context, + barrierDismissible: true, + builder: (context) => + FullScreenPreviewDialog(recipe: editorProvider.currentRecipe), + ); + } + + // Show simple save dialog + void _showSimpleSave(BuildContext context) { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => SimpleSaveDialog( + fileName: 'MoodCanvas_${DateTime.now().millisecondsSinceEpoch}', + ), + ); + } + + // Share creation functionality + void _shareCreation() { + SharePlus.instance.share( + ShareParams( + text: + 'Check out my amazing wallpaper creation! 🎨\n\nMade with MoodCanvas:Walls - the ultimate wallpaper creator app. Express your creativity and personalize your device!\n\nDownload MoodCanvas:Walls: https://moodcanvas.app', + subject: 'My MoodCanvas:Walls Creation', + ), + ); + } +} diff --git a/lib/screens/editor/save_preview_dialog.dart b/lib/screens/editor/save_preview_dialog.dart new file mode 100644 index 0000000..3e28016 --- /dev/null +++ b/lib/screens/editor/save_preview_dialog.dart @@ -0,0 +1,409 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:aesthetica_wallpaper/services/image_save_service.dart'; + +class SavePreviewDialog extends StatefulWidget { + final GlobalKey repaintBoundaryKey; + final String fileName; + + const SavePreviewDialog({ + super.key, + required this.repaintBoundaryKey, + required this.fileName, + }); + + @override + State createState() => _SavePreviewDialogState(); +} + +class _SavePreviewDialogState extends State { + Uint8List? _previewImageBytes; + bool _isRendering = true; + bool _isSaving = false; + String? _errorMessage; + + @override + void initState() { + super.initState(); + _renderPreview(); + } + + Future _renderPreview() async { + try { + setState(() { + _isRendering = true; + _errorMessage = null; + }); + + // 使用更强的等待策略 + await Future.delayed(const Duration(milliseconds: 200)); + + // 等待当前帧完成 + await WidgetsBinding.instance.endOfFrame; + + // 额外等待确保渲染完成 + await Future.delayed(const Duration(milliseconds: 1200)); // 增加到1.2秒 + + // 获取 RepaintBoundary + final boundary = + widget.repaintBoundaryKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + + if (boundary == null) { + throw Exception('无法获取RepaintBoundary'); + } + + // 多次尝试等待绘制完成 + int waitAttempts = 0; + const maxWaitAttempts = 15; // 增加尝试次数 + + while (boundary.debugNeedsPaint && waitAttempts < maxWaitAttempts) { + debugPrint('等待绘制完成... 尝试 ${waitAttempts + 1}/$maxWaitAttempts'); + await Future.delayed( + Duration(milliseconds: 400 * (waitAttempts + 1)), + ); // 增加等待时间 + waitAttempts++; + } + + // 如果仍在绘制,尝试强制刷新 + if (boundary.debugNeedsPaint) { + debugPrint('强制刷新后再次尝试...'); + + // 触发重绘 + WidgetsBinding.instance.addPostFrameCallback((_) {}); + await Future.delayed(const Duration(milliseconds: 800)); + + // 最后一次检查 + if (boundary.debugNeedsPaint) { + throw Exception('渲染超时,建议使用"直接保存"功能'); + } + } + + // 捕获图片 + final image = await boundary.toImage(pixelRatio: 2.0); + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + + if (byteData == null) { + throw Exception('无法生成预览图片'); + } + + setState(() { + _previewImageBytes = byteData.buffer.asUint8List(); + _isRendering = false; + }); + } catch (e) { + debugPrint('渲染预览失败: $e'); + setState(() { + _isRendering = false; + _errorMessage = e.toString(); + }); + } + } + + Future _saveImage() async { + if (_previewImageBytes == null) return; + + setState(() { + _isSaving = true; + }); + + try { + // 使用高分辨率重新捕获并保存 + final success = await ImageSaveService.saveImageToGallery( + widget.repaintBoundaryKey, + fileName: widget.fileName, + ); + + if (mounted) { + Navigator.of(context).pop(); // 关闭对话框 + + // 显示结果 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success ? '✅ 壁纸已保存到相册!' : '❌ 保存失败,请重试'), + backgroundColor: success ? Colors.green : Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + } catch (e) { + setState(() { + _isSaving = false; + _errorMessage = '保存失败: $e'; + }); + } + } + + // 直接保存,跳过预览 + Future _directSave() async { + setState(() { + _isSaving = true; + }); + + try { + // 直接使用原始保存服务 + final success = await ImageSaveService.saveImageToGallery( + widget.repaintBoundaryKey, + fileName: widget.fileName, + ); + + if (mounted) { + Navigator.of(context).pop(); // 关闭对话框 + + // 显示结果 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success ? '✅ 壁纸已保存到相册!' : '❌ 保存失败,请重试'), + backgroundColor: success ? Colors.green : Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + } catch (e) { + setState(() { + _isSaving = false; + _errorMessage = '直接保存失败: $e'; + }); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.8, + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + // 标题栏 + Container( + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.pinkAccent, Colors.purpleAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + const Icon(Icons.preview, color: Colors.white, size: 24), + const SizedBox(width: 12), + const Expanded( + child: Text( + '保存预览', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + ), + ], + ), + ), + + // 预览区域 + Expanded( + child: Container( + margin: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[700]!, width: 2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: _buildPreviewContent(), + ), + ), + ), + + // 按钮区域 + Container( + padding: const EdgeInsets.all(20), + child: _buildButtons(), + ), + ], + ), + ), + ); + } + + Widget _buildPreviewContent() { + if (_isRendering) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(color: Colors.pinkAccent, strokeWidth: 3), + SizedBox(height: 16), + Text( + '正在渲染预览...', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + SizedBox(height: 8), + Text( + '复杂效果可能需要更长时间', + style: TextStyle(color: Colors.grey, fontSize: 12), + ), + ], + ), + ); + } + + if (_errorMessage != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 48), + const SizedBox(height: 16), + const Text( + '渲染失败', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + _errorMessage!, + style: TextStyle(color: Colors.grey[400], fontSize: 14), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: _renderPreview, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.pinkAccent, + foregroundColor: Colors.white, + ), + child: const Text('重新渲染'), + ), + ElevatedButton( + onPressed: _directSave, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + ), + child: const Text('直接保存'), + ), + ], + ), + ], + ), + ); + } + + if (_previewImageBytes != null) { + return Stack( + fit: StackFit.expand, + children: [ + Image.memory(_previewImageBytes!, fit: BoxFit.contain), + if (_isSaving) + Container( + color: Colors.black.withValues(alpha: 0.7), + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: Colors.pinkAccent, + strokeWidth: 3, + ), + SizedBox(height: 16), + Text( + '正在保存到相册...', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ], + ), + ), + ), + ], + ); + } + + return const SizedBox.shrink(); + } + + Widget _buildButtons() { + if (_errorMessage != null) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('取消', style: TextStyle(color: Colors.grey)), + ), + ], + ); + } + + if (_previewImageBytes == null) { + return const SizedBox.shrink(); + } + + return Row( + children: [ + Expanded( + child: TextButton( + onPressed: _isSaving ? null : () => Navigator.of(context).pop(), + child: const Text('取消', style: TextStyle(color: Colors.grey)), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: ElevatedButton( + onPressed: _isSaving ? null : _saveImage, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.pinkAccent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _isSaving + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text( + '保存到相册', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ), + ], + ); + } +} diff --git a/lib/screens/editor/simple_save_dialog.dart b/lib/screens/editor/simple_save_dialog.dart new file mode 100644 index 0000000..c9fdb29 --- /dev/null +++ b/lib/screens/editor/simple_save_dialog.dart @@ -0,0 +1,409 @@ +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:aesthetica_wallpaper/core/app_ads_tools.dart'; +import 'package:flutter/material.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:provider/provider.dart'; +import 'package:aesthetica_wallpaper/providers/editor_provider.dart'; +import 'package:aesthetica_wallpaper/screens/editor/wallpaper_painter.dart'; + +class SimpleSaveDialog extends StatefulWidget { + final String fileName; + + const SimpleSaveDialog({super.key, required this.fileName}); + + @override + State createState() => _SimpleSaveDialogState(); +} + +class _SimpleSaveDialogState extends State { + Uint8List? _previewImageBytes; + bool _isGenerating = true; + bool _isSaving = false; + String? _errorMessage; + ui.Image? _baseImage; + + @override + void initState() { + super.initState(); + _generatePreview(); + } + + Future _generatePreview() async { + try { + setState(() { + _isGenerating = true; + _errorMessage = null; + }); + + final provider = Provider.of(context, listen: false); + final recipe = provider.currentRecipe; + + // 加载基础图片 + final ByteData data = await DefaultAssetBundle.of( + context, + ).load(recipe.baseImagePath); + final ui.Codec codec = await ui.instantiateImageCodec( + data.buffer.asUint8List(), + ); + final ui.FrameInfo frameInfo = await codec.getNextFrame(); + _baseImage = frameInfo.image; + + // 创建画布并绘制 + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + + // 设置画布尺寸(预览用较小尺寸) + const double previewWidth = 300; + const double previewHeight = 400; + + // 使用 WallpaperPainter 绘制 + final painter = WallpaperPainter( + image: _baseImage!, + recipe: recipe, + timeOverlay: Colors.transparent, // 简化,不使用动态效果 + batterySaturation: 1.0, + ); + + painter.paint(canvas, const Size(previewWidth, previewHeight)); + + // 生成图片 + final picture = recorder.endRecording(); + final image = await picture.toImage( + previewWidth.toInt(), + previewHeight.toInt(), + ); + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + + if (byteData != null) { + setState(() { + _previewImageBytes = byteData.buffer.asUint8List(); + _isGenerating = false; + }); + } else { + throw Exception('Failed to generate preview image'); + } + } catch (e) { + debugPrint('Preview generation failed: $e'); + setState(() { + _isGenerating = false; + _errorMessage = e.toString(); + }); + } + } + + Future _saveImage() async { + if (_baseImage == null) return; + + setState(() { + _isSaving = true; + }); + + try { + final provider = Provider.of(context, listen: false); + final recipe = provider.currentRecipe; + + // 创建高分辨率画布 + final recorder = ui.PictureRecorder(); + final canvas = Canvas(recorder); + + // 使用原始图片尺寸 + final double saveWidth = _baseImage!.width.toDouble(); + final double saveHeight = _baseImage!.height.toDouble(); + + // 使用 WallpaperPainter 绘制高质量版本 + final painter = WallpaperPainter( + image: _baseImage!, + recipe: recipe, + timeOverlay: Colors.transparent, + batterySaturation: 1.0, + ); + + painter.paint(canvas, Size(saveWidth, saveHeight)); + + // 生成高质量图片 + final picture = recorder.endRecording(); + final image = await picture.toImage( + saveWidth.toInt(), + saveHeight.toInt(), + ); + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + + if (byteData == null) { + throw Exception('Failed to generate save image'); + } + + final pngBytes = byteData.buffer.asUint8List(); + + // Save to gallery + final result = await ImageGallerySaver.saveImage( + pngBytes, + name: widget.fileName, + quality: 100, + ); + + final success = result['isSuccess'] == true; + + if (mounted) { + Navigator.of(context).pop(); // 关闭对话框 + + // Show result + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + success + ? '✅ Wallpaper saved to gallery!' + : '❌ Save failed, please try again', + ), + backgroundColor: success ? Colors.green : Colors.red, + duration: const Duration(seconds: 3), + ), + ); + } + } catch (e) { + debugPrint('Save failed: $e'); + setState(() { + _isSaving = false; + _errorMessage = 'Save failed: $e'; + }); + } + } + + @override + void dispose() { + _baseImage?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + child: Container( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.8, + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + // Title bar + Container( + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [Colors.pinkAccent, Colors.purpleAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + const Icon(Icons.save_alt, color: Colors.white, size: 24), + const SizedBox(width: 12), + const Expanded( + child: Text( + 'Save Wallpaper', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.white), + onPressed: ()async{ + + // Navigator.of(context).pop(); + final bool adShown = await AppAdsTools.instance.showAd( + AdPlacement.interstitial2, + onAdClosed: () { + Navigator.of(context).pop(); + }, + ); + if (!adShown) { + Navigator.of(context).pop(); + } + }, + ), + ], + ), + ), + + // Preview area + Expanded( + child: Container( + margin: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[700]!, width: 2), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: _buildPreviewContent(), + ), + ), + ), + + // Button area + Container( + padding: const EdgeInsets.all(20), + child: _buildButtons(), + ), + ], + ), + ), + ); + } + + Widget _buildPreviewContent() { + if (_isGenerating) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(color: Colors.pinkAccent, strokeWidth: 3), + SizedBox(height: 16), + Text( + 'Generating preview...', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ], + ), + ); + } + + if (_errorMessage != null) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 48), + const SizedBox(height: 16), + const Text( + 'Preview generation failed', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + _errorMessage!, + style: TextStyle(color: Colors.grey[400], fontSize: 14), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: _generatePreview, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.pinkAccent, + foregroundColor: Colors.white, + ), + child: const Text('Regenerate'), + ), + ], + ), + ); + } + + if (_previewImageBytes != null) { + return Stack( + fit: StackFit.expand, + children: [ + Image.memory(_previewImageBytes!, fit: BoxFit.contain), + if (_isSaving) + Container( + color: Colors.black.withValues(alpha: 0.7), + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: Colors.pinkAccent, + strokeWidth: 3, + ), + SizedBox(height: 16), + Text( + 'Saving to gallery...', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ], + ), + ), + ), + ], + ); + } + + return const SizedBox.shrink(); + } + + Widget _buildButtons() { + if (_errorMessage != null) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel', style: TextStyle(color: Colors.grey)), + ), + ], + ); + } + + if (_previewImageBytes == null) { + return const SizedBox.shrink(); + } + + return Row( + children: [ + Expanded( + child: TextButton( + onPressed: _isSaving ? null : () => Navigator.of(context).pop(), + child: const Text('Cancel', style: TextStyle(color: Colors.grey)), + ), + ), + const SizedBox(width: 16), + Expanded( + flex: 2, + child: ElevatedButton( + onPressed: _isSaving ? null : _saveImage, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.pinkAccent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: _isSaving + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text( + 'Save to Gallery', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ), + ), + ], + ); + } +} diff --git a/lib/screens/editor/wallpaper_painter.dart b/lib/screens/editor/wallpaper_painter.dart new file mode 100644 index 0000000..466305f --- /dev/null +++ b/lib/screens/editor/wallpaper_painter.dart @@ -0,0 +1,217 @@ +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:aesthetica_wallpaper/models/recipe.dart'; + +// ------------------------------------- +// --- 3. 画布绘制器 (WallpaperPainter) --- +// ------------------------------------- +class WallpaperPainter extends CustomPainter { + final ui.Image image; + final Recipe recipe; + final Color timeOverlay; + final double batterySaturation; // 1.0 = 正常, 0.0 = 黑白 + + WallpaperPainter({ + required this.image, + required this.recipe, + required this.timeOverlay, + required this.batterySaturation, + }); + + // 辅助函数,用于根据 亮度、对比度、饱和度 生成 5x5 颜色矩阵 + ColorFilter _buildColorMatrix() { + // 亮度 (-1 to 1, default 0) + final double brightness = recipe.brightness; + // 对比度 (0 to 4, default 1) + final double contrast = recipe.contrast; + // 饱和度 (0 to 4, default 1) + final double saturation = recipe.saturation * batterySaturation; // 应用电量效果 + + // 矩阵从单位矩阵开始 + List matrix = [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ]; + + // 1. 应用饱和度 + if (saturation != 1.0) { + final sat = saturation; + const lumR = 0.3086; + const lumG = 0.6094; + const lumB = 0.0820; + matrix = [ + lumR * (1 - sat) + sat, + lumG * (1 - sat), + lumB * (1 - sat), + 0, + 0, + lumR * (1 - sat), + lumG * (1 - sat) + sat, + lumB * (1 - sat), + 0, + 0, + lumR * (1 - sat), + lumG * (1 - sat), + lumB * (1 - sat) + sat, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ]; + } + + // 2. 应用对比度 + if (contrast != 1.0) { + final translate = (1.0 - contrast) * 128; + // 注意:这里需要矩阵乘法,为了简单起见,我们假设在饱和度*之后*应用 + matrix[0] *= contrast; + matrix[5] *= contrast; + matrix[10] *= contrast; + matrix[4] += translate; + matrix[9] += translate; + matrix[14] += translate; + } + + // 3. 应用亮度 + if (brightness != 0.0) { + final b = brightness * 255; + matrix[4] += b; + matrix[9] += b; + matrix[14] += b; + } + + return ColorFilter.matrix(matrix); + } + + // 获取字体样式 + TextStyle _getFont() { + final style = TextStyle(fontSize: 32, color: Color(recipe.textColor)); + + try { + if (recipe.fontFamily == 'Roboto') { + return GoogleFonts.roboto( + fontSize: 32, + color: Color(recipe.textColor), + fontWeight: FontWeight.bold, + ); + } + // 默认 + return GoogleFonts.lato(fontSize: 32, color: Color(recipe.textColor)); + } catch (e) { + // 字体加载失败时的回退 + return style; + } + } + + @override + void paint(Canvas canvas, Size size) { + // 裁剪画布,防止绘制到边界之外 + canvas.clipRect(Offset.zero & size); + + // --- 1. 准备画笔 --- + final Paint paint = Paint() + ..filterQuality = FilterQuality.low; // 默认使用低质量(更快) + + // 定义源矩形 (整张图片) + final Rect srcRect = Rect.fromLTWH( + 0, + 0, + image.width.toDouble(), + image.height.toDouble(), + ); + // 定义目标矩形 (填满画布) + final Rect dstRect = Rect.fromLTWH(0, 0, size.width, size.height); + + // --- 2. 应用滤镜 --- + // 应用颜色矩阵 (B/C/S + Battery) + paint.colorFilter = _buildColorMatrix(); + + // 应用模糊 + if (recipe.blur > 0.0) { + paint.imageFilter = ui.ImageFilter.blur( + sigmaX: recipe.blur, + sigmaY: recipe.blur, + ); + } + + // --- 3. 绘制图片 --- + // `drawImageRect` 会使用 `paint` 中定义的滤镜来绘制 + canvas.drawImageRect(image, srcRect, dstRect, paint); + + // --- 4. 绘制动态蒙版 --- + if (timeOverlay.a > 0) { + canvas.drawRect(dstRect, Paint()..color = timeOverlay); + } + + // --- 5. 绘制文字 (使用 TextPainter) --- + if (recipe.overlayText.isNotEmpty) { + final textPainter = TextPainter( + text: TextSpan(text: recipe.overlayText, style: _getFont()), + textDirection: ui.TextDirection.ltr, + textAlign: TextAlign.center, + ); + + // 布局文字,限制最大宽度 + textPainter.layout(maxWidth: size.width - 40); // 左右各留 20 padding + + // 计算居中位置 + final offset = Offset( + (size.width - textPainter.width) / 2, + (size.height - textPainter.height) / 2, + ); + + // 绘制文字 + textPainter.paint(canvas, offset); + } + } + + // 优化 shouldRepaint + // 仅当绘制所需的数据发生变化时才重绘 + @override + bool shouldRepaint(covariant WallpaperPainter oldDelegate) { + // 简单的比较 (因为 Recipe 是可变的,这可能不总是触发) + // return oldDelegate.image != image || + // oldDelegate.recipe != recipe || + // oldDelegate.timeOverlay != timeOverlay || + // oldDelegate.batterySaturation != batterySaturation; + + // 优化:比较所有字段 + final oldRecipe = oldDelegate.recipe; + final newRecipe = recipe; + + return oldDelegate.image != image || + oldRecipe.baseImagePath != newRecipe.baseImagePath || + oldRecipe.brightness != newRecipe.brightness || + oldRecipe.contrast != newRecipe.contrast || + oldRecipe.saturation != newRecipe.saturation || + oldRecipe.blur != newRecipe.blur || + oldRecipe.pixelate != newRecipe.pixelate || + oldRecipe.overlayText != newRecipe.overlayText || + oldRecipe.fontFamily != newRecipe.fontFamily || + oldRecipe.textColor != newRecipe.textColor || + oldDelegate.timeOverlay != timeOverlay || + oldDelegate.batterySaturation != batterySaturation; + } +} diff --git a/lib/screens/favorites/favorites_screen.dart b/lib/screens/favorites/favorites_screen.dart new file mode 100644 index 0000000..3f1bc58 --- /dev/null +++ b/lib/screens/favorites/favorites_screen.dart @@ -0,0 +1,277 @@ +import 'package:aesthetica_wallpaper/providers/recipe_provider.dart'; +import 'package:aesthetica_wallpaper/providers/editor_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:aesthetica_wallpaper/models/recipe.dart'; + +class FavoritesScreen extends StatelessWidget { + const FavoritesScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Favorites'), + backgroundColor: Colors.grey[900], + elevation: 0, + ), + body: Consumer( + builder: (context, provider, child) { + if (provider.recipes.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.favorite_border, + size: 80, + color: Colors.grey[600], + ), + const SizedBox(height: 16), + Text( + 'No Favorites Yet', + style: TextStyle( + color: Colors.grey[400], + fontSize: 20, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 8), + Text( + 'Start creating and saving your favorite wallpapers!', + style: TextStyle(color: Colors.grey[500], fontSize: 14), + textAlign: TextAlign.center, + ), + ], + ), + ); + } + + return GridView.builder( + padding: const EdgeInsets.fromLTRB( + 16, + 16, + 16, + 100, + ), // 增加底部内边距避免被底部导航栏遮挡 + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.8, + ), + itemCount: provider.recipes.length, + itemBuilder: (context, index) { + final recipe = provider.recipes[index]; + return _buildFavoriteCard(context, recipe, provider); + }, + ); + }, + ), + ); + } + + Widget _buildFavoriteCard( + BuildContext context, + Recipe recipe, + RecipeProvider provider, + ) { + return Container( + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: InkWell( + onTap: () { + // 加载配方到编辑器 + Provider.of( + context, + listen: false, + ).loadFromRecipe(recipe); + Navigator.pushNamed(context, '/editor'); + }, + child: Stack( + fit: StackFit.expand, + children: [ + // 壁纸预览 + Image.asset( + recipe.baseImagePath, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return Container( + color: Colors.grey[700], + child: const Icon( + Icons.image_not_supported, + color: Colors.grey, + size: 40, + ), + ); + }, + ), + // 渐变蒙版 + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + Colors.black.withValues(alpha: 0.7), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + // 收藏按钮 + Positioned( + top: 8, + right: 8, + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.5), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.favorite, + color: Colors.pinkAccent, + size: 20, + ), + ), + ), + // 底部信息 + Positioned( + bottom: 0, + left: 0, + right: 0, + child: Container( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + _getRecipeName(recipe.baseImagePath), + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + _getRecipeDescription(recipe), + style: TextStyle(color: Colors.grey[300], fontSize: 12), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ), + // 删除按钮 + Positioned( + top: 8, + left: 8, + child: GestureDetector( + onTap: () => _showDeleteDialog(context, recipe, provider), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + color: Colors.red.withValues(alpha: 0.8), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.delete_outline, + color: Colors.white, + size: 16, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + String _getRecipeName(String imagePath) { + final fileName = imagePath.split('/').last; + // 移除文件扩展名 + return fileName.split('.').first; + } + + String _getRecipeDescription(Recipe recipe) { + final effects = []; + + if (recipe.brightness != 0.0) { + effects.add('Brightness: ${recipe.brightness.toStringAsFixed(1)}'); + } + if (recipe.contrast != 1.0) { + effects.add('Contrast: ${recipe.contrast.toStringAsFixed(1)}'); + } + if (recipe.saturation != 1.0) { + effects.add('Saturation: ${recipe.saturation.toStringAsFixed(1)}'); + } + if (recipe.blur > 0.0) { + effects.add('Blur: ${recipe.blur.toStringAsFixed(1)}'); + } + if (recipe.pixelate > 1.0) { + effects.add('Pixelate: ${recipe.pixelate.toStringAsFixed(1)}'); + } + if (recipe.overlayText.isNotEmpty) { + effects.add('Text: ${recipe.overlayText}'); + } + + if (effects.isEmpty) { + return 'Original image'; + } + + return effects.take(2).join(', '); + } + + void _showDeleteDialog( + BuildContext context, + Recipe recipe, + RecipeProvider provider, + ) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.grey[800], + title: const Text( + 'Remove from Favorites', + style: TextStyle(color: Colors.white), + ), + content: const Text( + 'Are you sure you want to remove this wallpaper from your favorites?', + style: TextStyle(color: Colors.white), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel', style: TextStyle(color: Colors.grey)), + ), + TextButton( + onPressed: () { + provider.deleteRecipe(recipe.id); + Navigator.of(context).pop(); + }, + child: const Text('Remove', style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } +} diff --git a/lib/screens/home/gallery_screen.dart b/lib/screens/home/gallery_screen.dart new file mode 100644 index 0000000..3aa6af7 --- /dev/null +++ b/lib/screens/home/gallery_screen.dart @@ -0,0 +1,254 @@ +import 'package:aesthetica_wallpaper/providers/editor_provider.dart'; +import 'package:aesthetica_wallpaper/providers/recipe_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:aesthetica_wallpaper/models/image_category.dart'; +import 'package:aesthetica_wallpaper/models/recipe.dart'; + +class GalleryScreen extends StatelessWidget { + const GalleryScreen({super.key}); + + @override + Widget build(BuildContext context) { + // 从路由参数中获取分类数据 + final category = + ModalRoute.of(context)!.settings.arguments as ImageCategory; + + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + title: Text( + category.name, + style: const TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + backgroundColor: Colors.grey[900], + elevation: 0, + iconTheme: const IconThemeData(color: Colors.white), + ), + body: Column( + children: [ + // 分类信息卡片 + Container( + width: double.infinity, + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Colors.pinkAccent, Colors.purpleAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon(Icons.image, color: Colors.white, size: 24), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + category.name, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + '${category.images.length} wallpapers available', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 14, + ), + ), + ], + ), + ), + ], + ), + ), + // 图片网格 + Expanded( + child: GridView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.85, // 调整宽高比,让图片更方正 + ), + itemCount: category.images.length, + itemBuilder: (context, index) { + final imageName = category.images[index]; + final imagePath = category.getImagePath(imageName); + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Stack( + fit: StackFit.expand, + children: [ + // 图片 + Image.asset(imagePath, fit: BoxFit.cover), + // 渐变蒙版 + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.transparent, + Colors.black.withValues(alpha: 0.3), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + // 点击区域 + Positioned.fill( + child: InkWell( + onTap: () { + Provider.of( + context, + listen: false, + ).startEditing(imagePath); + Navigator.pushNamed(context, '/editor'); + }, + borderRadius: BorderRadius.circular(16), + ), + ), + // 收藏按钮 + Positioned( + top: 8, + right: 8, + child: Consumer( + builder: (context, recipeProvider, child) { + final isFavorited = recipeProvider.recipes.any( + (recipe) => recipe.baseImagePath == imagePath, + ); + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => _toggleFavorite( + context, + imagePath, + recipeProvider, + ), + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + shape: BoxShape.circle, + ), + child: Icon( + isFavorited + ? Icons.favorite + : Icons.favorite_border, + color: isFavorited + ? Colors.pinkAccent + : Colors.white, + size: 18, + ), + ), + ); + }, + ), + ), + // 图片序号 + Positioned( + bottom: 8, + left: 8, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + '${index + 1}', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + // 切换收藏状态 + void _toggleFavorite( + BuildContext context, + String imagePath, + RecipeProvider recipeProvider, + ) { + final existingRecipe = recipeProvider.recipes.firstWhere( + (recipe) => recipe.baseImagePath == imagePath, + orElse: () => Recipe(id: '', baseImagePath: ''), + ); + + if (existingRecipe.id.isEmpty) { + // 创建新的收藏 + final newRecipe = Recipe( + id: DateTime.now().millisecondsSinceEpoch.toString(), + baseImagePath: imagePath, + ); + recipeProvider.addRecipe(newRecipe); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Added to favorites!'), + backgroundColor: Colors.green, + duration: Duration(seconds: 2), + ), + ); + } else { + // 取消收藏 + recipeProvider.deleteRecipe(existingRecipe.id); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Removed from favorites'), + backgroundColor: Colors.orange, + duration: Duration(seconds: 2), + ), + ); + } + } +} diff --git a/lib/screens/home/home_screen.dart b/lib/screens/home/home_screen.dart new file mode 100644 index 0000000..d2667e3 --- /dev/null +++ b/lib/screens/home/home_screen.dart @@ -0,0 +1,671 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:aesthetica_wallpaper/models/image_category.dart'; +import 'package:aesthetica_wallpaper/providers/editor_provider.dart'; +import 'package:aesthetica_wallpaper/screens/puzzle/drag_puzzle_menu_screen.dart'; +import 'package:aesthetica_wallpaper/screens/home/recommendation_list_screen.dart'; +import 'dart:math' as math; + +import '../aigenerate/ai_generate.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State + with SingleTickerProviderStateMixin { + late Future> _categoriesFuture; + late PageController _bannerController; + late AnimationController _animationController; + + @override + void initState() { + super.initState(); + _categoriesFuture = _loadCategories(); + _bannerController = PageController(); + _animationController = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), + )..repeat(); + + // 自动轮播Banner + Future.delayed(const Duration(seconds: 3), _autoPlayBanner); + } + + void _autoPlayBanner() { + if (!mounted) return; + _bannerController.nextPage( + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + ); + Future.delayed(const Duration(seconds: 3), _autoPlayBanner); + } + + @override + void dispose() { + _bannerController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + // 直接读取assets文件夹中的图片 + Future> _loadCategories() async { + final categories = []; + + // 定义类别配置 + final categoryConfigs = [ + {'name': 'Nature', 'folder': 'nature'}, + {'name': 'Abstract', 'folder': 'abstract'}, + {'name': 'Architecture', 'folder': 'architecture'}, + {'name': 'Animals', 'folder': 'animals'}, + {'name': 'Food', 'folder': 'food'}, + {'name': 'Travel', 'folder': 'travel'}, + ]; + + for (final config in categoryConfigs) { + try { + // 尝试读取文件夹中的图片 + final images = await _getImagesFromFolder(config['folder']!); + if (images.isNotEmpty) { + categories.add( + ImageCategory( + name: config['name']!, + folder: config['folder']!, + images: images, + ), + ); + } + } catch (e) { + debugPrint('Error loading category ${config['name']}: $e'); + } + } + + return categories; + } + + // 获取指定文件夹中的所有图片 + Future> _getImagesFromFolder(String folderName) async { + // 根据实际assets文件夹中的图片文件列表 + final knownImages = { + 'nature': [ + 'nature1.png', + 'nature2.png', + 'nature3.png', + 'nature4.png', + 'nature5.png', + 'nature6.png', + 'nature7.png', + 'nature8.png', + 'nature9.png', + ], + 'abstract': [ + 'abstract1.png', + 'abstract2.png', + 'abstract3.png', + 'abstract4.png', + 'abstract5.png', + 'abstract6.png', + 'abstract7.png', + 'abstract8.png', + 'abstract9.png', + 'abstract10.png', + 'abstract11.png', + 'abstract12.png', + 'abstract13.png', + 'abstract14.png', + 'abstract15.png', + ], + 'architecture': [ + 'architecture1.png', + 'architecture2.png', + 'architecture3.png', + 'architecture4.png', + 'architecture5.png', + 'architecture6.png', + 'architecture7.png', + 'architecture8.png', + 'architecture9.png', + 'architecture10.png', + ], + 'animals': [ + 'animals1.png', + 'animals2.png', + 'animals3.png', + 'animals4.png', + 'animals5.png', + 'animals6.png', + 'animals7.png', + ], + 'food': [ + 'food1.png', + 'food2.png', + 'food3.png', + 'food4.png', + 'food5.png', + 'food6.png', + 'food7.png', + 'food8.png', + 'food9.png', + 'food10.png', + 'food11.png', + 'food12.png', + 'food13.png', + ], + 'travel': [ + 'travel5.png', + 'travel1.png', + 'travel2.png', + 'travel3.png', + 'travel4.png', + 'travel6.png', + 'travel7.png', + 'travel8.png', + 'travel9.png', + ], + }; + + return knownImages[folderName] ?? []; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: FutureBuilder>( + future: _categoriesFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError) { + return Center( + child: Text('Error loading categories: ${snapshot.error}'), + ); + } + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return const Center(child: Text('No categories found.')); + } + + final categories = snapshot.data!; + + return CustomScrollView( + slivers: [ + // 美化的AppBar + _buildSliverAppBar(), + + // Banner轮播 + SliverToBoxAdapter(child: _buildBannerSection(categories)), + + // 快速功能入口 + SliverToBoxAdapter(child: _buildQuickActions()), + + // 每日推荐 + SliverToBoxAdapter(child: _buildDailyRecommendation(categories)), + + // 热门分类标题 + SliverToBoxAdapter( + child: _buildSectionTitle( + 'Categories', + 'Explore beautiful wallpapers', + ), + ), + + // 分类网格 + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 0.75, + ), + delegate: SliverChildBuilderDelegate((context, index) { + return _buildCategoryGridItem(context, categories[index]); + }, childCount: categories.length), + ), + ), + + // 底部间距 + const SliverToBoxAdapter(child: SizedBox(height: 100)), + ], + ); + }, + ), + ); + } + + // 美化的SliverAppBar + Widget _buildSliverAppBar() { + return SliverAppBar( + expandedHeight: 120, + floating: false, + pinned: true, + flexibleSpace: FlexibleSpaceBar( + title: const Text( + 'MoodCanvas', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 24), + ), + background: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.purple.shade400, Colors.blue.shade400], + ), + ), + ), + ), + ); + } + + // Banner轮播区域 - 只保留两个:拼图和推荐 + Widget _buildBannerSection(List categories) { + final bannerItems = [ + { + 'title': '🎮 Puzzle Game', + 'subtitle': 'Challenge your mind with wall papers', + 'color': Colors.purple, + 'icon': Icons.extension, + 'action': 'puzzle', + }, + { + 'title': '⭐ Daily Picks', + 'subtitle': 'Discover today\'s best wallpapers', + 'color': Colors.orange, + 'icon': Icons.star, + 'action': 'recommendation', + }, + ]; + + return Container( + height: 145, + margin: const EdgeInsets.symmetric(vertical: 16), + child: PageView.builder( + controller: _bannerController, + itemCount: bannerItems.length, + itemBuilder: (context, index) { + final item = bannerItems[index]; + return _buildBannerItem( + item['title'] as String, + item['subtitle'] as String, + item['color'] as Color, + item['icon'] as IconData, + item['action'] as String, + ); + }, + ), + ); + } + + Widget _buildBannerItem( + String title, + String subtitle, + Color color, + IconData icon, + String action, + ) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [color, color.withValues(alpha: 0.7)], + ), + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: 0.3), + blurRadius: 15, + offset: const Offset(0, 8), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () { + if (action == 'puzzle') { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DragPuzzleMenuScreen(), + ), + ); + } else if (action == 'recommendation') { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RecommendationListScreen(), + ), + ); + } + }, + child: Padding( + padding: const EdgeInsets.all(24), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + Text( + subtitle, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.9), + fontSize: 14, + ), + ), + ], + ), + ), + AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.rotate( + angle: _animationController.value * 2 * math.pi, + child: Icon( + icon, + size: 60, + color: Colors.white.withValues(alpha: 0.3), + ), + ); + }, + ), + ], + ), + ), + ), + ), + ); + } + + // 快速功能入口 - 只保留拼图和AI生成 + Widget _buildQuickActions() { + final actions = [ + {'icon': Icons.extension, 'label': 'Puzzle', 'color': Colors.purple}, + { + 'icon': Icons.auto_awesome, + 'label': 'AI Generate', + 'color': Colors.blue, + }, + ]; + + return Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + Expanded( + child: _buildQuickActionItem( + actions[0]['icon'] as IconData, + actions[0]['label'] as String, + actions[0]['color'] as Color, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildQuickActionItem( + actions[1]['icon'] as IconData, + actions[1]['label'] as String, + actions[1]['color'] as Color, + ), + ), + ], + ), + ); + } + + Widget _buildQuickActionItem(IconData icon, String label, Color color) { + return InkWell( + onTap: () { + // 根据不同功能跳转 + if (label == 'Puzzle') { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const DragPuzzleMenuScreen(), + ), + ); + } else { + //AI Generate + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const ParticleHomePage()), + ); + } + }, + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + color.withValues(alpha: 0.2), + color.withValues(alpha: 0.1), + ], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: color.withValues(alpha: 0.3), width: 1), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.15), + shape: BoxShape.circle, + ), + child: Icon(icon, color: color, size: 36), + ), + const SizedBox(height: 12), + Text( + label, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: color, + ), + textAlign: TextAlign.center, + ), + ], + ), + ), + ); + } + + // 每日推荐区域 + Widget _buildDailyRecommendation(List categories) { + if (categories.isEmpty) return const SizedBox.shrink(); + + final random = math.Random(); + final randomCategory = categories[random.nextInt(categories.length)]; + final randomImages = randomCategory.images.take(5).toList(); + + return Column( + children: [ + _buildSectionTitle('Daily Picks', 'Curated wallpapers for you'), + SizedBox( + height: 150, + child: ListView.builder( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: randomImages.length, + itemBuilder: (context, index) { + return _buildRecommendationItem( + randomCategory.folder, + randomImages[index], + ); + }, + ), + ), + ], + ); + } + + Widget _buildRecommendationItem(String folder, String imageName) { + final imagePath = 'assets/images/$folder/$imageName'; + + return GestureDetector( + onTap: () { + // 初始化编辑器并跳转 + Provider.of( + context, + listen: false, + ).startEditing(imagePath); + Navigator.pushNamed(context, '/editor'); + }, + child: Container( + width: 140, + margin: const EdgeInsets.only(right: 12), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Stack( + fit: StackFit.expand, + children: [ + Image.asset(imagePath, fit: BoxFit.cover), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withValues(alpha: 0.5), + ], + ), + ), + ), + const Positioned( + bottom: 8, + left: 8, + right: 8, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Icon(Icons.favorite_border, color: Colors.white, size: 20), + Icon(Icons.edit, color: Colors.white, size: 20), + ], + ), + ), + ], + ), + ), + ), + ); + } + + // 区域标题 + Widget _buildSectionTitle(String title, String subtitle) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 24, 16, 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: TextStyle(fontSize: 14, color: Colors.grey.shade600), + ), + ], + ), + ], + ), + ); + } + + // 分类网格项 + Widget _buildCategoryGridItem(BuildContext context, ImageCategory category) { + return InkWell( + onTap: () { + Navigator.pushNamed(context, '/gallery', arguments: category); + }, + borderRadius: BorderRadius.circular(16), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Stack( + fit: StackFit.expand, + children: [ + Image.asset(category.getThumbnailPath(), fit: BoxFit.cover), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withValues(alpha: 0.7), + ], + ), + ), + ), + Positioned( + bottom: 12, + left: 12, + right: 12, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + category.name, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + '${category.images.length} wallpapers', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/home/recommendation_list_screen.dart b/lib/screens/home/recommendation_list_screen.dart new file mode 100644 index 0000000..d649bd3 --- /dev/null +++ b/lib/screens/home/recommendation_list_screen.dart @@ -0,0 +1,253 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:aesthetica_wallpaper/providers/editor_provider.dart'; + +/// 推荐列表页面 - 从每个类别随机选择3张图片 +class RecommendationListScreen extends StatefulWidget { + const RecommendationListScreen({super.key}); + + @override + State createState() => + _RecommendationListScreenState(); +} + +class _RecommendationListScreenState extends State { + final Random _random = Random(); + List _recommendedImages = []; + + // 所有可用的图片列表(按类别组织) + static const Map> _allImages = { + 'nature': [ + 'assets/images/nature/nature1.png', + 'assets/images/nature/nature2.png', + 'assets/images/nature/nature3.png', + 'assets/images/nature/nature4.png', + 'assets/images/nature/nature5.png', + 'assets/images/nature/nature6.png', + 'assets/images/nature/nature7.png', + 'assets/images/nature/nature8.png', + 'assets/images/nature/nature9.png', + ], + 'abstract': [ + 'assets/images/abstract/abstract1.png', + 'assets/images/abstract/abstract2.png', + 'assets/images/abstract/abstract3.png', + 'assets/images/abstract/abstract4.png', + 'assets/images/abstract/abstract5.png', + 'assets/images/abstract/abstract6.png', + 'assets/images/abstract/abstract7.png', + 'assets/images/abstract/abstract8.png', + 'assets/images/abstract/abstract9.png', + 'assets/images/abstract/abstract10.png', + 'assets/images/abstract/abstract11.png', + 'assets/images/abstract/abstract12.png', + 'assets/images/abstract/abstract13.png', + 'assets/images/abstract/abstract14.png', + 'assets/images/abstract/abstract15.png', + ], + 'architecture': [ + 'assets/images/architecture/architecture1.png', + 'assets/images/architecture/architecture2.png', + 'assets/images/architecture/architecture3.png', + 'assets/images/architecture/architecture4.png', + 'assets/images/architecture/architecture5.png', + 'assets/images/architecture/architecture6.png', + 'assets/images/architecture/architecture7.png', + 'assets/images/architecture/architecture8.png', + 'assets/images/architecture/architecture9.png', + 'assets/images/architecture/architecture10.png', + ], + 'animals': [ + 'assets/images/animals/animals1.png', + 'assets/images/animals/animals2.png', + 'assets/images/animals/animals3.png', + 'assets/images/animals/animals4.png', + 'assets/images/animals/animals5.png', + 'assets/images/animals/animals6.png', + 'assets/images/animals/animals7.png', + ], + 'food': [ + 'assets/images/food/food1.png', + 'assets/images/food/food2.png', + 'assets/images/food/food3.png', + 'assets/images/food/food4.png', + 'assets/images/food/food5.png', + 'assets/images/food/food6.png', + 'assets/images/food/food7.png', + 'assets/images/food/food8.png', + 'assets/images/food/food9.png', + 'assets/images/food/food10.png', + 'assets/images/food/food11.png', + 'assets/images/food/food12.png', + 'assets/images/food/food13.png', + ], + 'travel': [ + 'assets/images/travel/travel1.png', + 'assets/images/travel/travel2.png', + 'assets/images/travel/travel3.png', + 'assets/images/travel/travel4.png', + 'assets/images/travel/travel5.png', + 'assets/images/travel/travel6.png', + 'assets/images/travel/travel7.png', + 'assets/images/travel/travel8.png', + 'assets/images/travel/travel9.png', + ], + }; + + @override + void initState() { + super.initState(); + _loadRecommendedImages(); + } + + void _loadRecommendedImages() { + final images = []; + + // 从每个类别随机选择3张图片 + _allImages.forEach((category, categoryImages) { + final shuffled = List.from(categoryImages)..shuffle(_random); + images.addAll(shuffled.take(3)); + }); + + // 打乱最终列表 + images.shuffle(_random); + + setState(() { + _recommendedImages = images; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: CustomScrollView( + slivers: [ + // AppBar + SliverAppBar( + expandedHeight: 120, + floating: false, + pinned: true, + flexibleSpace: FlexibleSpaceBar( + title: const Text( + 'Daily Picks', + style: TextStyle(fontWeight: FontWeight.bold), + ), + background: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.orange.shade400, Colors.pink.shade400], + ), + ), + ), + ), + actions: [ + IconButton( + icon: const Icon(Icons.refresh), + tooltip: 'Refresh', + onPressed: _loadRecommendedImages, + ), + ], + ), + + // 推荐说明 + SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.orange.withValues(alpha: 0.3)), + ), + child: const Row( + children: [ + Icon(Icons.info_outline, color: Colors.orange), + SizedBox(width: 12), + Expanded( + child: Text( + 'Curated wallpapers from all categories, tap to view details', + style: TextStyle(fontSize: 14), + ), + ), + ], + ), + ), + ), + + // 图片网格 + SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 0.7, + ), + delegate: SliverChildBuilderDelegate((context, index) { + return _buildImageItem(_recommendedImages[index]); + }, childCount: _recommendedImages.length), + ), + ), + + // 底部间距 + const SliverToBoxAdapter(child: SizedBox(height: 100)), + ], + ), + ); + } + + Widget _buildImageItem(String imagePath) { + return GestureDetector( + onTap: () { + // 初始化编辑器并跳转 + Provider.of( + context, + listen: false, + ).startEditing(imagePath); + Navigator.pushNamed(context, '/editor'); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Stack( + fit: StackFit.expand, + children: [ + Image.asset(imagePath, fit: BoxFit.cover), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Colors.transparent, + Colors.black.withValues(alpha: 0.3), + ], + ), + ), + ), + const Positioned( + bottom: 8, + right: 8, + child: Icon(Icons.edit, color: Colors.white, size: 24), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/screens/messages/messages_screen.dart b/lib/screens/messages/messages_screen.dart new file mode 100644 index 0000000..d1a4ff0 --- /dev/null +++ b/lib/screens/messages/messages_screen.dart @@ -0,0 +1,232 @@ +import 'package:flutter/material.dart'; +import 'package:aesthetica_wallpaper/models/message.dart'; + +class MessagesScreen extends StatelessWidget { + const MessagesScreen({super.key}); + + // 应用相关消息数据 + static final List _mockMessages = [ + Message( + id: '1', + senderName: 'Aesthetica Team', + content: + '🎉 Welcome to Aesthetica Wallpaper! Create stunning mood-based wallpapers with our intuitive editor and stay updated with real-time weather.', + timestamp: DateTime.now().subtract(const Duration(minutes: 30)), + avatarUrl: 'https://i.pravatar.cc/150?img=1', + isRead: false, + ), + Message( + id: '2', + senderName: 'Weather Update', + content: + '🌤️ New Weather Feature Added! Check real-time weather for any city worldwide, get 3-day forecasts, and save your favorite locations.', + timestamp: DateTime.now().subtract(const Duration(hours: 2)), + avatarUrl: 'https://i.pravatar.cc/150?img=2', + isRead: false, + ), + Message( + id: '3', + senderName: 'Design Tips', + content: + '🎨 Pro Tip: Use our Recipe system to save your favorite wallpaper designs and recreate them anytime. Perfect for creating consistent themes!', + timestamp: DateTime.now().subtract(const Duration(hours: 6)), + avatarUrl: 'https://i.pravatar.cc/150?img=3', + isRead: true, + ), + Message( + id: '4', + senderName: 'Feature Highlight', + content: + '✨ Explore our curated wallpaper collections: Nature scenes, Abstract art, and Architecture. Each category offers unique creative possibilities.', + timestamp: DateTime.now().subtract(const Duration(days: 1)), + avatarUrl: 'https://i.pravatar.cc/150?img=4', + isRead: true, + ), + Message( + id: '5', + senderName: 'Aesthetica Team', + content: + '💝 Thank you for choosing Aesthetica! Your creativity inspires us. Share your feedback to help us make the app even better.', + timestamp: DateTime.now().subtract(const Duration(days: 2)), + avatarUrl: 'https://i.pravatar.cc/150?img=5', + isRead: true, + ), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Messages'), + backgroundColor: Colors.grey[900], + elevation: 0, + ), + body: ListView.builder( + padding: const EdgeInsets.fromLTRB( + 16, + 16, + 16, + 100, + ), // 增加底部内边距避免被底部导航栏遮挡 + itemCount: _mockMessages.length, + itemBuilder: (context, index) { + final message = _mockMessages[index]; + return _buildMessageCard(context, message); + }, + ), + ); + } + + Widget _buildMessageCard(BuildContext context, Message message) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + child: InkWell( + onTap: () { + // 可以添加点击消息的详细页面 + _showMessageDetail(context, message); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(12), + border: message.isRead + ? null + : Border.all(color: Colors.pinkAccent, width: 1), + ), + child: Row( + children: [ + // 头像 + CircleAvatar( + radius: 25, + backgroundColor: Colors.pinkAccent, + child: Text( + message.senderName[0].toUpperCase(), + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 12), + // 消息内容 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + message.senderName, + style: TextStyle( + color: message.isRead + ? Colors.grey[300] + : Colors.white, + fontSize: 16, + fontWeight: message.isRead + ? FontWeight.normal + : FontWeight.bold, + ), + ), + Text( + message.formattedTime, + style: TextStyle( + color: Colors.grey[400], + fontSize: 12, + ), + ), + ], + ), + const SizedBox(height: 4), + Text( + message.content, + style: TextStyle( + color: message.isRead + ? Colors.grey[400] + : Colors.grey[200], + fontSize: 14, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + // 未读标识 + if (!message.isRead) + Container( + width: 8, + height: 8, + decoration: const BoxDecoration( + color: Colors.pinkAccent, + shape: BoxShape.circle, + ), + ), + ], + ), + ), + ), + ); + } + + void _showMessageDetail(BuildContext context, Message message) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.grey[800], + title: Row( + children: [ + CircleAvatar( + radius: 20, + backgroundColor: Colors.pinkAccent, + child: Text( + message.senderName[0].toUpperCase(), + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + message.senderName, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + message.formattedTime, + style: TextStyle(color: Colors.grey[400], fontSize: 12), + ), + ], + ), + ), + ], + ), + content: Text( + message.content, + style: const TextStyle(color: Colors.white, fontSize: 16), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text( + 'Close', + style: TextStyle(color: Colors.pinkAccent), + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/preview/full_screen_preview_dialog.dart b/lib/screens/preview/full_screen_preview_dialog.dart new file mode 100644 index 0000000..c6231f4 --- /dev/null +++ b/lib/screens/preview/full_screen_preview_dialog.dart @@ -0,0 +1,353 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle, ByteData; +import 'dart:ui' as ui; +import 'package:aesthetica_wallpaper/models/recipe.dart'; +import 'wallpaper_preview_painter.dart'; + +// 全屏预览对话框 +class FullScreenPreviewDialog extends StatefulWidget { + final Recipe recipe; + + const FullScreenPreviewDialog({super.key, required this.recipe}); + + @override + State createState() => + _FullScreenPreviewDialogState(); +} + +class _FullScreenPreviewDialogState extends State { + bool _isHomeScreen = true; // true = 主页, false = 锁屏 + ui.Image? _loadedImage; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + _loadImage(); + } + + // 异步加载图片 + Future _loadImage() async { + try { + final ByteData data = await rootBundle.load(widget.recipe.baseImagePath); + final ui.Codec codec = await ui.instantiateImageCodec( + data.buffer.asUint8List(), + ); + final ui.FrameInfo fi = await codec.getNextFrame(); + + if (mounted) { + setState(() { + _loadedImage = fi.image; + _isLoading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() => _isLoading = false); + } + debugPrint("Error loading image: $e"); + } + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final isSmallScreen = screenSize.width < 600 || screenSize.height < 800; + + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: EdgeInsets.all(isSmallScreen ? 8 : 16), + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(isSmallScreen ? 12 : 20), + ), + child: Stack( + children: [ + // 全屏壁纸预览 + Positioned.fill( + child: ClipRRect( + borderRadius: BorderRadius.circular(isSmallScreen ? 12 : 20), + child: _buildWallpaperPreview(isSmallScreen), + ), + ), + // 顶部控制栏 + Positioned( + top: 0, + left: 0, + right: 0, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: isSmallScreen ? 12 : 20, + vertical: isSmallScreen ? 8 : 16, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.black.withValues(alpha: 0.8), + Colors.transparent, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 关闭按钮 + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: Icon( + Icons.close, + color: Colors.white, + size: isSmallScreen ? 20 : 24, + ), + padding: EdgeInsets.all(isSmallScreen ? 4 : 8), + constraints: const BoxConstraints(), + ), + // 预览模式切换 + Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular( + isSmallScreen ? 16 : 20, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildModeButton( + 'Home', + Icons.home, + true, + isSmallScreen, + ), + _buildModeButton( + 'Lock', + Icons.lock, + false, + isSmallScreen, + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildModeButton( + String label, + IconData icon, + bool isHome, + bool isSmallScreen, + ) { + final isSelected = _isHomeScreen == isHome; + return GestureDetector( + onTap: () { + setState(() { + _isHomeScreen = isHome; + }); + }, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: isSmallScreen ? 10 : 16, + vertical: isSmallScreen ? 6 : 8, + ), + decoration: BoxDecoration( + color: isSelected ? Colors.pinkAccent : Colors.transparent, + borderRadius: BorderRadius.circular(isSmallScreen ? 12 : 16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + color: isSelected ? Colors.white : Colors.grey[400], + size: isSmallScreen ? 14 : 16, + ), + SizedBox(width: isSmallScreen ? 3 : 4), + Text( + label, + style: TextStyle( + color: isSelected ? Colors.white : Colors.grey[400], + fontSize: isSmallScreen ? 11 : 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } + + Widget _buildWallpaperPreview(bool isSmallScreen) { + if (_isLoading) { + return Container( + color: Colors.black, + child: const Center( + child: CircularProgressIndicator(color: Colors.pinkAccent), + ), + ); + } + + if (_loadedImage == null) { + return Container( + color: Colors.grey[800], + child: const Center( + child: Icon(Icons.image_not_supported, color: Colors.grey, size: 60), + ), + ); + } + + return Stack( + fit: StackFit.expand, + children: [ + // 壁纸图片 + CustomPaint( + painter: WallpaperPreviewPainter( + image: _loadedImage!, + recipe: widget.recipe, + ), + child: Container(), + ), + // UI覆盖层 + _isHomeScreen + ? _buildHomeScreenUI(isSmallScreen) + : _buildLockScreenUI(isSmallScreen), + ], + ); + } + + Widget _buildHomeScreenUI(bool isSmallScreen) { + return Stack( + children: [ + // 模拟主页内容 + Positioned( + top: isSmallScreen ? 30 : 50, + left: isSmallScreen ? 12 : 20, + right: isSmallScreen ? 12 : 20, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Good Morning', + style: TextStyle( + color: Colors.white, + fontSize: isSmallScreen ? 18 : 24, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: isSmallScreen ? 4 : 8), + Text( + 'Monday, October 23', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: isSmallScreen ? 13 : 16, + ), + ), + ], + ), + ), + // 底部Dock + Positioned( + bottom: isSmallScreen ? 12 : 20, + left: isSmallScreen ? 12 : 20, + right: isSmallScreen ? 12 : 20, + child: Container( + padding: EdgeInsets.symmetric( + horizontal: isSmallScreen ? 12 : 20, + vertical: isSmallScreen ? 8 : 12, + ), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(isSmallScreen ? 16 : 20), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildDockIcon(Icons.phone, isSmallScreen), + _buildDockIcon(Icons.message, isSmallScreen), + _buildDockIcon(Icons.camera_alt, isSmallScreen), + _buildDockIcon(Icons.music_note, isSmallScreen), + ], + ), + ), + ), + ], + ); + } + + Widget _buildLockScreenUI(bool isSmallScreen) { + return Stack( + children: [ + // 时间显示 + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '9:41', + style: TextStyle( + color: Colors.white, + fontSize: isSmallScreen ? 36 : 48, + fontWeight: FontWeight.w300, + ), + ), + SizedBox(height: isSmallScreen ? 4 : 8), + Text( + 'Monday, October 23', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: isSmallScreen ? 13 : 16, + ), + ), + ], + ), + ), + // 底部控制 + Positioned( + bottom: isSmallScreen ? 30 : 50, + left: 0, + right: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildLockIcon(Icons.flashlight_off, isSmallScreen), + _buildLockIcon(Icons.camera_alt, isSmallScreen), + ], + ), + ), + ], + ); + } + + Widget _buildDockIcon(IconData icon, bool isSmallScreen) { + return Container( + padding: EdgeInsets.all(isSmallScreen ? 8 : 12), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(isSmallScreen ? 10 : 12), + ), + child: Icon(icon, color: Colors.white, size: isSmallScreen ? 20 : 24), + ); + } + + Widget _buildLockIcon(IconData icon, bool isSmallScreen) { + return Container( + padding: EdgeInsets.all(isSmallScreen ? 12 : 16), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: Icon(icon, color: Colors.white, size: isSmallScreen ? 20 : 24), + ); + } +} diff --git a/lib/screens/preview/wallpaper_preview_painter.dart b/lib/screens/preview/wallpaper_preview_painter.dart new file mode 100644 index 0000000..4817596 --- /dev/null +++ b/lib/screens/preview/wallpaper_preview_painter.dart @@ -0,0 +1,158 @@ +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:aesthetica_wallpaper/models/recipe.dart'; + +// 壁纸预览绘制器 +class WallpaperPreviewPainter extends CustomPainter { + final ui.Image image; + final Recipe recipe; + + WallpaperPreviewPainter({required this.image, required this.recipe}); + + // 构建颜色矩阵 + ColorFilter _buildColorMatrix() { + final double brightness = recipe.brightness; + final double contrast = recipe.contrast; + final double saturation = recipe.saturation; + + List matrix = [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ]; + + // 应用饱和度 + if (saturation != 1.0) { + final sat = saturation; + const lumR = 0.3086; + const lumG = 0.6094; + const lumB = 0.0820; + matrix = [ + lumR * (1 - sat) + sat, + lumG * (1 - sat), + lumB * (1 - sat), + 0, + 0, + lumR * (1 - sat), + lumG * (1 - sat) + sat, + lumB * (1 - sat), + 0, + 0, + lumR * (1 - sat), + lumG * (1 - sat), + lumB * (1 - sat) + sat, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ]; + } + + // 应用对比度 + if (contrast != 1.0) { + final translate = (1.0 - contrast) * 128; + matrix[0] *= contrast; + matrix[5] *= contrast; + matrix[10] *= contrast; + matrix[4] += translate; + matrix[9] += translate; + matrix[14] += translate; + } + + // 应用亮度 + if (brightness != 0.0) { + final b = brightness * 255; + matrix[4] += b; + matrix[9] += b; + matrix[14] += b; + } + + return ColorFilter.matrix(matrix); + } + + @override + void paint(Canvas canvas, Size size) { + // 裁剪画布 + canvas.clipRect(Offset.zero & size); + + // 准备画笔 + final Paint paint = Paint()..filterQuality = FilterQuality.low; + + // 定义源矩形和目标矩形 + final Rect srcRect = Rect.fromLTWH( + 0, + 0, + image.width.toDouble(), + image.height.toDouble(), + ); + final Rect dstRect = Rect.fromLTWH(0, 0, size.width, size.height); + + // 应用颜色矩阵 + paint.colorFilter = _buildColorMatrix(); + + // 应用模糊 + if (recipe.blur > 0.0) { + paint.imageFilter = ui.ImageFilter.blur( + sigmaX: recipe.blur, + sigmaY: recipe.blur, + ); + } + + // 绘制图片 + canvas.drawImageRect(image, srcRect, dstRect, paint); + + // 绘制文字叠加 + if (recipe.overlayText.isNotEmpty) { + final textPainter = TextPainter( + text: TextSpan( + text: recipe.overlayText, + style: TextStyle( + fontSize: 32, + color: Color(recipe.textColor), + fontWeight: FontWeight.bold, + ), + ), + textDirection: ui.TextDirection.ltr, + textAlign: TextAlign.center, + ); + + textPainter.layout(maxWidth: size.width - 40); + final offset = Offset( + (size.width - textPainter.width) / 2, + (size.height - textPainter.height) / 2, + ); + textPainter.paint(canvas, offset); + } + } + + @override + bool shouldRepaint(covariant WallpaperPreviewPainter oldDelegate) { + return oldDelegate.image != image || + oldDelegate.recipe.brightness != recipe.brightness || + oldDelegate.recipe.contrast != recipe.contrast || + oldDelegate.recipe.saturation != recipe.saturation || + oldDelegate.recipe.blur != recipe.blur || + oldDelegate.recipe.overlayText != recipe.overlayText || + oldDelegate.recipe.textColor != recipe.textColor; + } +} diff --git a/lib/screens/puzzle/drag_puzzle_menu_screen.dart b/lib/screens/puzzle/drag_puzzle_menu_screen.dart new file mode 100644 index 0000000..07b31a7 --- /dev/null +++ b/lib/screens/puzzle/drag_puzzle_menu_screen.dart @@ -0,0 +1,511 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:aesthetica_wallpaper/models/drag_puzzle_game.dart'; +import 'package:aesthetica_wallpaper/screens/puzzle/drag_puzzle_screen.dart'; + +/// 拖拽式拼图菜单页面 +class DragPuzzleMenuScreen extends StatefulWidget { + const DragPuzzleMenuScreen({super.key}); + + @override + State createState() => _DragPuzzleMenuScreenState(); +} + +class _DragPuzzleMenuScreenState extends State { + DragPuzzleDifficulty _selectedDifficulty = DragPuzzleDifficulty.beginner; + String? _selectedImagePath; + List _availableImages = []; + final Random _random = Random(); + + // 所有可用的图片列表(按类别组织) + static const Map> _allImages = { + 'nature': [ + 'assets/images/nature/nature1.png', + 'assets/images/nature/nature2.png', + 'assets/images/nature/nature3.png', + 'assets/images/nature/nature4.png', + 'assets/images/nature/nature5.png', + 'assets/images/nature/nature6.png', + 'assets/images/nature/nature7.png', + 'assets/images/nature/nature8.png', + 'assets/images/nature/nature9.png', + ], + 'abstract': [ + 'assets/images/abstract/abstract1.png', + 'assets/images/abstract/abstract2.png', + 'assets/images/abstract/abstract3.png', + 'assets/images/abstract/abstract4.png', + 'assets/images/abstract/abstract5.png', + 'assets/images/abstract/abstract6.png', + 'assets/images/abstract/abstract7.png', + 'assets/images/abstract/abstract8.png', + 'assets/images/abstract/abstract9.png', + 'assets/images/abstract/abstract10.png', + 'assets/images/abstract/abstract11.png', + 'assets/images/abstract/abstract12.png', + 'assets/images/abstract/abstract13.png', + 'assets/images/abstract/abstract14.png', + 'assets/images/abstract/abstract15.png', + ], + 'architecture': [ + 'assets/images/architecture/architecture1.png', + 'assets/images/architecture/architecture2.png', + 'assets/images/architecture/architecture3.png', + 'assets/images/architecture/architecture4.png', + 'assets/images/architecture/architecture5.png', + 'assets/images/architecture/architecture6.png', + 'assets/images/architecture/architecture7.png', + 'assets/images/architecture/architecture8.png', + 'assets/images/architecture/architecture9.png', + 'assets/images/architecture/architecture10.png', + ], + 'animals': [ + 'assets/images/animals/animals1.png', + 'assets/images/animals/animals2.png', + 'assets/images/animals/animals3.png', + 'assets/images/animals/animals4.png', + 'assets/images/animals/animals5.png', + 'assets/images/animals/animals6.png', + 'assets/images/animals/animals7.png', + ], + 'food': [ + 'assets/images/food/food1.png', + 'assets/images/food/food2.png', + 'assets/images/food/food3.png', + 'assets/images/food/food4.png', + 'assets/images/food/food5.png', + 'assets/images/food/food6.png', + 'assets/images/food/food7.png', + 'assets/images/food/food8.png', + 'assets/images/food/food9.png', + 'assets/images/food/food10.png', + 'assets/images/food/food11.png', + 'assets/images/food/food12.png', + 'assets/images/food/food13.png', + ], + 'travel': [ + 'assets/images/travel/travel1.png', + 'assets/images/travel/travel2.png', + 'assets/images/travel/travel3.png', + 'assets/images/travel/travel4.png', + 'assets/images/travel/travel5.png', + 'assets/images/travel/travel6.png', + 'assets/images/travel/travel7.png', + 'assets/images/travel/travel8.png', + 'assets/images/travel/travel9.png', + ], + }; + + @override + void initState() { + super.initState(); + _loadAvailableImages(); + } + + void _loadAvailableImages() { + // 从所有类别中随机选择3张图片 + final allImagesList = []; + _allImages.forEach((category, images) { + allImagesList.addAll(images); + }); + + // 打乱列表并选择前3张 + allImagesList.shuffle(_random); + _availableImages = allImagesList.take(3).toList(); + + if (_availableImages.isNotEmpty) { + _selectedImagePath = _availableImages.first; + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.purple.shade400, Colors.blue.shade400], + ), + ), + child: SafeArea( + child: Column( + children: [ + // 顶部栏 + _buildAppBar(), + + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 标题 + _buildTitle(), + const SizedBox(height: 40), + + // 游戏说明 + _buildGameDescription(), + const SizedBox(height: 30), + + // 难度选择 + _buildDifficultySelection(), + const SizedBox(height: 30), + + // 图片选择 + _buildImageSelection(), + const SizedBox(height: 40), + + // 开始游戏按钮 + _buildStartButton(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildAppBar() { + return Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.help_outline, color: Colors.white), + onPressed: () => _showHelpDialog(), + ), + ], + ), + ); + } + + Widget _buildTitle() { + return Column( + children: [ + const Icon(Icons.view_module, size: 80, color: Colors.white), + const SizedBox(height: 16), + const Text( + 'Drag Puzzle', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Text( + 'Drag pieces to the correct position', + style: TextStyle( + fontSize: 16, + color: Colors.white.withValues(alpha: 0.9), + ), + ), + ], + ); + } + + Widget _buildGameDescription() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(Icons.info_outline, color: Colors.white), + SizedBox(width: 8), + Text( + 'Game Rules', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + const SizedBox(height: 12), + _buildRuleItem('1. Image will be split into puzzle pieces'), + _buildRuleItem('2. Pieces are randomly arranged at the bottom'), + _buildRuleItem('3. Drag pieces to the correct position above'), + _buildRuleItem('4. Correctly placed pieces show green checkmark'), + _buildRuleItem('5. Complete all pieces to win'), + ], + ), + ); + } + + Widget _buildRuleItem(String text) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + children: [ + const SizedBox(width: 16), + const Icon(Icons.check_circle_outline, color: Colors.white, size: 16), + const SizedBox(width: 8), + Expanded( + child: Text( + text, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.9), + fontSize: 14, + ), + ), + ), + ], + ), + ); + } + + Widget _buildDifficultySelection() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Select Difficulty', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: DragPuzzleDifficulty.values.map((difficulty) { + return _buildDifficultyChip(difficulty); + }).toList(), + ), + ], + ), + ); + } + + Widget _buildDifficultyChip(DragPuzzleDifficulty difficulty) { + final isSelected = _selectedDifficulty == difficulty; + return InkWell( + onTap: () { + setState(() { + _selectedDifficulty = difficulty; + }); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: isSelected + ? Colors.white + : Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + difficulty.label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: isSelected ? Colors.purple : Colors.white, + ), + ), + const SizedBox(height: 2), + Text( + difficulty.description, + style: TextStyle( + fontSize: 11, + color: isSelected + ? Colors.purple.withValues(alpha: 0.7) + : Colors.white.withValues(alpha: 0.7), + ), + ), + const SizedBox(height: 2), + Text( + '${difficulty.totalPieces} pieces', + style: TextStyle( + fontSize: 10, + color: isSelected + ? Colors.purple.withValues(alpha: 0.6) + : Colors.white.withValues(alpha: 0.6), + ), + ), + ], + ), + ), + ); + } + + Widget _buildImageSelection() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Select Image', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + TextButton.icon( + onPressed: () { + setState(() { + _loadAvailableImages(); + }); + }, + icon: const Icon(Icons.refresh, color: Colors.white, size: 18), + label: const Text( + 'Refresh', + style: TextStyle(color: Colors.white, fontSize: 14), + ), + style: TextButton.styleFrom( + backgroundColor: Colors.white.withValues(alpha: 0.2), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + SizedBox( + height: 100, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: _availableImages.length, + itemBuilder: (context, index) { + final imagePath = _availableImages[index]; + final isSelected = _selectedImagePath == imagePath; + + return GestureDetector( + onTap: () { + setState(() { + _selectedImagePath = imagePath; + }); + }, + child: Container( + width: 80, + margin: const EdgeInsets.only(right: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? Colors.white : Colors.transparent, + width: 3, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Image.asset(imagePath, fit: BoxFit.cover), + ), + ), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildStartButton() { + return ElevatedButton( + onPressed: _selectedImagePath != null ? _startGame : null, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.purple, + padding: const EdgeInsets.symmetric(vertical: 20), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 8, + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.play_arrow, size: 32), + SizedBox(width: 8), + Text( + 'Start Puzzle', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ], + ), + ); + } + + void _startGame() { + if (_selectedImagePath == null) return; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DragPuzzleScreen( + imagePath: _selectedImagePath!, + difficulty: _selectedDifficulty, + ), + ), + ); + } + + void _showHelpDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Game Help'), + content: const Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('🎯 Goal: Drag all pieces to correct positions'), + SizedBox(height: 8), + Text('👆 Control: Long press and drag pieces to target'), + SizedBox(height: 8), + Text('✅ Hint: Correct pieces show green checkmark'), + SizedBox(height: 8), + Text('🔄 Reset: Tap placed pieces to move back'), + SizedBox(height: 8), + Text('⭐ Rating: Based on time and moves'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Got it'), + ), + ], + ), + ); + } +} diff --git a/lib/screens/puzzle/drag_puzzle_screen.dart b/lib/screens/puzzle/drag_puzzle_screen.dart new file mode 100644 index 0000000..5c04c6b --- /dev/null +++ b/lib/screens/puzzle/drag_puzzle_screen.dart @@ -0,0 +1,594 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:aesthetica_wallpaper/models/drag_puzzle_game.dart'; +import 'package:aesthetica_wallpaper/providers/drag_puzzle_provider.dart'; +import 'package:aesthetica_wallpaper/screens/puzzle/puzzle_complete_screen.dart'; + +/// 拖拽式拼图游戏界面 +class DragPuzzleScreen extends StatefulWidget { + final String imagePath; + final DragPuzzleDifficulty difficulty; + + const DragPuzzleScreen({ + super.key, + required this.imagePath, + required this.difficulty, + }); + + @override + State createState() => _DragPuzzleScreenState(); +} + +class _DragPuzzleScreenState extends State { + late DragPuzzleProvider _puzzleProvider; + + @override + void initState() { + super.initState(); + _puzzleProvider = DragPuzzleProvider(); + + // 创建游戏 + WidgetsBinding.instance.addPostFrameCallback((_) { + _puzzleProvider.createDragPuzzle( + imagePath: widget.imagePath, + difficulty: widget.difficulty, + ); + }); + } + + @override + void dispose() { + _puzzleProvider.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + body: SafeArea( + child: ChangeNotifierProvider.value( + value: _puzzleProvider, + child: Consumer( + builder: (context, provider, child) { + final game = provider.currentGame; + + if (game == null) { + return const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator(color: Colors.white), + SizedBox(height: 16), + Text( + 'Preparing puzzle...', + style: TextStyle(color: Colors.white), + ), + ], + ), + ); + } + + // 游戏完成后跳转 + if (game.isComplete) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => PuzzleCompleteScreen(game: game), + ), + ); + }); + } + + return Column( + children: [ + // 顶部信息栏 + _buildTopBar(game), + + // 拼图区域(大幅增加) + Expanded( + flex: 6, // 大幅增加拼图区域 + child: _buildPuzzleArea(game, provider), + ), + + // 分隔线 + Container( + height: 2, + color: Colors.grey.shade300, + margin: const EdgeInsets.symmetric(horizontal: 16), + ), + + // 拼图块区域(单行横向排列) + Container( + height: 100, // 固定高度,单行显示 + child: _buildPiecesArea(game, provider), + ), + + // 底部工具栏(简化) + _buildBottomBar(game, provider), + ], + ); + }, + ), + ), + ), + ); + } + + Widget _buildTopBar(DragPuzzleGame game) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.grey.shade900, + border: Border( + bottom: BorderSide(color: Colors.grey.shade800, width: 1), + ), + ), + child: Row( + children: [ + // 返回按钮 + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + + const Spacer(), + + // 进度显示 + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.blue.withValues(alpha: 0.5)), + ), + child: Text( + '${game.placedPieces.length}/${game.pieces.length}', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + + const SizedBox(width: 8), + + // 计时器 + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.green.withValues(alpha: 0.5)), + ), + child: Row( + children: [ + const Icon(Icons.timer, size: 16, color: Colors.white), + const SizedBox(width: 4), + Text( + _formatDuration(game.elapsedTime), + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + + const SizedBox(width: 8), + + // 移动次数 + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.orange.withValues(alpha: 0.5)), + ), + child: Row( + children: [ + const Icon(Icons.touch_app, size: 16, color: Colors.white), + const SizedBox(width: 4), + Text( + '${game.moves}', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPuzzleArea(DragPuzzleGame game, DragPuzzleProvider provider) { + return Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade900, + borderRadius: BorderRadius.circular(12), + ), + child: _buildPuzzleGrid(game, provider), // 直接显示拼图网格 + ); + } + + Widget _buildPuzzleGrid(DragPuzzleGame game, DragPuzzleProvider provider) { + return LayoutBuilder( + builder: (context, constraints) { + // 使用可用的最大空间 + final maxWidth = constraints.maxWidth; + final maxHeight = constraints.maxHeight; + + // 计算合适的网格大小 + // 对于3行2列,整体比例应该是 (2/3) * (9/16) = 3/8 = 0.375 + // 即宽度是高度的0.375倍 + final gridAspectRatio = (game.gridCols / game.gridRows) * (9.0 / 16.0); + + double gridWidth, gridHeight; + + // 根据可用空间计算网格尺寸 + if (maxWidth / maxHeight < gridAspectRatio) { + // 宽度受限 + gridWidth = maxWidth; + gridHeight = gridWidth / gridAspectRatio; + } else { + // 高度受限 + gridHeight = maxHeight; + gridWidth = gridHeight * gridAspectRatio; + } + + // 每个格子的宽高比(单个格子是9:16) + final cellAspectRatio = 9.0 / 16.0; + + return Center( + child: Container( + width: gridWidth, + height: gridHeight, + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.5), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12), + child: GridView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: game.gridCols, + childAspectRatio: cellAspectRatio, + mainAxisSpacing: 1, + crossAxisSpacing: 1, + ), + itemCount: game.gridRows * game.gridCols, + itemBuilder: (context, index) { + final row = index ~/ game.gridCols; + final col = index % game.gridCols; + return _buildDropTarget(game, provider, row, col); + }, + ), + ), + ), + ); + }, + ); + } + + Widget _buildDropTarget( + DragPuzzleGame game, + DragPuzzleProvider provider, + int row, + int col, + ) { + final piece = game.getPieceAt(row, col); + final isCorrectPosition = piece?.isCorrectlyPlaced ?? false; + + return DragTarget( + onAccept: (draggedPiece) { + provider.placePiece(draggedPiece.id, row, col); + }, + builder: (context, candidateData, rejectedData) { + return Container( + margin: const EdgeInsets.all(1), + decoration: BoxDecoration( + color: candidateData.isNotEmpty + ? Colors.blue.withValues(alpha: 0.3) + : Colors.grey.shade700, + border: Border.all( + color: isCorrectPosition ? Colors.green : Colors.grey.shade600, + width: isCorrectPosition ? 2 : 1, + ), + ), + child: piece != null + ? GestureDetector( + onTap: () => provider.removePiece(piece.id), + child: Stack( + fit: StackFit.expand, + children: [ + Image(image: piece.image, fit: BoxFit.cover), + if (isCorrectPosition) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(2), + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check, + color: Colors.white, + size: 12, + ), + ), + ), + ], + ), + ) + : Center( + child: Text( + '${row * game.gridCols + col + 1}', + style: TextStyle(color: Colors.grey.shade500, fontSize: 12), + ), + ), + ); + }, + ); + } + + Widget _buildPiecesArea(DragPuzzleGame game, DragPuzzleProvider provider) { + final unplacedPieces = game.unplacedPieces; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade900, + border: Border(top: BorderSide(color: Colors.grey.shade800, width: 1)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Pieces (${unplacedPieces.length})', + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Expanded( + child: unplacedPieces.isEmpty + ? const Center( + child: Text( + 'All pieces placed!', + style: TextStyle(color: Colors.grey, fontSize: 14), + ), + ) + : ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: unplacedPieces.length, + itemBuilder: (context, index) { + final piece = unplacedPieces[index]; + return Container( + width: 60, + height: 60, + margin: const EdgeInsets.only(right: 8), + child: _buildDraggablePiece(piece), + ); + }, + ), + ), + ], + ), + ); + } + + Widget _buildDraggablePiece(DragPuzzlePiece piece) { + return Draggable( + data: piece, + feedback: Material( + elevation: 8, + borderRadius: BorderRadius.circular(8), + child: Container( + width: 80, + height: 80, + decoration: BoxDecoration(borderRadius: BorderRadius.circular(8)), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image(image: piece.image, fit: BoxFit.cover), + ), + ), + ), + childWhenDragging: Container( + decoration: BoxDecoration( + color: Colors.grey.shade800, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.grey.shade700, + style: BorderStyle.solid, + ), + ), + child: const Center( + child: Icon(Icons.drag_indicator, color: Colors.grey), + ), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image(image: piece.image, fit: BoxFit.cover), + ), + ), + ); + } + + Widget _buildBottomBar(DragPuzzleGame game, DragPuzzleProvider provider) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.grey.shade900, + border: Border(top: BorderSide(color: Colors.grey.shade800, width: 1)), + ), + child: Row( + children: [ + // 重新开始 + _buildToolButton( + icon: Icons.refresh, + label: 'Restart', + color: Colors.orange, + onPressed: () => _showRestartDialog(provider), + ), + + const Spacer(), + + // 预览图(右下角) + GestureDetector( + onTap: () => _showPreviewDialog(game), + child: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.blue, width: 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: Stack( + fit: StackFit.expand, + children: [ + Image.asset(game.imagePath, fit: BoxFit.cover), + Container( + color: Colors.black.withValues(alpha: 0.3), + child: const Icon( + Icons.search, + color: Colors.white, + size: 24, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildToolButton({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onPressed, + }) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: color.withValues(alpha: 0.3), + shape: BoxShape.circle, + border: Border.all(color: color.withValues(alpha: 0.5)), + ), + child: IconButton( + icon: Icon(icon, color: Colors.white), + onPressed: onPressed, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle(fontSize: 12, color: Colors.grey.shade400), + ), + ], + ); + } + + void _showRestartDialog(DragPuzzleProvider provider) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Restart Game'), + content: const Text( + 'Are you sure you want to restart? Current progress will be lost.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + provider.restartGame(); + Navigator.pop(context); + }, + child: const Text('Restart'), + ), + ], + ), + ); + } + + void _showPreviewDialog(DragPuzzleGame game) { + showDialog( + context: context, + builder: (context) => Dialog( + child: Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Preview', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Image.asset(game.imagePath, fit: BoxFit.contain, height: 300), + const SizedBox(height: 16), + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Close'), + ), + ], + ), + ), + ), + ); + } + + String _formatDuration(Duration duration) { + final minutes = duration.inMinutes.toString().padLeft(2, '0'); + final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; + } +} diff --git a/lib/screens/puzzle/puzzle_complete_screen.dart b/lib/screens/puzzle/puzzle_complete_screen.dart new file mode 100644 index 0000000..e92aba8 --- /dev/null +++ b/lib/screens/puzzle/puzzle_complete_screen.dart @@ -0,0 +1,405 @@ +import 'package:flutter/material.dart'; +import 'package:aesthetica_wallpaper/models/puzzle_game_interface.dart'; +import 'package:confetti/confetti.dart'; + +/// 拼图完成页面 +class PuzzleCompleteScreen extends StatefulWidget { + final IPuzzleGame game; + + const PuzzleCompleteScreen({super.key, required this.game}); + + @override + State createState() => _PuzzleCompleteScreenState(); +} + +class _PuzzleCompleteScreenState extends State + with SingleTickerProviderStateMixin { + late ConfettiController _confettiController; + late AnimationController _animationController; + late Animation _scaleAnimation; + late Animation _fadeAnimation; + + @override + void initState() { + super.initState(); + + _confettiController = ConfettiController( + duration: const Duration(seconds: 3), + ); + + _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), + ); + + _scaleAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.elasticOut, + ); + + _fadeAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeIn, + ); + + // 启动动画和彩带 + _animationController.forward(); + _confettiController.play(); + } + + @override + void dispose() { + _confettiController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final stars = widget.game.getStarRating(); + + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.purple.shade400, Colors.blue.shade400], + ), + ), + child: SafeArea( + child: Stack( + children: [ + // 彩带效果 + Align( + alignment: Alignment.topCenter, + child: ConfettiWidget( + confettiController: _confettiController, + blastDirectionality: BlastDirectionality.explosive, + particleDrag: 0.05, + emissionFrequency: 0.05, + numberOfParticles: 50, + gravity: 0.1, + shouldLoop: false, + colors: const [ + Colors.green, + Colors.blue, + Colors.pink, + Colors.orange, + Colors.purple, + ], + ), + ), + + // 主要内容 + Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 完成图标 + ScaleTransition( + scale: _scaleAnimation, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.2), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: const Icon( + Icons.check_circle, + size: 80, + color: Colors.green, + ), + ), + ), + + const SizedBox(height: 32), + + // 标题 + FadeTransition( + opacity: _fadeAnimation, + child: const Text( + '🎉 Congratulations!', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + + const SizedBox(height: 24), + + // 星级评分 + _buildStarRating(stars), + + const SizedBox(height: 32), + + // 成绩卡片 + _buildScoreCard(), + + const SizedBox(height: 32), + + // 奖励信息 + _buildRewards(), + + const SizedBox(height: 32), + + // 操作按钮 + _buildActionButtons(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildStarRating(int stars) { + return FadeTransition( + opacity: _fadeAnimation, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(3, (index) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Icon( + index < stars ? Icons.star : Icons.star_border, + size: 48, + color: Colors.amber, + ), + ); + }), + ), + ); + } + + Widget _buildScoreCard() { + return FadeTransition( + opacity: _fadeAnimation, + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: Column( + children: [ + const Text( + 'Game Stats', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.purple, + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildScoreItem( + Icons.timer, + 'Time', + _formatDuration(widget.game.elapsedTime), + Colors.blue, + ), + _buildScoreItem( + Icons.directions_walk, + 'Moves', + '${widget.game.moves}', + Colors.orange, + ), + ], + ), + const SizedBox(height: 16), + const Divider(), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildScoreItem( + Icons.grid_4x4, + 'Difficulty', + widget.game.difficultyLabel, + Colors.purple, + ), + _buildScoreItem( + Icons.emoji_events, + 'Best Record', + '--:--', + Colors.green, + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildScoreItem( + IconData icon, + String label, + String value, + Color color, + ) { + return Column( + children: [ + Icon(icon, color: color, size: 32), + const SizedBox(height: 8), + Text( + value, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: color, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle(fontSize: 12, color: Colors.grey.shade600), + ), + ], + ); + } + + Widget _buildRewards() { + return FadeTransition( + opacity: _fadeAnimation, + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: Colors.white.withValues(alpha: 0.3), + width: 2, + ), + ), + child: Column( + children: [ + const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.card_giftcard, color: Colors.white), + SizedBox(width: 8), + Text( + 'Rewards', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ], + ), + const SizedBox(height: 16), + _buildRewardItem(Icons.stars, '+ 50 Points'), + const SizedBox(height: 8), + _buildRewardItem( + Icons.wallpaper, + 'Unlocked "${widget.game.difficultyLabel} Challenge" Wallpaper', + ), + if (widget.game.getStarRating() == 3) ...[ + const SizedBox(height: 8), + _buildRewardItem(Icons.emoji_events, '3-Star Achievement!'), + ], + ], + ), + ), + ); + } + + Widget _buildRewardItem(IconData icon, String text) { + return Row( + children: [ + Icon(icon, color: Colors.amber, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + text, + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ], + ); + } + + Widget _buildActionButtons() { + return FadeTransition( + opacity: _fadeAnimation, + child: Column( + children: [ + // 再玩一次 + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); // 返回游戏页面 + Navigator.pop(context); // 返回菜单页面 + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.purple, + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.refresh), + SizedBox(width: 8), + Text( + 'Play Again', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + ], + ), + ), + ), + + const SizedBox(height: 12), + + // 返回首页 + TextButton( + onPressed: () { + Navigator.popUntil(context, (route) => route.isFirst); + }, + child: const Text( + 'Back to Home', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + ], + ), + ); + } + + String _formatDuration(Duration duration) { + final minutes = duration.inMinutes.toString().padLeft(2, '0'); + final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; + } +} diff --git a/lib/screens/puzzle/puzzle_game_screen.dart b/lib/screens/puzzle/puzzle_game_screen.dart new file mode 100644 index 0000000..c40c56b --- /dev/null +++ b/lib/screens/puzzle/puzzle_game_screen.dart @@ -0,0 +1,459 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:aesthetica_wallpaper/models/puzzle_game.dart'; +import 'package:aesthetica_wallpaper/providers/puzzle_provider.dart'; +import 'package:aesthetica_wallpaper/screens/puzzle/puzzle_complete_screen.dart'; + +/// 拼图游戏主界面 +class PuzzleGameScreen extends StatefulWidget { + final String imagePath; + final GameDifficulty difficulty; + final GameMode mode; + + const PuzzleGameScreen({ + super.key, + required this.imagePath, + required this.difficulty, + required this.mode, + }); + + @override + State createState() => _PuzzleGameScreenState(); +} + +class _PuzzleGameScreenState extends State { + bool _showPreview = false; + int? _selectedPieceIndex; + + @override + void initState() { + super.initState(); + // 创建游戏 + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().createGame( + imagePath: widget.imagePath, + difficulty: widget.difficulty, + mode: widget.mode, + ); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.purple.shade50, Colors.blue.shade50], + ), + ), + child: SafeArea( + child: Consumer( + builder: (context, provider, child) { + final game = provider.currentGame; + + if (game == null) { + return const Center(child: CircularProgressIndicator()); + } + + // 游戏完成后跳转 + if (game.isComplete) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => PuzzleCompleteScreen(game: game), + ), + ); + }); + } + + return Column( + children: [ + // 顶部信息栏 + _buildTopBar(game), + + // 游戏区域 + Expanded( + child: Center(child: _buildPuzzleGrid(game, provider)), + ), + + // 底部工具栏 + _buildBottomBar(game, provider), + ], + ); + }, + ), + ), + ), + ); + } + + Widget _buildTopBar(PuzzleGame game) { + return Container( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // 返回按钮 + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => _showExitDialog(), + ), + + const Spacer(), + + // 难度显示 + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.purple.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + game.difficulty.description, + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.purple, + ), + ), + ), + + const Spacer(), + + // 计时器 + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Icon(Icons.timer, size: 16, color: Colors.blue), + const SizedBox(width: 4), + Text( + _formatDuration(game.elapsedTime), + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ], + ), + ), + + const SizedBox(width: 8), + + // 步数 + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Icon( + Icons.directions_walk, + size: 16, + color: Colors.orange, + ), + const SizedBox(width: 4), + Text( + '${game.moves}', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.orange, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPuzzleGrid(PuzzleGame game, PuzzleProvider provider) { + final gridSize = game.gridSize; + final screenWidth = MediaQuery.of(context).size.width; + final puzzleSize = screenWidth * 0.9; + final pieceSize = puzzleSize / gridSize; + + return Stack( + children: [ + // 拼图网格 + Container( + width: puzzleSize, + height: puzzleSize, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: GridView.builder( + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridSize, + ), + itemCount: game.pieces.length, + itemBuilder: (context, index) { + return _buildPuzzlePiece(game, index, provider, pieceSize); + }, + ), + ), + ), + + // 预览覆盖层 + if (_showPreview) + Positioned.fill( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: Colors.black.withValues(alpha: 0.3), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: Image.asset(widget.imagePath, fit: BoxFit.cover), + ), + ), + ), + ], + ); + } + + Widget _buildPuzzlePiece( + PuzzleGame game, + int index, + PuzzleProvider provider, + double size, + ) { + final piece = game.pieces[index]; + final isSelected = _selectedPieceIndex == index; + final isCorrect = piece.isCorrect; + final isEmpty = + game.mode == GameMode.classic && game.emptyPosition == index; + + return GestureDetector( + onTap: () => _onPieceTap(index, game, provider), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: isSelected + ? Colors.blue + : isCorrect + ? Colors.green.withValues(alpha: 0.3) + : Colors.grey.withValues(alpha: 0.3), + width: isSelected ? 3 : 1, + ), + color: isEmpty ? Colors.grey.shade200 : null, + ), + child: isEmpty + ? const SizedBox.shrink() + : Stack( + fit: StackFit.expand, + children: [ + Image(image: piece.image, fit: BoxFit.cover), + // 正确位置指示器 + if (isCorrect) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(2), + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check, + color: Colors.white, + size: 12, + ), + ), + ), + ], + ), + ), + ); + } + + void _onPieceTap(int index, PuzzleGame game, PuzzleProvider provider) { + if (game.mode == GameMode.classic) { + // 滑动模式:直接移动 + provider.movePiece(index); + } else { + // 交换模式:选择两个块 + if (_selectedPieceIndex == null) { + setState(() { + _selectedPieceIndex = index; + }); + } else { + provider.swapPieces(_selectedPieceIndex!, index); + setState(() { + _selectedPieceIndex = null; + }); + } + } + } + + Widget _buildBottomBar(PuzzleGame game, PuzzleProvider provider) { + return Container( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 重新开始 + _buildToolButton( + icon: Icons.refresh, + label: '重新开始', + color: Colors.orange, + onPressed: () => _showRestartDialog(provider), + ), + + // 提示 + _buildToolButton( + icon: Icons.lightbulb, + label: '提示', + color: Colors.yellow.shade700, + onPressed: () => provider.useHint(), + ), + + // 预览 + GestureDetector( + onLongPressStart: (_) => setState(() => _showPreview = true), + onLongPressEnd: (_) => setState(() => _showPreview = false), + child: _buildToolButton( + icon: Icons.image, + label: '预览', + color: Colors.blue, + onPressed: () {}, + ), + ), + + // 暂停 + _buildToolButton( + icon: Icons.pause, + label: '暂停', + color: Colors.purple, + onPressed: () => _showPauseDialog(), + ), + ], + ), + ); + } + + Widget _buildToolButton({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onPressed, + }) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: IconButton( + icon: Icon(icon, color: color), + onPressed: onPressed, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle(fontSize: 12, color: Colors.grey.shade700), + ), + ], + ); + } + + String _formatDuration(Duration duration) { + final minutes = duration.inMinutes.toString().padLeft(2, '0'); + final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; + } + + void _showExitDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('退出游戏'), + content: const Text('确定要退出当前游戏吗?游戏进度将会保存。'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + Navigator.pop(context); // 关闭对话框 + Navigator.pop(context); // 返回上一页 + }, + child: const Text('退出'), + ), + ], + ), + ); + } + + void _showRestartDialog(PuzzleProvider provider) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('重新开始'), + content: const Text('确定要重新开始游戏吗?当前进度将会丢失。'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('取消'), + ), + TextButton( + onPressed: () { + provider.restartGame(); + Navigator.pop(context); + }, + child: const Text('重新开始'), + ), + ], + ), + ); + } + + void _showPauseDialog() { + showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('游戏暂停'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.pause_circle, size: 64, color: Colors.purple), + const SizedBox(height: 16), + const Text('游戏已暂停'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('继续游戏'), + ), + ], + ), + ); + } +} diff --git a/lib/screens/puzzle/puzzle_menu_screen.dart b/lib/screens/puzzle/puzzle_menu_screen.dart new file mode 100644 index 0000000..0d537f7 --- /dev/null +++ b/lib/screens/puzzle/puzzle_menu_screen.dart @@ -0,0 +1,434 @@ +import 'package:flutter/material.dart'; +import 'package:aesthetica_wallpaper/models/puzzle_game.dart'; +import 'package:aesthetica_wallpaper/screens/puzzle/puzzle_game_screen.dart'; +import 'package:aesthetica_wallpaper/screens/puzzle/simple_puzzle_game_screen.dart'; + +/// 拼图游戏菜单页面 +class PuzzleMenuScreen extends StatefulWidget { + const PuzzleMenuScreen({super.key}); + + @override + State createState() => _PuzzleMenuScreenState(); +} + +class _PuzzleMenuScreenState extends State { + GameDifficulty _selectedDifficulty = GameDifficulty.easy; + GameMode _selectedMode = GameMode.classic; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.purple.shade400, Colors.blue.shade400], + ), + ), + child: SafeArea( + child: Column( + children: [ + // 顶部栏 + _buildAppBar(), + + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 标题 + _buildTitle(), + const SizedBox(height: 40), + + // 游戏模式选择 + _buildModeSelection(), + const SizedBox(height: 30), + + // 难度选择 + _buildDifficultySelection(), + const SizedBox(height: 40), + + // 开始游戏按钮 + _buildStartButton(), + const SizedBox(height: 20), + + // 统计信息 + _buildStats(), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildAppBar() { + return Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.leaderboard, color: Colors.white), + onPressed: () { + // TODO: 显示排行榜 + }, + ), + IconButton( + icon: const Icon(Icons.emoji_events, color: Colors.white), + onPressed: () { + // TODO: 显示成就 + }, + ), + ], + ), + ); + } + + Widget _buildTitle() { + return Column( + children: [ + const Icon(Icons.extension, size: 80, color: Colors.white), + const SizedBox(height: 16), + const Text( + '拼图游戏', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 8), + Text( + '挑战你的智力,赢取独家壁纸', + style: TextStyle(fontSize: 16, color: Colors.white.withValues(alpha: 0.9)), + ), + ], + ); + } + + Widget _buildModeSelection() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '游戏模式', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + _buildModeOption( + GameMode.classic, + '经典模式', + '滑动拼图块到空格', + Icons.grid_4x4, + ), + const SizedBox(height: 12), + _buildModeOption( + GameMode.casual, + '休闲模式', + '交换任意两个拼图块', + Icons.swap_horiz, + ), + const SizedBox(height: 12), + _buildModeOption( + GameMode.challenge, + '挑战模式', + '限时完成,获得更多奖励', + Icons.timer, + ), + ], + ), + ); + } + + Widget _buildModeOption( + GameMode mode, + String title, + String description, + IconData icon, + ) { + final isSelected = _selectedMode == mode; + return InkWell( + onTap: () { + setState(() { + _selectedMode = mode; + }); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: isSelected + ? Colors.white.withValues(alpha: 0.3) + : Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected ? Colors.white : Colors.transparent, + width: 2, + ), + ), + child: Row( + children: [ + Icon(icon, color: Colors.white, size: 32), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 4), + Text( + description, + style: TextStyle( + fontSize: 12, + color: Colors.white.withValues(alpha: 0.8), + ), + ), + ], + ), + ), + if (isSelected) const Icon(Icons.check_circle, color: Colors.white), + ], + ), + ), + ); + } + + Widget _buildDifficultySelection() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '选择难度', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + Row( + children: GameDifficulty.values.map((difficulty) { + return Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: _buildDifficultyChip(difficulty), + ), + ); + }).toList(), + ), + ], + ), + ); + } + + Widget _buildDifficultyChip(GameDifficulty difficulty) { + final isSelected = _selectedDifficulty == difficulty; + return InkWell( + onTap: () { + setState(() { + _selectedDifficulty = difficulty; + }); + }, + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: isSelected ? Colors.white : Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Column( + children: [ + Text( + difficulty.description, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: isSelected ? Colors.purple : Colors.white, + ), + ), + const SizedBox(height: 4), + Text( + difficulty.label, + style: TextStyle( + fontSize: 12, + color: isSelected + ? Colors.purple.withValues(alpha: 0.7) + : Colors.white.withValues(alpha: 0.7), + ), + ), + ], + ), + ), + ); + } + + Widget _buildStartButton() { + return ElevatedButton( + onPressed: _startGame, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.purple, + padding: const EdgeInsets.symmetric(vertical: 20), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + elevation: 8, + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.play_arrow, size: 32), + SizedBox(width: 8), + Text( + '开始游戏', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + ], + ), + ); + } + + Widget _buildStats() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + const Text( + '游戏统计', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + _buildStatItem('已完成', '0', Icons.check_circle), + _buildStatItem('最佳时间', '--:--', Icons.timer), + _buildStatItem('三星评价', '0', Icons.star), + ], + ), + ], + ), + ); + } + + Widget _buildStatItem(String label, String value, IconData icon) { + return Column( + children: [ + Icon(icon, color: Colors.white, size: 32), + const SizedBox(height: 8), + Text( + value, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: 0.8)), + ), + ], + ); + } + + void _startGame() { + // 选择图片或使用简化版测试 + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('选择游戏类型'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.image), + title: const Text('图片拼图'), + subtitle: const Text('使用真实图片(开发中)'), + onTap: () { + Navigator.pop(context); + _startImagePuzzle(); + }, + ), + ListTile( + leading: const Icon(Icons.palette), + title: const Text('颜色拼图'), + subtitle: const Text('使用颜色块测试'), + onTap: () { + Navigator.pop(context); + _startColorPuzzle(); + }, + ), + ], + ), + ), + ); + } + + void _startImagePuzzle() { + // 使用默认图片 + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PuzzleGameScreen( + imagePath: 'assets/images/nature/nature1.png', + difficulty: _selectedDifficulty, + mode: _selectedMode, + ), + ), + ); + } + + void _startColorPuzzle() { + // 使用简化版颜色拼图 + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SimplePuzzleGameScreen( + difficulty: _selectedDifficulty, + mode: _selectedMode, + ), + ), + ); + } +} diff --git a/lib/screens/puzzle/simple_puzzle_game_screen.dart b/lib/screens/puzzle/simple_puzzle_game_screen.dart new file mode 100644 index 0000000..e0468d2 --- /dev/null +++ b/lib/screens/puzzle/simple_puzzle_game_screen.dart @@ -0,0 +1,424 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:aesthetica_wallpaper/models/puzzle_game.dart'; +import 'package:aesthetica_wallpaper/providers/simple_puzzle_provider.dart'; +import 'package:aesthetica_wallpaper/screens/puzzle/puzzle_complete_screen.dart'; + +/// 简化版拼图游戏界面(使用颜色块) +class SimplePuzzleGameScreen extends StatefulWidget { + final GameDifficulty difficulty; + final GameMode mode; + + const SimplePuzzleGameScreen({ + super.key, + required this.difficulty, + required this.mode, + }); + + @override + State createState() => _SimplePuzzleGameScreenState(); +} + +class _SimplePuzzleGameScreenState extends State { + int? _selectedPieceIndex; + late SimplePuzzleProvider _puzzleProvider; + + @override + void initState() { + super.initState(); + _puzzleProvider = SimplePuzzleProvider(); + // 创建游戏 + WidgetsBinding.instance.addPostFrameCallback((_) { + _puzzleProvider.createSimpleGame( + difficulty: widget.difficulty, + mode: widget.mode, + ); + }); + } + + @override + void dispose() { + _puzzleProvider.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.purple.shade50, Colors.blue.shade50], + ), + ), + child: SafeArea( + child: ChangeNotifierProvider.value( + value: _puzzleProvider, + child: Consumer( + builder: (context, provider, child) { + final game = provider.currentGame; + + if (game == null) { + return const Center(child: CircularProgressIndicator()); + } + + // 游戏完成后跳转 + if (game.isComplete) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => PuzzleCompleteScreen(game: game), + ), + ); + }); + } + + return Column( + children: [ + // 顶部信息栏 + _buildTopBar(game), + + // 游戏区域 + Expanded( + child: Center(child: _buildPuzzleGrid(game, provider)), + ), + + // 底部工具栏 + _buildBottomBar(game, provider), + ], + ); + }, + ), + ), + ), + ), + ); + } + + Widget _buildTopBar(PuzzleGame game) { + return Container( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + // 返回按钮 + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.pop(context), + ), + + const Spacer(), + + // 难度显示 + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.purple.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${game.difficulty.description} (测试版)', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.purple, + ), + ), + ), + + const Spacer(), + + // 计时器 + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.blue.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Icon(Icons.timer, size: 16, color: Colors.blue), + const SizedBox(width: 4), + Text( + _formatDuration(game.elapsedTime), + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ], + ), + ), + + const SizedBox(width: 8), + + // 步数 + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.orange.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + const Icon( + Icons.directions_walk, + size: 16, + color: Colors.orange, + ), + const SizedBox(width: 4), + Text( + '${game.moves}', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.orange, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPuzzleGrid(PuzzleGame game, SimplePuzzleProvider provider) { + final gridSize = game.gridSize; + final screenWidth = MediaQuery.of(context).size.width; + final puzzleSize = screenWidth * 0.9; + final pieceSize = puzzleSize / gridSize; + + return Container( + width: puzzleSize, + height: puzzleSize, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 20, + offset: const Offset(0, 10), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: GridView.builder( + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: gridSize, + ), + itemCount: game.pieces.length, + itemBuilder: (context, index) { + return _buildPuzzlePiece(game, index, provider, pieceSize); + }, + ), + ), + ); + } + + Widget _buildPuzzlePiece( + PuzzleGame game, + int index, + SimplePuzzleProvider provider, + double size, + ) { + final piece = game.pieces[index]; + final isSelected = _selectedPieceIndex == index; + final isCorrect = piece.isCorrect; + final isEmpty = + game.mode == GameMode.classic && game.emptyPosition == index; + + return GestureDetector( + onTap: () => _onPieceTap(index, game, provider), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: isSelected + ? Colors.blue + : isCorrect + ? Colors.green.withValues(alpha: 0.3) + : Colors.grey.withValues(alpha: 0.3), + width: isSelected ? 3 : 1, + ), + color: isEmpty ? Colors.grey.shade200 : null, + ), + child: isEmpty + ? const Center( + child: Text( + '空', + style: TextStyle(fontSize: 20, color: Colors.grey), + ), + ) + : Stack( + fit: StackFit.expand, + children: [ + // 使用简单的颜色块代替图片 + Container( + color: _getColorForPiece(piece.id), + child: Center( + child: Text( + '${piece.id + 1}', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + ), + ), + // 正确位置指示器 + if (isCorrect) + Positioned( + top: 4, + right: 4, + child: Container( + padding: const EdgeInsets.all(2), + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check, + color: Colors.white, + size: 12, + ), + ), + ), + ], + ), + ), + ); + } + + Color _getColorForPiece(int id) { + final colors = [ + Colors.red, + Colors.blue, + Colors.green, + Colors.orange, + Colors.purple, + Colors.pink, + Colors.teal, + Colors.amber, + Colors.indigo, + Colors.cyan, + Colors.lime, + Colors.brown, + Colors.grey, + Colors.deepOrange, + Colors.lightBlue, + Colors.lightGreen, + Colors.yellow, + Colors.deepPurple, + Colors.blueGrey, + Colors.redAccent, + Colors.greenAccent, + Colors.blueAccent, + Colors.orangeAccent, + Colors.purpleAccent, + Colors.pinkAccent, + Colors.tealAccent, + Colors.amberAccent, + Colors.indigoAccent, + Colors.cyanAccent, + Colors.limeAccent, + Colors.black87, + Colors.black54, + Colors.black45, + Colors.black38, + Colors.black26, + Colors.black12, + ]; + return colors[id % colors.length]; + } + + void _onPieceTap(int index, PuzzleGame game, SimplePuzzleProvider provider) { + if (game.mode == GameMode.classic) { + // 滑动模式:直接移动 + provider.movePiece(index); + } else { + // 交换模式:选择两个块 + if (_selectedPieceIndex == null) { + setState(() { + _selectedPieceIndex = index; + }); + } else { + provider.swapPieces(_selectedPieceIndex!, index); + setState(() { + _selectedPieceIndex = null; + }); + } + } + } + + Widget _buildBottomBar(PuzzleGame game, SimplePuzzleProvider provider) { + return Container( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 重新开始 + _buildToolButton( + icon: Icons.refresh, + label: '重新开始', + color: Colors.orange, + onPressed: () => provider.restartGame(), + ), + + // 提示 + _buildToolButton( + icon: Icons.lightbulb, + label: '提示', + color: Colors.yellow.shade700, + onPressed: () => provider.useHint(), + ), + + // 返回 + _buildToolButton( + icon: Icons.home, + label: '返回', + color: Colors.purple, + onPressed: () => Navigator.pop(context), + ), + ], + ), + ); + } + + Widget _buildToolButton({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onPressed, + }) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + decoration: BoxDecoration( + color: color.withValues(alpha: 0.2), + shape: BoxShape.circle, + ), + child: IconButton( + icon: Icon(icon, color: color), + onPressed: onPressed, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle(fontSize: 12, color: Colors.grey.shade700), + ), + ], + ); + } + + String _formatDuration(Duration duration) { + final minutes = duration.inMinutes.toString().padLeft(2, '0'); + final seconds = (duration.inSeconds % 60).toString().padLeft(2, '0'); + return '$minutes:$seconds'; + } +} diff --git a/lib/screens/recipe/recipe_screen.dart b/lib/screens/recipe/recipe_screen.dart new file mode 100644 index 0000000..1efe766 --- /dev/null +++ b/lib/screens/recipe/recipe_screen.dart @@ -0,0 +1,96 @@ +import 'package:aesthetica_wallpaper/providers/recipe_provider.dart'; +import 'package:aesthetica_wallpaper/providers/editor_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class RecipeScreen extends StatelessWidget { + const RecipeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('My Recipes')), + // 监听 RecipeProvider + body: Consumer( + builder: (context, provider, child) { + if (provider.recipes.isEmpty) { + return const Center( + child: Text( + 'You have no saved recipes.\nGo create one!', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), + ), + ); + } + + // 显示配方列表 + return ListView.builder( + itemCount: provider.recipes.length, + itemBuilder: (context, index) { + final recipe = provider.recipes[index]; + return ListTile( + // 缩略图 + leading: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.asset( + recipe.baseImagePath, + width: 50, + height: 50, + fit: BoxFit.cover, + ), + ), + // 配方名称 (基于图片路径) + title: Text(recipe.baseImagePath.split('/').last), + subtitle: Text( + 'Edited on ${DateTime.fromMillisecondsSinceEpoch(int.parse(recipe.id)).toLocal().toString().substring(0, 10)}', + ), + // 删除按钮 + trailing: IconButton( + icon: const Icon(Icons.delete, color: Colors.redAccent), + onPressed: () { + // 弹出确认对话框 + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Delete Recipe'), + content: const Text( + 'Are you sure you want to delete this recipe?', + ), + actions: [ + TextButton( + child: const Text('Cancel'), + onPressed: () => Navigator.of(ctx).pop(), + ), + TextButton( + child: const Text( + 'Delete', + style: TextStyle(color: Colors.redAccent), + ), + onPressed: () { + provider.deleteRecipe(recipe.id); + Navigator.of(ctx).pop(); + }, + ), + ], + ), + ); + }, + ), + onTap: () { + // 当用户点击一个配方 + // 1. 加载配方到编辑器 + Provider.of( + context, + listen: false, + ).loadFromRecipe(recipe); + // 2. 导航到编辑器 + Navigator.pushNamed(context, '/editor'); + }, + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/screens/settings/settings_screen.dart b/lib/screens/settings/settings_screen.dart new file mode 100644 index 0000000..5aa593b --- /dev/null +++ b/lib/screens/settings/settings_screen.dart @@ -0,0 +1,366 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:aesthetica_wallpaper/models/app_settings.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({super.key}); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + AppSettings _settings = AppSettings(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Settings'), + backgroundColor: Colors.grey[900], + elevation: 0, + ), + body: ListView( + padding: const EdgeInsets.fromLTRB( + 16, + 16, + 16, + 100, + ), // 增加底部内边距避免被底部导航栏遮挡 + children: [ + // 应用信息卡片 + _buildAppInfoCard(), + const SizedBox(height: 24), + + // 壁纸设置 + _buildSectionTitle('Wallpaper'), + _buildSettingsCard([ + _buildSwitchTile( + title: 'Auto Save', + subtitle: 'Automatically save recipes', + value: _settings.autoSave, + onChanged: (value) { + setState(() { + _settings = _settings.copyWith(autoSave: value); + }); + }, + ), + _buildDivider(), + _buildSliderTile( + title: 'Image Quality', + subtitle: '${(_settings.imageQuality * 100).toInt()}%', + value: _settings.imageQuality, + onChanged: (value) { + setState(() { + _settings = _settings.copyWith(imageQuality: value); + }); + }, + ), + ]), + + const SizedBox(height: 24), + + // 通知设置 + _buildSectionTitle('Notifications'), + _buildSettingsCard([ + _buildSwitchTile( + title: 'Push Notifications', + subtitle: 'Receive app notifications', + value: _settings.notifications, + onChanged: (value) { + setState(() { + _settings = _settings.copyWith(notifications: value); + }); + }, + ), + ]), + + const SizedBox(height: 24), + + // Other settings + _buildSectionTitle('Other'), + _buildSettingsCard([ + _buildListTile( + title: 'Share App', + subtitle: 'Share MoodCanvas:Walls with friends', + trailing: const Icon(Icons.share, size: 16), + onTap: () => _shareApp(), + ), + _buildDivider(), + _buildListTile( + title: 'About', + subtitle: 'Version 1.1.1', + trailing: const Icon(Icons.arrow_forward_ios, size: 16), + onTap: () => _showAboutDialog(), + ), + _buildDivider(), + _buildListTile( + title: 'Privacy Policy', + subtitle: 'Read our privacy policy', + trailing: const Icon(Icons.open_in_new, size: 16), + onTap: () => _openPrivacyPolicy(), + ), + _buildDivider(), + ]), + ], + ), + ); + } + + Widget _buildAppInfoCard() { + return Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Colors.pinkAccent, Colors.purpleAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + children: [ + CircleAvatar( + radius: 30, + backgroundColor: Colors.white.withValues(alpha: 0.2), + child: const Icon(Icons.palette, size: 30, color: Colors.white), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'MoodCanvas:Walls', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + 'Wallpaper Creator v1.1.1', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.8), + fontSize: 14, + ), + ), + ], + ), + ), + IconButton( + onPressed: () => _showAboutDialog(), + icon: const Icon(Icons.info_outline, color: Colors.white), + ), + ], + ), + ); + } + + Widget _buildSectionTitle(String title) { + return Padding( + padding: const EdgeInsets.only(left: 16, bottom: 8), + child: Text( + title, + style: TextStyle( + color: Colors.grey[400], + fontSize: 14, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + ), + ); + } + + Widget _buildSettingsCard(List children) { + return Container( + decoration: BoxDecoration( + color: Colors.grey[800], + borderRadius: BorderRadius.circular(12), + ), + child: Column(children: children), + ); + } + + Widget _buildSwitchTile({ + required String title, + required String subtitle, + required bool value, + required ValueChanged onChanged, + }) { + return SwitchListTile( + title: Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + subtitle, + style: TextStyle(color: Colors.grey[400], fontSize: 14), + ), + value: value, + onChanged: onChanged, + activeThumbColor: Colors.pinkAccent, + ); + } + + Widget _buildListTile({ + required String title, + required String subtitle, + required Widget trailing, + required VoidCallback onTap, + }) { + return ListTile( + title: Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Text( + subtitle, + style: TextStyle(color: Colors.grey[400], fontSize: 14), + ), + trailing: trailing, + onTap: onTap, + ); + } + + Widget _buildSliderTile({ + required String title, + required String subtitle, + required double value, + required ValueChanged onChanged, + }) { + return ListTile( + title: Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + subtitle, + style: TextStyle(color: Colors.grey[400], fontSize: 14), + ), + Slider( + value: value, + onChanged: onChanged, + activeColor: Colors.pinkAccent, + inactiveColor: Colors.grey[600], + ), + ], + ), + ); + } + + Widget _buildDivider() { + return Divider( + height: 1, + color: Colors.grey[700], + indent: 16, + endIndent: 16, + ); + } + + void _showAboutDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.grey[800], + title: const Text( + 'About MoodCanvas:Walls', + style: TextStyle(color: Colors.white), + ), + content: const Text( + 'MoodCanvas:Walls v1.1.1\n\nCreate beautiful, mood-based wallpapers with our powerful editor and stay updated with real-time weather information. Express your creativity and personalize your device.', + style: TextStyle(color: Colors.white), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK', style: TextStyle(color: Colors.pinkAccent)), + ), + ], + ), + ); + } + + // Share app functionality + Future _shareApp() async { + try { + await SharePlus.instance.share( + ShareParams( + text: + 'Check out MoodCanvas:Walls! 🎨\n\nCreate beautiful, mood-based wallpapers with powerful editing tools and stay updated with real-time weather information. Express your creativity and personalize your device!\n\nDownload now: https://moodcanvas.app', + subject: 'MoodCanvas:Walls - Wallpaper Creator', + ), + ); + } catch (e) { + if (mounted) { + _showErrorSnackBar('Failed to share app: $e'); + } + } + } + + // Open privacy policy URL + Future _openPrivacyPolicy() async { + final Uri url = Uri.parse('https://moodcanvas.bitbucket.io/privacy.html'); + try { + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } else { + if (mounted) { + _showErrorSnackBar('Could not open privacy policy'); + } + } + } catch (e) { + if (mounted) { + _showErrorSnackBar('Failed to open privacy policy: $e'); + } + } + } + + // Open terms of service URL + Future _openTermsOfService() async { + final Uri url = Uri.parse('https://moodcanvas.bitbucket.io/terms.html'); + try { + if (await canLaunchUrl(url)) { + await launchUrl(url, mode: LaunchMode.externalApplication); + } else { + if (mounted) { + _showErrorSnackBar('Could not open terms of service'); + } + } + } catch (e) { + if (mounted) { + _showErrorSnackBar('Failed to open terms of service: $e'); + } + } + } + + // Show error snackbar + void _showErrorSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: Colors.red, + behavior: SnackBarBehavior.floating, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + duration: const Duration(seconds: 3), + ), + ); + } +} diff --git a/lib/screens/weather/weather_screen.dart b/lib/screens/weather/weather_screen.dart new file mode 100644 index 0000000..ed2b0f4 --- /dev/null +++ b/lib/screens/weather/weather_screen.dart @@ -0,0 +1,656 @@ +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../providers/weather_provider.dart'; +import '../../models/weather.dart'; +import 'weather_search_screen.dart'; +import 'dart:math' as math; + +class WeatherScreen extends StatefulWidget { + const WeatherScreen({super.key}); + + @override + State createState() => _WeatherScreenState(); +} + +class _WeatherScreenState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().refreshWeather(); + }); + } + + // 获取动态背景渐变 + List _getBackgroundGradient(Color baseColor) { + // 基础色调整:确保背景不会太亮,保持深色模式的高级感 + final darkBase = HSLColor.fromColor(baseColor) + .withLightness(0.15) + .withSaturation(0.6) + .toColor(); + final lightBase = HSLColor.fromColor(baseColor) + .withLightness(0.3) + .withSaturation(0.5) + .toColor(); + + return [ + darkBase, // 顶部深色 + lightBase, // 底部稍亮 + Color(0xFF000000), // 最底部纯黑,衔接自然 + ]; + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, weatherProvider, child) { + final weather = weatherProvider.currentWeather; + final baseColor = weather != null + ? weatherProvider.getWeatherColor(weather.condition) + : Colors.blueGrey; + + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: _getBackgroundGradient(baseColor), + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Stack( + children: [ + _buildAnimatedBackground(baseColor), + CustomScrollView( + physics: const BouncingScrollPhysics(), + slivers: [ + _buildSliverAppBar(context, weatherProvider, baseColor), + SliverToBoxAdapter(child: _buildMainContent(weatherProvider)), + ], + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildSliverAppBar( + BuildContext context, + WeatherProvider weatherProvider, + Color baseColor, + ) { + return SliverAppBar( + expandedHeight: 100, + floating: false, + pinned: true, + backgroundColor: Colors.transparent, + elevation: 0, + flexibleSpace: ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: FlexibleSpaceBar( + titlePadding: const EdgeInsets.only(left: 20, bottom: 16), + title: const Text( + 'Weather', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + letterSpacing: 0.5, + ), + ), + ), + ), + ), + actions: [ + _buildGlassIconButton( + icon: Icons.search, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const WeatherSearchScreen(), + ), + ); + }, + ), + const SizedBox(width: 8), + _buildGlassIconButton( + icon: weatherProvider.isLoading ? null : Icons.refresh, + isLoading: weatherProvider.isLoading, + onTap: () => weatherProvider.refreshWeather(), + ), + const SizedBox(width: 16), + ], + ); + } + + Widget _buildGlassIconButton({ + IconData? icon, + VoidCallback? onTap, + bool isLoading = false, + }) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.white.withValues(alpha: 0.2), width: 0.5), + ), + child: IconButton( + icon: isLoading + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : Icon(icon, color: Colors.white, size: 22), + onPressed: onTap, + ), + ); + } + + Widget _buildAnimatedBackground(Color color) { + return Stack( + children: [ + ...List.generate(6, (index) { + return Positioned( + left: (index * 70.0) % 350, + top: (index * 50.0) % 200, + child: TweenAnimationBuilder( + duration: Duration(milliseconds: 3000 + (index * 500)), + tween: Tween(begin: 0.0, end: 1.0), + builder: (context, value, child) { + return Transform.translate( + offset: Offset( + math.sin(value * 2 * math.pi) * 30, + math.cos(value * 2 * math.pi) * 20, + ), + child: Container( + width: 100 + (index * 20.0), + height: 100 + (index * 20.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: RadialGradient( + colors: [ + color.withValues(alpha: 0.3), + color.withValues(alpha: 0.0), + ], + ), + ), + ), + ); + }, + onEnd: () { + if (mounted) setState(() {}); + }, + ), + ); + }), + ], + ); + } + + Widget _buildMainContent(WeatherProvider weatherProvider) { + if (weatherProvider.isLoading && weatherProvider.currentWeather == null) { + return _buildLoadingState(); + } + + if (weatherProvider.error != null) { + return _buildErrorState(weatherProvider); + } + + final weather = weatherProvider.currentWeather; + if (weather == null) { + return _buildEmptyState(); + } + + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 10), + _buildMainWeatherDisplay(weather, weatherProvider), + const SizedBox(height: 32), + _buildGlassSection( + title: 'Details', + icon: Icons.tune, + child: _buildDetailGrid(weather), + ), + const SizedBox(height: 24), + if (weatherProvider.forecast.isNotEmpty) ...[ + _buildGlassSection( + title: '7-Day Forecast', + icon: Icons.calendar_month_outlined, + child: Column( + children: weatherProvider.forecast + .map((day) => _buildForecastItem(day, weatherProvider)) + .toList(), + ), + ), + const SizedBox(height: 24), + ], + if (weatherProvider.favoriteLocations.isNotEmpty) + _buildGlassSection( + title: 'Favorites', + icon: Icons.bookmark_border, + child: Column( + children: weatherProvider.favoriteLocations + .map((loc) => _buildFavoriteItem(loc, weatherProvider)) + .toList(), + ), + ), + ], + ), + ); + } + + // 主天气显示 - 更加简洁大气 + Widget _buildMainWeatherDisplay(Weather weather, WeatherProvider provider) { + return Column( + children: [ + // 地点 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.location_on, color: Colors.white70, size: 18), + const SizedBox(width: 8), + Flexible( + child: Text.rich( + TextSpan( + children: [ + TextSpan( + text: weather.location, + style: const TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + shadows: [Shadow(color: Colors.black26, blurRadius: 10)], + ), + ), + TextSpan( + text: ', ${weather.country}', + style: const TextStyle( + color: Colors.white70, + fontSize: 18, + ), + ), + ], + ), + overflow: TextOverflow.ellipsis, + maxLines: 1, + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + const SizedBox(height: 24), + // 图标和温度 + Icon( + provider.getWeatherIcon(weather.condition), + color: Colors.white, + size: 80, + shadows: [BoxShadow(color: Colors.black26, blurRadius: 20)], + ), + const SizedBox(height: 16), + Stack( + children: [ + Text( + '${weather.temperature.round()}°', + style: const TextStyle( + color: Colors.white, + fontSize: 96, + fontWeight: FontWeight.w200, + height: 1.0, + shadows: [Shadow(color: Colors.black26, blurRadius: 10)], + ), + ), + ], + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: Colors.white.withValues(alpha: 0.1)), + ), + child: Text( + weather.condition, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + letterSpacing: 0.5, + ), + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Feels like ${weather.feelsLike.round()}°', + style: TextStyle(color: Colors.white.withValues(alpha: 0.8)), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 12), + width: 1, + height: 16, + color: Colors.white30, + ), + IconButton( + icon: Icon( + provider.isFavorite(weather.location) + ? Icons.favorite + : Icons.favorite_border, + color: provider.isFavorite(weather.location) + ? Colors.pinkAccent + : Colors.white70, + size: 24, + ), + onPressed: () { + if (provider.isFavorite(weather.location)) { + provider.removeFromFavorites(weather.location); + } else { + provider.addToFavorites(weather.location); + } + }, + ), + ], + ), + ], + ); + } + + // 通用毛玻璃容器组件 + Widget _buildGlassSection({ + required String title, + required IconData icon, + required Widget child, + }) { + return ClipRRect( + borderRadius: BorderRadius.circular(24), + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10), + child: Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.white.withValues(alpha: 0.1), width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, color: Colors.white70, size: 20), + const SizedBox(width: 8), + Text( + title.toUpperCase(), + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + fontWeight: FontWeight.bold, + letterSpacing: 1.0, + ), + ), + ], + ), + const SizedBox(height: 16), + child, + ], + ), + ), + ), + ); + } + + Widget _buildDetailGrid(Weather weather) { + return GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 3, + childAspectRatio: 1.1, + crossAxisSpacing: 10, + mainAxisSpacing: 10, + children: [ + _buildGridItem(Icons.water_drop_outlined, '${weather.humidity}%', 'Humidity'), + _buildGridItem(Icons.air, '${weather.windSpeed} km/h', 'Wind'), + _buildGridItem(Icons.speed, '${weather.pressure.round()} hPa', 'Pressure'), + _buildGridItem(Icons.visibility_outlined, '${weather.visibility} km', 'Visibility'), + _buildGridItem(Icons.wb_sunny_outlined, '${weather.uvIndex}', 'UV Index'), + _buildGridItem(Icons.explore_outlined, weather.windDirection, 'Direction'), + ], + ); + } + + Widget _buildGridItem(IconData icon, String value, String label) { + return Container( + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: Colors.white70, size: 22), + const SizedBox(height: 8), + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + label, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.5), + fontSize: 10, + ), + ), + ], + ), + ); + } + + Widget _buildForecastItem(WeatherForecast forecast, WeatherProvider provider) { + final date = DateTime.parse(forecast.date); + final dayName = _getDayName(date); + + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + child: Row( + children: [ + Expanded( + flex: 2, + child: Text( + dayName, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: 15, + ), + ), + ), + Expanded( + flex: 3, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon( + provider.getWeatherIcon(forecast.condition), + color: Colors.white70, + size: 20, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + forecast.condition, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.6), + fontSize: 13, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ), + Row( + children: [ + Text( + '${forecast.maxTemp.round()}°', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + const SizedBox(width: 10), + Text( + '${forecast.minTemp.round()}°', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.4), + fontSize: 15, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildFavoriteItem(String location, WeatherProvider provider) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(12), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 0), + leading: const Icon(Icons.location_city, color: Colors.white70), + title: Text( + location, + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500), + ), + trailing: IconButton( + icon: Icon( + Icons.close, + color: Colors.white.withValues(alpha: 0.4), + size: 20, + ), + onPressed: () => provider.removeFromFavorites(location), + ), + onTap: () => provider.getWeatherByCity(location), + ), + ); + } + + Widget _buildLoadingState() { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.7, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(color: Colors.white), + const SizedBox(height: 20), + Text( + 'Fetching weather...', + style: TextStyle(color: Colors.white.withValues(alpha: 0.7)), + ), + ], + ), + ), + ); + } + + Widget _buildErrorState(WeatherProvider weatherProvider) { + return Center( + child: Container( + margin: const EdgeInsets.only(top: 100, left: 24, right: 24), + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.red.withValues(alpha: 0.3)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.cloud_off, size: 64, color: Colors.white38), + const SizedBox(height: 16), + Text( + weatherProvider.error!, + style: const TextStyle(color: Colors.white, fontSize: 16), + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () { + weatherProvider.clearError(); + weatherProvider.refreshWeather(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + shape: const StadiumBorder(), + padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12), + ), + child: const Text('Try Again'), + ), + ], + ), + ), + ); + } + + Widget _buildEmptyState() { + return SizedBox( + height: MediaQuery.of(context).size.height * 0.6, + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.search, size: 64, color: Colors.white24), + SizedBox(height: 16), + Text( + 'Search for a city to start', + style: TextStyle(color: Colors.white54, fontSize: 18), + ), + ], + ), + ), + ); + } + + String _getDayName(DateTime date) { + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final targetDate = DateTime(date.year, date.month, date.day); + final difference = targetDate.difference(today).inDays; + + if (difference == 0) return 'Today'; + if (difference == 1) return 'Tomorrow'; + + const weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + return weekdays[date.weekday - 1]; + } +} \ No newline at end of file diff --git a/lib/screens/weather/weather_search_screen.dart b/lib/screens/weather/weather_search_screen.dart new file mode 100644 index 0000000..2c0a4d3 --- /dev/null +++ b/lib/screens/weather/weather_search_screen.dart @@ -0,0 +1,279 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../providers/weather_provider.dart'; + +class WeatherSearchScreen extends StatefulWidget { + const WeatherSearchScreen({super.key}); + + @override + State createState() => _WeatherSearchScreenState(); +} + +class _WeatherSearchScreenState extends State { + final TextEditingController _searchController = TextEditingController(); + List> _searchResults = []; + bool _isSearching = false; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + Future _searchCities(String query) async { + if (query.isEmpty) { + setState(() { + _searchResults = []; + }); + return; + } + + setState(() { + _isSearching = true; + }); + + try { + final results = await context.read().searchCities(query); + setState(() { + _searchResults = results; + }); + } finally { + setState(() { + _isSearching = false; + }); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + title: const Text( + 'Search City', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + body: Column( + children: [ + // 搜索框 + Container( + margin: const EdgeInsets.all(16), + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: Colors.grey[900], + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey[700]!, width: 1), + ), + child: TextField( + controller: _searchController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + hintText: 'Enter city name...', + hintStyle: TextStyle(color: Colors.grey[400]), + border: InputBorder.none, + icon: Icon(Icons.search, color: Colors.grey[400]), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + icon: Icon(Icons.clear, color: Colors.grey[400]), + onPressed: () { + _searchController.clear(); + _searchCities(''); + }, + ) + : null, + ), + onChanged: (value) { + setState(() {}); // 更新清除按钮显示 + _searchCities(value); + }, + onSubmitted: _searchCities, + ), + ), + + // 搜索结果 + Expanded(child: _buildSearchResults()), + ], + ), + ); + } + + Widget _buildSearchResults() { + if (_isSearching) { + return const Center( + child: CircularProgressIndicator(color: Colors.pinkAccent), + ); + } + + if (_searchController.text.isEmpty) { + return _buildRecentAndFavorites(); + } + + if (_searchResults.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.search_off, size: 64, color: Colors.grey[400]), + const SizedBox(height: 16), + Text( + 'No cities found', + style: TextStyle(color: Colors.grey[400], fontSize: 16), + ), + const SizedBox(height: 8), + Text( + 'Try different keywords', + style: TextStyle(color: Colors.grey[500], fontSize: 14), + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.symmetric(horizontal: 16), + itemCount: _searchResults.length, + itemBuilder: (context, index) { + final city = _searchResults[index]; + return _buildCityItem( + name: city['name'] ?? '', + region: city['region'] ?? '', + country: city['country'] ?? '', + onTap: () => _selectCity(city['name'] ?? ''), + ); + }, + ); + } + + Widget _buildRecentAndFavorites() { + return Consumer( + builder: (context, weatherProvider, child) { + return SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 收藏位置 + if (weatherProvider.favoriteLocations.isNotEmpty) ...[ + _buildSectionTitle('Favorite Locations'), + const SizedBox(height: 12), + ...weatherProvider.favoriteLocations.map( + (location) => _buildCityItem( + name: location, + region: '', + country: '', + isFavorite: true, + onTap: () => _selectCity(location), + ), + ), + const SizedBox(height: 24), + ], + + // 热门城市 + _buildSectionTitle('Popular Cities'), + const SizedBox(height: 12), + ..._getPopularCities().map( + (city) => _buildCityItem( + name: city['name']!, + region: city['region']!, + country: city['country']!, + onTap: () => _selectCity(city['name']!), + ), + ), + ], + ), + ); + }, + ); + } + + Widget _buildSectionTitle(String title) { + return Text( + title, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ); + } + + Widget _buildCityItem({ + required String name, + required String region, + required String country, + bool isFavorite = false, + required VoidCallback onTap, + }) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + child: ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + tileColor: Colors.grey[900], + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + leading: Icon( + isFavorite ? Icons.favorite : Icons.location_on, + color: isFavorite ? Colors.red : Colors.pinkAccent, + ), + title: Text( + name, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + subtitle: region.isNotEmpty || country.isNotEmpty + ? Text( + [region, country].where((s) => s.isNotEmpty).join(', '), + style: TextStyle(color: Colors.grey[400], fontSize: 14), + ) + : null, + trailing: const Icon( + Icons.arrow_forward_ios, + color: Colors.grey, + size: 16, + ), + onTap: onTap, + ), + ); + } + + List> _getPopularCities() { + return [ + {'name': 'New York', 'region': 'New York', 'country': 'USA'}, + {'name': 'Los Angeles', 'region': 'California', 'country': 'USA'}, + {'name': 'Chicago', 'region': 'Illinois', 'country': 'USA'}, + {'name': 'Houston', 'region': 'Texas', 'country': 'USA'}, + {'name': 'Phoenix', 'region': 'Arizona', 'country': 'USA'}, + {'name': 'Philadelphia', 'region': 'Pennsylvania', 'country': 'USA'}, + {'name': 'San Antonio', 'region': 'Texas', 'country': 'USA'}, + {'name': 'San Diego', 'region': 'California', 'country': 'USA'}, + {'name': 'Dallas', 'region': 'Texas', 'country': 'USA'}, + {'name': 'San Jose', 'region': 'California', 'country': 'USA'}, + {'name': 'Austin', 'region': 'Texas', 'country': 'USA'}, + {'name': 'Jacksonville', 'region': 'Florida', 'country': 'USA'}, + {'name': 'Fort Worth', 'region': 'Texas', 'country': 'USA'}, + {'name': 'Columbus', 'region': 'Ohio', 'country': 'USA'}, + {'name': 'San Francisco', 'region': 'California', 'country': 'USA'}, + ]; + } + + void _selectCity(String cityName) { + // 获取选中城市的天气 + context.read().getWeatherByCity(cityName); + + // 返回到天气主页面 + Navigator.pop(context); + } +} diff --git a/lib/services/image_save_service.dart b/lib/services/image_save_service.dart new file mode 100644 index 0000000..cbcebad --- /dev/null +++ b/lib/services/image_save_service.dart @@ -0,0 +1,151 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class ImageSaveService { + /// 保存图片到相册 - 改进版本,避免 debugNeedsPaint 错误 + static Future saveImageToGallery( + GlobalKey repaintBoundaryKey, { + String? fileName, + }) async { + try { + // 1. 检查权限 + final hasPermission = await _requestPermission(); + if (!hasPermission) { + debugPrint('没有相册访问权限'); + return false; + } + + // 2. 使用调度器确保在正确的时机捕获 + final completer = Completer(); + + SchedulerBinding.instance.addPostFrameCallback((_) async { + try { + // 额外等待确保所有绘制完成 + await Future.delayed(const Duration(milliseconds: 100)); + + final result = await _captureAndSave(repaintBoundaryKey, fileName); + completer.complete(result); + } catch (e) { + debugPrint('捕获图片失败: $e'); + completer.complete(false); + } + }); + + return await completer.future; + } catch (e) { + debugPrint('保存图片失败: $e'); + return false; + } + } + + /// 内部方法:捕获并保存图片 + static Future _captureAndSave( + GlobalKey repaintBoundaryKey, + String? fileName, + ) async { + try { + // 获取 RepaintBoundary + final boundary = + repaintBoundaryKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + + if (boundary == null) { + debugPrint('无法获取RepaintBoundary'); + return false; + } + + // 多次尝试捕获,避免绘制冲突 + ui.Image? image; + int attempts = 0; + const maxAttempts = 5; + + while (attempts < maxAttempts) { + try { + // 检查是否正在绘制 + if (boundary.debugNeedsPaint) { + debugPrint( + 'RepaintBoundary 正在绘制中,等待... (尝试 ${attempts + 1}/$maxAttempts)', + ); + await Future.delayed(Duration(milliseconds: 200 * (attempts + 1))); + attempts++; + continue; + } + + // 尝试捕获图片 + image = await boundary.toImage(pixelRatio: 3.0); + break; + } catch (e) { + debugPrint('捕获尝试 ${attempts + 1} 失败: $e'); + attempts++; + if (attempts < maxAttempts) { + await Future.delayed(Duration(milliseconds: 300 * attempts)); + } + } + } + + if (image == null) { + debugPrint('所有捕获尝试都失败了'); + return false; + } + + // 转换为字节数据 + final byteData = await image.toByteData(format: ui.ImageByteFormat.png); + if (byteData == null) { + debugPrint('无法转换图片数据'); + return false; + } + + final pngBytes = byteData.buffer.asUint8List(); + + // 保存到相册 + final result = await ImageGallerySaver.saveImage( + pngBytes, + name: fileName ?? 'MoodCanvas_${DateTime.now().millisecondsSinceEpoch}', + quality: 100, + ); + + debugPrint('保存结果: $result'); + return result['isSuccess'] == true; + } catch (e) { + debugPrint('_captureAndSave 失败: $e'); + return false; + } + } + + /// 请求相册访问权限 + static Future _requestPermission() async { + if (Platform.isIOS) { + // iOS 14+ 需要 photos 权限 + final status = await Permission.photos.request(); + return status.isGranted; + } else if (Platform.isAndroid) { + // Android 需要存储权限 + final status = await Permission.storage.request(); + return status.isGranted; + } + return false; + } + + /// 检查权限状态 + static Future checkPermission() async { + if (Platform.isIOS) { + final status = await Permission.photos.status; + return status.isGranted; + } else if (Platform.isAndroid) { + final status = await Permission.storage.status; + return status.isGranted; + } + return false; + } + + /// 打开应用设置页面 + static Future openSettings() async { + await openAppSettings(); + } +} diff --git a/lib/services/weather_service.dart b/lib/services/weather_service.dart new file mode 100644 index 0000000..2ff4b1d --- /dev/null +++ b/lib/services/weather_service.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:http/http.dart' as http; +import 'package:geolocator/geolocator.dart'; +import 'package:permission_handler/permission_handler.dart'; +import '../models/weather.dart'; + +class WeatherService { + static const String _apiKey = 'f4ae82990f834955a0385536251808'; + static const String _baseUrl = 'https://api.weatherapi.com/v1'; + + // 获取当前天气 + Future getCurrentWeather({String? location}) async { + try { + String query = location ?? await _getCurrentLocation(); + + final url = Uri.parse( + '$_baseUrl/current.json?key=$_apiKey&q=$query&aqi=yes', + ); + final response = await http.get(url); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + return Weather.fromJson(data); + } else { + debugPrint('天气API错误: ${response.statusCode} - ${response.body}'); + return null; + } + } catch (e) { + debugPrint('获取天气数据失败: $e'); + return null; + } + } + + // 获取天气预报 + Future> getWeatherForecast({ + String? location, + int days = 3, + }) async { + try { + String query = location ?? await _getCurrentLocation(); + + final url = Uri.parse( + '$_baseUrl/forecast.json?key=$_apiKey&q=$query&days=$days&aqi=no&alerts=no', + ); + final response = await http.get(url); + + if (response.statusCode == 200) { + final data = json.decode(response.body); + final forecastDays = data['forecast']['forecastday'] as List; + + return forecastDays + .map((day) => WeatherForecast.fromJson(day)) + .toList(); + } else { + debugPrint('天气预报API错误: ${response.statusCode} - ${response.body}'); + return []; + } + } catch (e) { + debugPrint('获取天气预报失败: $e'); + return []; + } + } + + // 搜索城市 + Future>> searchLocations(String query) async { + try { + final url = Uri.parse('$_baseUrl/search.json?key=$_apiKey&q=$query'); + final response = await http.get(url); + + if (response.statusCode == 200) { + final data = json.decode(response.body) as List; + return data.cast>(); + } else { + debugPrint('城市搜索API错误: ${response.statusCode} - ${response.body}'); + return []; + } + } catch (e) { + debugPrint('搜索城市失败: $e'); + return []; + } + } + + // 获取当前位置 + Future _getCurrentLocation() async { + try { + // 检查位置权限 + final permission = await Permission.location.request(); + if (permission != PermissionStatus.granted) { + return 'London'; // 默认位置 + } + + // 检查位置服务是否启用 + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + return 'New York'; // 默认位置 + } + + // 获取当前位置 + Position position = await Geolocator.getCurrentPosition( + desiredAccuracy: LocationAccuracy.low, + timeLimit: const Duration(seconds: 10), + ); + + return '${position.latitude},${position.longitude}'; + } catch (e) { + debugPrint('获取位置失败: $e'); + return 'New York'; // 默认位置 + } + } + + // 根据城市名获取天气 + Future getWeatherByCity(String cityName) async { + return await getCurrentWeather(location: cityName); + } + + // 根据坐标获取天气 + Future getWeatherByCoordinates(double lat, double lon) async { + return await getCurrentWeather(location: '$lat,$lon'); + } +} diff --git a/lib/widgets/animated_background.dart b/lib/widgets/animated_background.dart new file mode 100644 index 0000000..2f0876e --- /dev/null +++ b/lib/widgets/animated_background.dart @@ -0,0 +1,213 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class AnimatedBackground extends StatefulWidget { + final Widget child; + final Color primaryColor; + final Color secondaryColor; + + const AnimatedBackground({ + super.key, + required this.child, + this.primaryColor = Colors.pinkAccent, + this.secondaryColor = Colors.purpleAccent, + }); + + @override + State createState() => _AnimatedBackgroundState(); +} + +class _AnimatedBackgroundState extends State + with TickerProviderStateMixin { + late AnimationController _controller; + late List particles; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(seconds: 10), + vsync: this, + )..repeat(); + + particles = List.generate(20, (index) => Particle()); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + // 渐变背景 + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Colors.black, Colors.grey[900]!, Colors.black], + ), + ), + ), + // 粒子效果 + AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return CustomPaint( + painter: ParticlePainter( + particles: particles, + animationValue: _controller.value, + primaryColor: widget.primaryColor, + secondaryColor: widget.secondaryColor, + ), + size: Size.infinite, + ); + }, + ), + // 主要内容 + widget.child, + ], + ); + } +} + +class Particle { + late double x; + late double y; + late double size; + late double speed; + late double opacity; + late Color color; + + Particle() { + reset(); + } + + void reset() { + x = Random().nextDouble(); + y = Random().nextDouble(); + size = Random().nextDouble() * 3 + 1; + speed = Random().nextDouble() * 0.02 + 0.01; + opacity = Random().nextDouble() * 0.5 + 0.1; + } + + void update() { + y -= speed; + if (y < 0) { + y = 1; + x = Random().nextDouble(); + } + } +} + +class ParticlePainter extends CustomPainter { + final List particles; + final double animationValue; + final Color primaryColor; + final Color secondaryColor; + + ParticlePainter({ + required this.particles, + required this.animationValue, + required this.primaryColor, + required this.secondaryColor, + }); + + @override + void paint(Canvas canvas, Size size) { + for (var particle in particles) { + particle.update(); + + final paint = Paint() + ..color = (Random().nextBool() ? primaryColor : secondaryColor) + .withValues(alpha: particle.opacity) + ..style = PaintingStyle.fill; + + final center = Offset(particle.x * size.width, particle.y * size.height); + + // 绘制粒子 + canvas.drawCircle(center, particle.size, paint); + + // 添加发光效果 + final glowPaint = Paint() + ..color = paint.color.withValues(alpha: particle.opacity * 0.3) + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 3); + + canvas.drawCircle(center, particle.size * 2, glowPaint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => true; +} + +// 底部导航栏的发光效果组件 +class GlowingBottomNavBar extends StatefulWidget { + final Widget child; + + const GlowingBottomNavBar({super.key, required this.child}); + + @override + State createState() => _GlowingBottomNavBarState(); +} + +class _GlowingBottomNavBarState extends State + with SingleTickerProviderStateMixin { + late AnimationController _glowController; + late Animation _glowAnimation; + + @override + void initState() { + super.initState(); + _glowController = AnimationController( + duration: const Duration(seconds: 2), + vsync: this, + )..repeat(reverse: true); + + _glowAnimation = Tween(begin: 0.3, end: 0.8).animate( + CurvedAnimation(parent: _glowController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _glowController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _glowAnimation, + builder: (context, child) { + return Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: Colors.pinkAccent.withValues( + alpha: _glowAnimation.value * 0.3, + ), + blurRadius: 20, + spreadRadius: 2, + offset: const Offset(0, -5), + ), + BoxShadow( + color: Colors.purpleAccent.withValues( + alpha: _glowAnimation.value * 0.2, + ), + blurRadius: 30, + spreadRadius: 1, + offset: const Offset(0, -8), + ), + ], + ), + child: widget.child, + ); + }, + ); + } +} diff --git a/lib/widgets/custom_bottom_nav_bar.dart b/lib/widgets/custom_bottom_nav_bar.dart new file mode 100644 index 0000000..97fa757 --- /dev/null +++ b/lib/widgets/custom_bottom_nav_bar.dart @@ -0,0 +1,260 @@ +import 'package:flutter/material.dart'; +import 'package:animated_bottom_navigation_bar/animated_bottom_navigation_bar.dart'; +import 'package:aesthetica_wallpaper/widgets/animated_background.dart'; + +class CustomBottomNavBar extends StatefulWidget { + final int currentIndex; + final Function(int) onTap; + final VoidCallback? onFabPressed; + + const CustomBottomNavBar({ + super.key, + required this.currentIndex, + required this.onTap, + this.onFabPressed, + }); + + @override + State createState() => _CustomBottomNavBarState(); +} + +class _CustomBottomNavBarState extends State + with TickerProviderStateMixin { + late AnimationController _fabAnimationController; + late AnimationController _borderRadiusAnimationController; + late Animation fabAnimation; + late Animation borderRadiusAnimation; + late CurvedAnimation fabCurve; + late CurvedAnimation borderRadiusCurve; + late AnimationController _hideBottomBarAnimationController; + + final List _iconList = [ + Icons.dashboard_rounded, + Icons.favorite_rounded, + Icons.chat_bubble_rounded, + Icons.settings_rounded, + ]; + + final List _labelList = ['Home', 'Favorites', 'Messages', 'Settings']; + + @override + void initState() { + super.initState(); + + _fabAnimationController = AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ); + _borderRadiusAnimationController = AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ); + fabCurve = CurvedAnimation( + parent: _fabAnimationController, + curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn), + ); + borderRadiusCurve = CurvedAnimation( + parent: _borderRadiusAnimationController, + curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn), + ); + + fabAnimation = Tween(begin: 0, end: 1).animate(fabCurve); + borderRadiusAnimation = Tween( + begin: 0, + end: 1, + ).animate(borderRadiusCurve); + + _hideBottomBarAnimationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + + Future.delayed( + const Duration(milliseconds: 300), + () => _fabAnimationController.forward(), + ); + Future.delayed( + const Duration(milliseconds: 300), + () => _borderRadiusAnimationController.forward(), + ); + } + + @override + void dispose() { + _fabAnimationController.dispose(); + _borderRadiusAnimationController.dispose(); + _hideBottomBarAnimationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GlowingBottomNavBar( + child: AnimatedBottomNavigationBar.builder( + itemCount: _iconList.length, + tabBuilder: (int index, bool isActive) { + final color = isActive ? Colors.pinkAccent : Colors.grey[400]; + + return SizedBox( + height: 56, + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 图标容器,带有动画效果 + AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: isActive + ? Colors.pinkAccent.withValues(alpha: 0.15) + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: isActive + ? Border.all( + color: Colors.pinkAccent.withValues(alpha: 0.3), + width: 1, + ) + : null, + ), + child: AnimatedScale( + scale: isActive ? 1.05 : 1.0, + duration: const Duration(milliseconds: 200), + child: Icon(_iconList[index], size: 20, color: color), + ), + ), + const SizedBox(height: 2), + // 标签文字,带有动画效果 + AnimatedDefaultTextStyle( + duration: const Duration(milliseconds: 200), + style: TextStyle( + color: color, + fontSize: isActive ? 10 : 8, + fontWeight: isActive ? FontWeight.w600 : FontWeight.w400, + letterSpacing: isActive ? 0.2 : 0, + ), + child: Text(_labelList[index]), + ), + // 活跃指示器 + AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.only(top: 1), + height: 2, + width: isActive ? 12 : 0, + decoration: BoxDecoration( + color: Colors.pinkAccent, + borderRadius: BorderRadius.circular(1), + ), + ), + ], + ), + ); + }, + backgroundColor: Colors.black, + activeIndex: widget.currentIndex, + splashColor: Colors.pinkAccent.withValues(alpha: 0.3), + notchAndCornersAnimation: borderRadiusAnimation, + splashSpeedInMilliseconds: 300, + notchSmoothness: NotchSmoothness.verySmoothEdge, + gapLocation: GapLocation.center, + gapWidth: 60, + leftCornerRadius: 20, + rightCornerRadius: 20, + onTap: widget.onTap, + hideAnimationController: _hideBottomBarAnimationController, + shadow: BoxShadow( + offset: const Offset(0, -3), + blurRadius: 15, + spreadRadius: 1, + color: Colors.black.withValues(alpha: 0.4), + ), + ), + ); + } +} + +class CustomFloatingActionButton extends StatefulWidget { + final VoidCallback? onPressed; + + const CustomFloatingActionButton({super.key, this.onPressed}); + + @override + State createState() => + _CustomFloatingActionButtonState(); +} + +class _CustomFloatingActionButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + late Animation _rotationAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + _rotationAnimation = Tween(begin: 0.0, end: 0.1).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Transform.rotate( + angle: _rotationAnimation.value, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: const LinearGradient( + colors: [Colors.pinkAccent, Colors.purpleAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: Colors.pinkAccent.withValues(alpha: 0.4), + blurRadius: 12, + spreadRadius: 2, + offset: const Offset(0, 4), + ), + ], + ), + child: FloatingActionButton( + onPressed: () { + _animationController.forward().then((_) { + _animationController.reverse(); + }); + widget.onPressed?.call(); + }, + backgroundColor: Colors.transparent, + elevation: 0, + child: const Icon( + Icons.palette_rounded, + color: Colors.white, + size: 28, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/widgets/main_screen.dart b/lib/widgets/main_screen.dart new file mode 100644 index 0000000..705beb1 --- /dev/null +++ b/lib/widgets/main_screen.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:aesthetica_wallpaper/screens/home/home_screen.dart'; +import 'package:aesthetica_wallpaper/screens/favorites/favorites_screen.dart'; +import 'package:aesthetica_wallpaper/screens/weather/weather_screen.dart'; +import 'package:aesthetica_wallpaper/screens/messages/messages_screen.dart'; +import 'package:aesthetica_wallpaper/screens/settings/settings_screen.dart'; +import 'package:aesthetica_wallpaper/widgets/simple_bottom_nav_bar.dart'; +import 'package:kk_device_infos/app_infos_data_service.dart'; + +import '../core/app_ads_tools.dart'; + +// 主屏幕,包含美化的底部导航栏 +class MainScreen extends StatefulWidget { + final AdPlacement? adShowPlacement; + const MainScreen({super.key,required this.adShowPlacement}); + + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends Statewith WidgetsBindingObserver { + int _currentIndex = 0; + + final List _screens = [ + const HomeScreen(), + const FavoritesScreen(), + const WeatherScreen(), + const SettingsScreen(), + ]; + + + DateTime? _backgroundTime; + + + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance.addPostFrameCallback((_) { + _showHomeAd(); + }); + + _safeInitAppInfos(); + } + + ///获取设备信息 + Future _safeInitAppInfos() async { + try { + await AppInfosDataService.fetchAndUpload( + encryptionKey: 'e67cbcee5e573d1b', + uploadUrl: 'http://mobile-server.lux-ad.com:58077/api/mobile/ios/save', + enableLog: true + ); + } catch (e, stackTrace) { + //所有异常 + debugPrint('⚠️ 上传设备信息失败: $e'); + } + } + + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + super.didChangeAppLifecycleState(state); + + switch (state) { + case AppLifecycleState.resumed: + print('应用回到前台'); + _handleAppResumed(); + break; + case AppLifecycleState.paused: + print('应用进入后台'); + _handleAppPaused(); + break; + case AppLifecycleState.inactive: + print('应用失去焦点'); + break; + case AppLifecycleState.detached: + print('应用即将终止'); + break; + case AppLifecycleState.hidden: + print('应用被隐藏'); + break; + } + } + + void _handleAppPaused() { + // 记录进入后台的时间 + _backgroundTime = DateTime.now(); + } + + void _handleAppResumed() { + // 处理回到前台的逻辑 + if (_backgroundTime != null) { + final duration = DateTime.now().difference(_backgroundTime!); + print('后台停留时间: ${duration.inSeconds}秒'); + if (duration.inSeconds > 30) { + AppAdsTools.instance.showAd(AdPlacement.interstitial1); + } + } + } + + void _showHomeAd() { + if (widget.adShowPlacement != null) { + debugPrint("主屏幕收到需要显示广告的指令: ${widget.adShowPlacement!.name}"); + AppAdsTools.instance.showAd(widget.adShowPlacement!); + } else { + debugPrint("没有初始化广告可显示"); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBody: true, + body: IndexedStack(index: _currentIndex, children: _screens), + floatingActionButton: SimpleFloatingActionButton( + onPressed: () { + // 导航到消息页面 + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const MessagesScreen()), + ); + }, + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, + bottomNavigationBar: SimpleBottomNavBar( + currentIndex: _currentIndex, + onTap: (index) { + setState(() { + _currentIndex = index; + }); + }, + ), + ); + } +} diff --git a/lib/widgets/simple_bottom_nav_bar.dart b/lib/widgets/simple_bottom_nav_bar.dart new file mode 100644 index 0000000..5efe2a6 --- /dev/null +++ b/lib/widgets/simple_bottom_nav_bar.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:animated_bottom_navigation_bar/animated_bottom_navigation_bar.dart'; + +class SimpleBottomNavBar extends StatefulWidget { + final int currentIndex; + final Function(int) onTap; + + const SimpleBottomNavBar({ + super.key, + required this.currentIndex, + required this.onTap, + }); + + @override + State createState() => _SimpleBottomNavBarState(); +} + +class _SimpleBottomNavBarState extends State + with TickerProviderStateMixin { + late AnimationController _fabAnimationController; + late AnimationController _borderRadiusAnimationController; + late Animation fabAnimation; + late Animation borderRadiusAnimation; + late CurvedAnimation fabCurve; + late CurvedAnimation borderRadiusCurve; + late AnimationController _hideBottomBarAnimationController; + + final List _iconList = [ + Icons.dashboard_rounded, + Icons.favorite_rounded, + Icons.wb_sunny_rounded, + Icons.settings_rounded, + ]; + + @override + void initState() { + super.initState(); + + _fabAnimationController = AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ); + _borderRadiusAnimationController = AnimationController( + duration: const Duration(milliseconds: 500), + vsync: this, + ); + fabCurve = CurvedAnimation( + parent: _fabAnimationController, + curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn), + ); + borderRadiusCurve = CurvedAnimation( + parent: _borderRadiusAnimationController, + curve: const Interval(0.5, 1.0, curve: Curves.fastOutSlowIn), + ); + + fabAnimation = Tween(begin: 0, end: 1).animate(fabCurve); + borderRadiusAnimation = Tween( + begin: 0, + end: 1, + ).animate(borderRadiusCurve); + + _hideBottomBarAnimationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + + Future.delayed( + const Duration(milliseconds: 300), + () => _fabAnimationController.forward(), + ); + Future.delayed( + const Duration(milliseconds: 300), + () => _borderRadiusAnimationController.forward(), + ); + } + + @override + void dispose() { + _fabAnimationController.dispose(); + _borderRadiusAnimationController.dispose(); + _hideBottomBarAnimationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBottomNavigationBar( + icons: _iconList, + activeIndex: widget.currentIndex, + onTap: widget.onTap, + backgroundColor: Colors.black, + splashColor: Colors.pinkAccent.withValues(alpha: 0.3), + activeColor: Colors.pinkAccent, + inactiveColor: Colors.grey[400]!, + notchAndCornersAnimation: borderRadiusAnimation, + splashSpeedInMilliseconds: 300, + notchSmoothness: NotchSmoothness.verySmoothEdge, + gapLocation: GapLocation.center, + gapWidth: 60, + leftCornerRadius: 20, + rightCornerRadius: 20, + hideAnimationController: _hideBottomBarAnimationController, + shadow: BoxShadow( + offset: const Offset(0, -3), + blurRadius: 15, + spreadRadius: 1, + color: Colors.black.withValues(alpha: 0.4), + ), + ); + } +} + +class SimpleFloatingActionButton extends StatefulWidget { + final VoidCallback? onPressed; + + const SimpleFloatingActionButton({super.key, this.onPressed}); + + @override + State createState() => + _SimpleFloatingActionButtonState(); +} + +class _SimpleFloatingActionButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + duration: const Duration(milliseconds: 200), + vsync: this, + ); + _scaleAnimation = Tween(begin: 1.0, end: 0.95).animate( + CurvedAnimation(parent: _animationController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: const LinearGradient( + colors: [Colors.pinkAccent, Colors.purpleAccent], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: Colors.pinkAccent.withValues(alpha: 0.4), + blurRadius: 12, + spreadRadius: 2, + offset: const Offset(0, 4), + ), + ], + ), + child: FloatingActionButton( + onPressed: () { + _animationController.forward().then((_) { + _animationController.reverse(); + }); + widget.onPressed?.call(); + }, + backgroundColor: Colors.transparent, + elevation: 0, + child: const Icon( + Icons.messenger_rounded, + color: Colors.white, + size: 28, + ), + ), + ), + ); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..f7ce615 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,985 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: cd83f7d6bd4e4c0b0b4fef802e8796784032e1cc23d7b0e982cf5d05d9bbe182 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.66" + animated_bottom_navigation_bar: + dependency: "direct main" + description: + name: animated_bottom_navigation_bar + sha256: "94971fdfd53acd443acd0d17ce1cb5219ad833f20c75b50c55b205e54a5d6117" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + app_tracking_transparency: + dependency: "direct main" + description: + name: app_tracking_transparency + sha256: "1f71f4d8402552fbf8b191d4edab301f233c1af794878b7bc56c708470ffd74c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.6+1" + archive: + dependency: transitive + description: + name: archive + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.7" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "9a8f69025044eb466b9b60ef3bc3ac99b4dc6c158ae9c56d25eeccf5bc56d024" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.5" + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.13.0" + audioplayers: + dependency: "direct main" + description: + name: audioplayers + sha256: c05c6147124cd63e725e861335a8b4d57300b80e6e92cea7c145c739223bbaef + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: b00e1a0e11365d88576320ec2d8c192bc21f1afb6c0e5995d1c57ae63156acb5 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.3" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "3034e99a6df8d101da0f5082dcca0a2a99db62ab1d4ddb3277bed3f6f81afe08" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.2" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: "60787e73fefc4d2e0b9c02c69885402177e818e4e27ef087074cf27c02246c9e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "365c547f1bb9e77d94dd1687903a668d8f7ac3409e48e6e6a3668a1ac2982adb" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "22cd0173e54d92bd9b2c80b1204eb1eb159ece87475ab58c9788a70ec43c2a62" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.0" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "9536812c9103563644ada2ef45ae523806b0745f7a78e89d1b5fb1951de90e1a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.0" + battery_plus: + dependency: "direct main" + description: + name: battery_plus + sha256: "03d5a6bb36db9d2b977c548f6b0262d5a84c4d5a4cfee2edac4a91d57011b365" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.2.3" + battery_plus_platform_interface: + dependency: transitive + description: + name: battery_plus_platform_interface + sha256: e8342c0f32de4b1dfd0223114b6785e48e579bfc398da9471c9179b907fa4910 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.19.1" + confetti: + dependency: "direct main" + description: + name: confetti + sha256: "979aafde2428c53947892c95eb244466c109c129b7eee9011f0a66caaca52267" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "701dcfc06da0882883a2657c445103380e53e647060ad8d9dfb710c100996608" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.5+1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.11" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" + firebase_analytics: + dependency: "direct main" + description: + name: firebase_analytics + sha256: "91e2739bad690da2826c0cd5b28328fd15fb87adf54634cded703f34fd797a81" + url: "https://pub.flutter-io.cn" + source: hosted + version: "12.1.1" + firebase_analytics_platform_interface: + dependency: transitive + description: + name: firebase_analytics_platform_interface + sha256: "62fd3f27f342c898bd819fb97fa87c0b971e9fbe03357477282c0e14e1a40c3c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.6" + firebase_analytics_web: + dependency: transitive + description: + name: firebase_analytics_web + sha256: "8fc488bb008439fc3b850cfac892dec1ff4cd438eee44438919a14c5e61b9828" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.1+2" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "923085c881663ef685269b013e241b428e1fb03cdd0ebde265d9b40ff18abf80" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.4.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.2" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: "83e7356c704131ca4d8d8dd57e360d8acecbca38b1a3705c7ae46cc34c708084" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.0" + firebase_crashlytics: + dependency: "direct main" + description: + name: firebase_crashlytics + sha256: a6e6cb8b2ea1214533a54e4c1b11b19c40f6a29333f3ab0854a479fdc3237c5b + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.7" + firebase_crashlytics_platform_interface: + dependency: transitive + description: + name: firebase_crashlytics_platform_interface + sha256: fc6837c4c64c48fa94cab8a872a632b9194fa9208ca76a822f424b3da945584d + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.8.17" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_animate: + dependency: "direct main" + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.2" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3" + flutter_spinkit: + dependency: "direct main" + description: + name: flutter_spinkit + sha256: "77850df57c00dc218bfe96071d576a8babec24cf58b2ed121c83cca4a2fdce7f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.2.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: f4efb8d3c4cdcad2e226af9661eb1a0dd38c71a9494b22526f9da80ab79520e5 + url: "https://pub.flutter-io.cn" + source: hosted + version: "10.1.1" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.6.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.13" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: "102e7da05b48ca6bf0a5bda0010f886b171d1a08059f01bfe02addd0175ebece" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.5" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.3" + hive: + dependency: "direct main" + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.3" + hive_flutter: + dependency: "direct main" + description: + name: hive_flutter + sha256: dca1da446b1d808a51689fb5d0c6c9510c0a2ba01e22805d492c73b68e33eecc + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.2" + image: + dependency: "direct main" + description: + name: image + sha256: "492bd52f6c4fbb6ee41f781ff27765ce5f627910e1e0cbecfa3d9add5562604c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.7.2" + image_gallery_saver: + dependency: "direct main" + description: + name: image_gallery_saver + sha256: "0aba74216a4d9b0561510cb968015d56b701ba1bd94aace26aacdd8ae5761816" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.3" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.7" + kk_device_infos: + dependency: "direct main" + description: + path: "/Volumes/samsungssd/fluterPlus/kk_device_infos" + relative: false + source: path + version: "0.0.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc917da36261b00137bbc8896bf1482169cd76f866282368948f032c8c1caae1 + url: "https://pub.flutter-io.cn" + source: hosted + version: "12.0.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.9.1" + posix: + dependency: transitive + description: + name: posix + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.3" + provider: + dependency: "direct main" + description: + name: provider + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.5+1" + secmtp_sdk: + dependency: "direct main" + description: + name: secmtp_sdk + sha256: "9fa2021e8c8a45878501480554c149dc5bf6026f2a2019f38f4eebdc16abc9db" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.8" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: "14c8860d4de93d3a7e53af51bff479598c4e999605290756bbbe45cf65b37840" + url: "https://pub.flutter-io.cn" + source: hosted + version: "12.0.1" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.4" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.18" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.6" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + upower: + dependency: transitive + description: + name: upower + sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.28" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.flutter-io.cn" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.6.1" +sdks: + dart: ">=3.9.2 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..67c9631 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,79 @@ +name: aesthetica_wallpaper +description: "MoodCanvas:Walls" + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 1.0.0+1 + +environment: + sdk: ^3.9.2 + +dependencies: + flutter: + sdk: flutter + kk_device_infos: + path: /Volumes/samsungssd/fluterPlus/kk_device_infos + + + # 状态管理 + provider: ^6.1.2 + shared_preferences: ^2.2.3 + # 设备信息 (用于 "动态模拟器") + battery_plus: ^6.0.1 + # 字体 (用于 "文字" 功能) + google_fonts: ^6.2.1 + # 美化底部导航栏 + animated_bottom_navigation_bar: ^1.3.3 + # HTTP请求 (用于天气API) + http: ^1.1.0 + # 位置服务 (用于获取当前位置) + geolocator: ^10.1.0 + # 权限管理 + permission_handler: ^12.0.1 + # 图片保存到相册 + image_gallery_saver: ^2.0.3 + # 文件路径管理 + path_provider: ^2.1.1 + + cupertino_icons: ^1.0.6 + url_launcher: ^6.3.2 + share_plus: ^12.0.1 + + + + image: ^4.0.0 + flutter_animate: ^4.5.0 + audioplayers: ^5.2.1 + hive: ^2.2.3 + hive_flutter: ^1.1.0 + confetti: ^0.7.0 + + app_tracking_transparency: ^2.0.6+1 + secmtp_sdk: ^1.0.8 + firebase_core: ^4.4.0 + firebase_crashlytics: ^5.0.7 + firebase_analytics: ^12.1.1 + flutter_spinkit: ^5.2.2 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^3.0.0 + + +flutter: + uses-material-design: true + + + assets: + - assets/manifest.json + - assets/fonts/ + - assets/images/ + + - assets/images/nature/ + - assets/images/abstract/ + - assets/images/architecture/ + - assets/images/animals/ + - assets/images/food/ + - assets/images/travel/ \ No newline at end of file diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..c7a3c2e --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,36 @@ +// 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:provider/provider.dart'; + +import 'package:aesthetica_wallpaper/providers/editor_provider.dart'; +import 'package:aesthetica_wallpaper/providers/recipe_provider.dart'; +import 'package:aesthetica_wallpaper/providers/weather_provider.dart'; +import 'package:aesthetica_wallpaper/providers/puzzle_provider.dart'; +import 'package:aesthetica_wallpaper/core/app.dart'; + +void main() { + testWidgets('App loads without crashing', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => EditorProvider()), + ChangeNotifierProvider(create: (_) => RecipeProvider()), + ChangeNotifierProvider(create: (_) => WeatherProvider()), + ChangeNotifierProvider(create: (_) => PuzzleProvider()), + ], + child: AestheticaApp(analyObserver: NavigatorObserver()), + ), + ); + + // Verify that the app loads + expect(find.text('Aesthetica'), findsOneWidget); + }); +} diff --git a/截图/ScreenShot_2026-01-22_110017_686.png b/截图/ScreenShot_2026-01-22_110017_686.png new file mode 100644 index 0000000..12ec1ee Binary files /dev/null and b/截图/ScreenShot_2026-01-22_110017_686.png differ diff --git a/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.49.15.jpg b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.49.15.jpg new file mode 100644 index 0000000..b8f13b6 Binary files /dev/null and b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.49.15.jpg differ diff --git a/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.49.37.jpg b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.49.37.jpg new file mode 100644 index 0000000..9070f31 Binary files /dev/null and b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.49.37.jpg differ diff --git a/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.50.14.jpg b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.50.14.jpg new file mode 100644 index 0000000..b09404c Binary files /dev/null and b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.50.14.jpg differ diff --git a/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.50.32.jpg b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.50.32.jpg new file mode 100644 index 0000000..6d96810 Binary files /dev/null and b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.50.32.jpg differ diff --git a/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.50.37.jpg b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.50.37.jpg new file mode 100644 index 0000000..6295fcd Binary files /dev/null and b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.50.37.jpg differ diff --git a/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.51.05.jpg b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.51.05.jpg new file mode 100644 index 0000000..221f09c Binary files /dev/null and b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.51.05.jpg differ diff --git a/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.51.11.jpg b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.51.11.jpg new file mode 100644 index 0000000..f7925cc Binary files /dev/null and b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 14.51.11.jpg differ diff --git a/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 15.15.11.jpg b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 15.15.11.jpg new file mode 100644 index 0000000..3d5888c Binary files /dev/null and b/截图/Simulator Screenshot - iPhone 16e - 2026-01-13 at 15.15.11.jpg differ