commit 364fc837b36c5969b182108ea03a070d370a68e4 Author: jingyun <> Date: Thu Jun 18 15:02:28 2026 +0800 feat:参照研听的基础工程 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1b0a90b --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +# build_runner 生成物,构建前由 `make runner` 重新生成,不纳入版本库 +**/*.g.dart +.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..c3664dd --- /dev/null +++ b/.metadata @@ -0,0 +1,36 @@ +# 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: "9f455d2486bcb28cad87b062475f42edc959f636" + channel: "[user-branch]" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + - platform: android + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + - platform: ios + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + - platform: web + create_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + base_revision: 9f455d2486bcb28cad87b062475f42edc959f636 + + # 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/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ed81351 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,8 @@ +# AGENTS.md - 金值 Flutter 客户端 + +当前阶段:工程骨架可运行。 + +- 业务按金值 PRD 与交互原型重新实现。 +- 首期数据使用 mock repository,不接真实报价源。 +- 复用工程基础能力:Riverpod、go_router、Dio 空壳、StorageService、主题与通用组件。 +- 不迁移旧业务页面和旧接口模型。 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4a44c96 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,11 @@ +# 金值 Flutter 客户端 + +金值是贵金属资产管家。当前代码库处于第一阶段,只保留可运行工程骨架和 4 个空白 Tab。 + +开发原则: + +- 使用 Flutter 原生 Widget 实现 PRD 和 HTML 原型,不嵌 WebView。 +- 状态管理沿用 Riverpod。 +- 路由沿用 go_router。 +- 首期数据走本地 mock repository。 +- 不复用旧业务页面、旧业务接口模型和旧业务命名。 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4569704 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: runner run test lint apk ipa build-web + +WEB_API_URL ?= https://jinzhi.api.local +WEB_BASE_HREF ?= /flutter/ +GITVERSION := $(shell git describe --tags --always --dirty 2>/dev/null || git rev-parse --short HEAD) +COMMON_DART_DEFINES := --dart-define=GITVERSION=$(GITVERSION) + +runner: + dart run build_runner build --delete-conflicting-outputs + +run: + flutter run $(COMMON_DART_DEFINES) --dart-define=JINZHI_API_BASE_URL=http://localhost:8088 + +test: + flutter test + +lint: + flutter analyze + +apk: + flutter build apk $(COMMON_DART_DEFINES) --release --target-platform android-arm64 + +ipa: + flutter build ipa $(COMMON_DART_DEFINES) --release + +build-web: + API_BASE_URL=$(WEB_API_URL) BASE_HREF=$(WEB_BASE_HREF) bash scripts/build_web.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf7d94c --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# jinzhi_flutter_app + +金值 - 贵金属资产管家。 + +第一阶段目标:基于既有 Flutter 工程骨架完成可运行空壳,保留主题、路由、状态管理、网络、存储和通用组件能力,业务从金值 PRD 重新实现。 diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..e45d43c --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,11 @@ +include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - "**/*.g.dart" + - "lib/models/pb/**" + - "packages/**" + plugins: + - custom_lint +linter: + rules: + prefer_const_constructors: true diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..45e3900 --- /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..ce70da5 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,79 @@ +import java.util.Properties + +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") +} + +val androidSigningPropertiesFile = rootProject.file("key.properties") +val androidSigningProperties = Properties() +if (!androidSigningPropertiesFile.isFile) { + throw GradleException("Missing Android signing properties: $androidSigningPropertiesFile") +} +androidSigningPropertiesFile.inputStream().use { androidSigningProperties.load(it) } + +fun requireSigningProperty(name: String): String { + val value = androidSigningProperties.getProperty(name)?.trim() + if (value.isNullOrBlank()) { + throw GradleException("Missing Android signing property: $name") + } + return value +} + +android { + namespace = "com.jinzhi.app" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + signingConfigs { + create("release") { + keyAlias = requireSigningProperty("keyAlias") + keyPassword = requireSigningProperty("keyPassword") + storeFile = file(requireSigningProperty("storeFile")) + storePassword = requireSigningProperty("storePassword") + } + } + + 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.jinzhi.app" + // 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 + + val umengChannel = System.getenv("UMENG_CHANNEL") ?: "" + manifestPlaceholders.putAll( + mutableMapOf( + "UMENG_CHANNEL" to umengChannel, + ) + ) + } + + buildTypes { + release { + signingConfig = signingConfigs.getByName("release") + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/files/jinzhi.jks b/android/app/files/jinzhi.jks new file mode 100644 index 0000000..22af8aa Binary files /dev/null and b/android/app/files/jinzhi.jks differ diff --git a/android/app/files/yanting.jks b/android/app/files/yanting.jks new file mode 100644 index 0000000..37bf2ae Binary files /dev/null and b/android/app/files/yanting.jks differ diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..a60226e --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1 @@ +# Add project-specific release rules here. 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..c9894ec --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/ic_launcher-playstore.png b/android/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..d7bfa71 Binary files /dev/null and b/android/app/src/main/ic_launcher-playstore.png differ diff --git a/android/app/src/main/kotlin/com/jinzhi/app/MainActivity.kt b/android/app/src/main/kotlin/com/jinzhi/app/MainActivity.kt new file mode 100644 index 0000000..d7d0fe6 --- /dev/null +++ b/android/app/src/main/kotlin/com/jinzhi/app/MainActivity.kt @@ -0,0 +1,5 @@ +package com.jinzhi.app + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-hdpi/android12splash.png b/android/app/src/main/res/drawable-hdpi/android12splash.png new file mode 100644 index 0000000..5093f59 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 0000000..1534ef8 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png new file mode 100644 index 0000000..5093f59 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/android12splash.png b/android/app/src/main/res/drawable-mdpi/android12splash.png new file mode 100644 index 0000000..46e07f9 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_notification.png b/android/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 0000000..16618c3 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-mdpi/splash.png b/android/app/src/main/res/drawable-mdpi/splash.png new file mode 100644 index 0000000..46e07f9 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-hdpi/android12splash.png b/android/app/src/main/res/drawable-night-hdpi/android12splash.png new file mode 100644 index 0000000..5093f59 Binary files /dev/null and b/android/app/src/main/res/drawable-night-hdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-hdpi/splash.png b/android/app/src/main/res/drawable-night-hdpi/splash.png new file mode 100644 index 0000000..5093f59 Binary files /dev/null and b/android/app/src/main/res/drawable-night-hdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-mdpi/android12splash.png b/android/app/src/main/res/drawable-night-mdpi/android12splash.png new file mode 100644 index 0000000..46e07f9 Binary files /dev/null and b/android/app/src/main/res/drawable-night-mdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-mdpi/splash.png b/android/app/src/main/res/drawable-night-mdpi/splash.png new file mode 100644 index 0000000..46e07f9 Binary files /dev/null and b/android/app/src/main/res/drawable-night-mdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-v21/background.png b/android/app/src/main/res/drawable-night-v21/background.png new file mode 100644 index 0000000..8e1c00e Binary files /dev/null and b/android/app/src/main/res/drawable-night-v21/background.png differ diff --git a/android/app/src/main/res/drawable-night-v21/launch_background.xml b/android/app/src/main/res/drawable-night-v21/launch_background.xml new file mode 100644 index 0000000..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable-night-v21/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-night-xhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png new file mode 100644 index 0000000..456eaa1 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xhdpi/splash.png b/android/app/src/main/res/drawable-night-xhdpi/splash.png new file mode 100644 index 0000000..456eaa1 Binary files /dev/null and b/android/app/src/main/res/drawable-night-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png new file mode 100644 index 0000000..5b77eba Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxhdpi/splash.png b/android/app/src/main/res/drawable-night-xxhdpi/splash.png new file mode 100644 index 0000000..5b77eba Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png new file mode 100644 index 0000000..b65478b Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-night-xxxhdpi/splash.png b/android/app/src/main/res/drawable-night-xxxhdpi/splash.png new file mode 100644 index 0000000..b65478b Binary files /dev/null and b/android/app/src/main/res/drawable-night-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-night/background.png b/android/app/src/main/res/drawable-night/background.png new file mode 100644 index 0000000..8e1c00e Binary files /dev/null and b/android/app/src/main/res/drawable-night/background.png differ diff --git a/android/app/src/main/res/drawable-night/launch_background.xml b/android/app/src/main/res/drawable-night/launch_background.xml new file mode 100644 index 0000000..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable-night/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png new file mode 100644 index 0000000..8e21404 Binary files /dev/null and b/android/app/src/main/res/drawable-v21/background.png differ 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..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/drawable-xhdpi/android12splash.png b/android/app/src/main/res/drawable-xhdpi/android12splash.png new file mode 100644 index 0000000..456eaa1 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 0000000..c6a7145 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/splash.png b/android/app/src/main/res/drawable-xhdpi/splash.png new file mode 100644 index 0000000..456eaa1 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxhdpi/android12splash.png new file mode 100644 index 0000000..5b77eba Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 0000000..b5bee59 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/splash.png b/android/app/src/main/res/drawable-xxhdpi/splash.png new file mode 100644 index 0000000..5b77eba Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png new file mode 100644 index 0000000..b65478b Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/android12splash.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png new file mode 100644 index 0000000..993ab66 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png new file mode 100644 index 0000000..b65478b Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash.png differ diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png new file mode 100644 index 0000000..8e21404 Binary files /dev/null and b/android/app/src/main/res/drawable/background.png differ diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..3cc4948 --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..e7b046d --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..e7b046d --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + 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..a871ffa Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 0000000..2cfa113 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..e53b692 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..7f19578 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..2a95881 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.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..9792efa Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 0000000..ccf8049 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..1ac078a Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..5434a98 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..a13ce71 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.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..f3f2623 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 0000000..bca57d5 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..9b7fee2 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..d50fecf Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..6ea422e Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.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..0f140c6 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..dcd5da5 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..b66d2d9 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..effb57c Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..09d5242 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.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..4a80d8f Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 0000000..d8f3085 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..cf2d951 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png new file mode 100644 index 0000000..5e2e721 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..9222e27 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 0000000..f47fefc --- /dev/null +++ b/android/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,22 @@ + + + + + + + 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..3c4a1fe --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml new file mode 100644 index 0000000..78315d3 --- /dev/null +++ b/android/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..f8056e5 --- /dev/null +++ b/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #95E300 + 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..847e1be --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.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/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/key.properties b/android/key.properties new file mode 100644 index 0000000..fd3a3f1 --- /dev/null +++ b/android/key.properties @@ -0,0 +1,4 @@ +storePassword=jinzhi123 +keyPassword=jinzhi123 +keyAlias=jinzhi +storeFile=files/jinzhi.jks 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/env/.env b/assets/env/.env new file mode 100644 index 0000000..678724e --- /dev/null +++ b/assets/env/.env @@ -0,0 +1,6 @@ +# .env file +# UMENG keys for Muzhi environment (replace with actual keys later) +UMENG_ANDROID_APP_KEY=6a27b1f46f259537c7b616be +UMENG_IOS_APP_KEY=6a27b24e6f259537c7b617a8 +UMENG_ANDROID_PUSH_SECRET=xxx +# UMENG_CHANNEL is usually set via build flavors or defaults, but can be here if needed. \ No newline at end of file diff --git a/assets/icons_launcher/ic_background.png b/assets/icons_launcher/ic_background.png new file mode 100644 index 0000000..a1de39b Binary files /dev/null and b/assets/icons_launcher/ic_background.png differ diff --git a/assets/icons_launcher/ic_black_white.png b/assets/icons_launcher/ic_black_white.png new file mode 100644 index 0000000..fbbaff3 Binary files /dev/null and b/assets/icons_launcher/ic_black_white.png differ diff --git a/assets/icons_launcher/ic_foreground.png b/assets/icons_launcher/ic_foreground.png new file mode 100644 index 0000000..a075860 Binary files /dev/null and b/assets/icons_launcher/ic_foreground.png differ diff --git a/assets/icons_launcher/ic_logo_radius.png b/assets/icons_launcher/ic_logo_radius.png new file mode 100644 index 0000000..93870b7 Binary files /dev/null and b/assets/icons_launcher/ic_logo_radius.png differ diff --git a/assets/icons_launcher/ic_logo_rectangle_dark.png b/assets/icons_launcher/ic_logo_rectangle_dark.png new file mode 100644 index 0000000..6082c6f Binary files /dev/null and b/assets/icons_launcher/ic_logo_rectangle_dark.png differ diff --git a/assets/icons_launcher/ic_logo_rectangle_light.png b/assets/icons_launcher/ic_logo_rectangle_light.png new file mode 100644 index 0000000..1b000d6 Binary files /dev/null and b/assets/icons_launcher/ic_logo_rectangle_light.png differ diff --git a/assets/icons_launcher/ic_logo_rectangle_tinted.png b/assets/icons_launcher/ic_logo_rectangle_tinted.png new file mode 100644 index 0000000..25a6b45 Binary files /dev/null and b/assets/icons_launcher/ic_logo_rectangle_tinted.png differ diff --git a/assets/icons_launcher/ic_logo_round.png b/assets/icons_launcher/ic_logo_round.png new file mode 100644 index 0000000..45e5797 Binary files /dev/null and b/assets/icons_launcher/ic_logo_round.png differ diff --git a/assets/launch/launch-bg.mp4 b/assets/launch/launch-bg.mp4 new file mode 100644 index 0000000..6f3e29c Binary files /dev/null and b/assets/launch/launch-bg.mp4 differ diff --git a/assets/launch/launch-poster.jpg b/assets/launch/launch-poster.jpg new file mode 100644 index 0000000..c8ecc4c Binary files /dev/null and b/assets/launch/launch-poster.jpg differ diff --git a/assets/launch/mark-grid-lime.png b/assets/launch/mark-grid-lime.png new file mode 100644 index 0000000..9eb0918 Binary files /dev/null and b/assets/launch/mark-grid-lime.png differ diff --git a/assets/launch/mark-star-lime.png b/assets/launch/mark-star-lime.png new file mode 100644 index 0000000..70fba16 Binary files /dev/null and b/assets/launch/mark-star-lime.png differ diff --git a/assets/splash/splash.png b/assets/splash/splash.png new file mode 100644 index 0000000..ebc570c Binary files /dev/null and b/assets/splash/splash.png differ diff --git a/assets/splash/splash_dark.png b/assets/splash/splash_dark.png new file mode 100644 index 0000000..ebc570c Binary files /dev/null and b/assets/splash/splash_dark.png differ diff --git a/flutter_native_splash.yaml b/flutter_native_splash.yaml new file mode 100644 index 0000000..b421a70 --- /dev/null +++ b/flutter_native_splash.yaml @@ -0,0 +1,16 @@ +# dart run flutter_native_splash:create +# 颜色 = lime_theme 背景色;图 = 定稿 lime mark(浅深共用) +flutter_native_splash: + color: "#FFFFFF" + image: assets/splash/splash.png + color_dark: "#0F0F0F" + image_dark: assets/splash/splash_dark.png + android: true + ios: true + web: false + fullscreen: true + android_12: + icon_background_color: "#FFFFFF" + icon_background_color_dark: "#0F0F0F" + image: assets/splash/splash.png + image_dark: assets/splash/splash_dark.png diff --git a/icons_launcher.yaml b/icons_launcher.yaml new file mode 100644 index 0000000..abe28c4 --- /dev/null +++ b/icons_launcher.yaml @@ -0,0 +1,17 @@ +# dart run icons_launcher:create +# 第一阶段沿用模板图标资源占位,后续替换为金值正式图标。 +icons_launcher: + platforms: + android: + enable: true + image_path: "assets/icons_launcher/ic_logo_radius.png" + notification_image: "assets/icons_launcher/ic_foreground.png" + adaptive_background_image: "assets/icons_launcher/ic_background.png" + adaptive_foreground_image: "assets/icons_launcher/ic_foreground.png" + adaptive_round_image: "assets/icons_launcher/ic_logo_round.png" + adaptive_monochrome_image: "assets/icons_launcher/ic_black_white.png" + ios: + enable: true + image_path: "assets/icons_launcher/ic_logo_rectangle_light.png" + dark_path: "assets/icons_launcher/ic_logo_rectangle_dark.png" + tinted_path: "assets/icons_launcher/ic_logo_rectangle_tinted.png" 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..620e46e --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.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) + end +end diff --git a/ios/Podfile.lock b/ios/Podfile.lock new file mode 100644 index 0000000..e60e3ec --- /dev/null +++ b/ios/Podfile.lock @@ -0,0 +1,86 @@ +PODS: + - device_info_plus (0.0.1): + - Flutter + - Flutter (1.0.0) + - flutter_native_splash (2.4.3): + - Flutter + - flutter_paid (0.0.1): + - Flutter + - flutter_secure_storage_darwin (10.0.0): + - Flutter + - FlutterMacOS + - fluttertoast (0.0.2): + - Flutter + - package_info_plus (0.4.5): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - platform_metadata (0.0.1): + - Flutter + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_ios (0.0.1): + - Flutter + - video_player_avfoundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) + - Flutter (from `Flutter`) + - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) + - flutter_paid (from `.symlinks/plugins/flutter_paid/ios`) + - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - platform_metadata (from `.symlinks/plugins/platform_metadata/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) + - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) + +EXTERNAL SOURCES: + device_info_plus: + :path: ".symlinks/plugins/device_info_plus/ios" + Flutter: + :path: Flutter + flutter_native_splash: + :path: ".symlinks/plugins/flutter_native_splash/ios" + flutter_paid: + :path: ".symlinks/plugins/flutter_paid/ios" + flutter_secure_storage_darwin: + :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" + package_info_plus: + :path: ".symlinks/plugins/package_info_plus/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + platform_metadata: + :path: ".symlinks/plugins/platform_metadata/ios" + shared_preferences_foundation: + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + url_launcher_ios: + :path: ".symlinks/plugins/url_launcher_ios/ios" + video_player_avfoundation: + :path: ".symlinks/plugins/video_player_avfoundation/darwin" + +SPEC CHECKSUMS: + device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf + flutter_paid: 6055d15f439071fefb849e3cebc279915ed70db5 + flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 + platform_metadata: a4d1e9569219bb2dcbb54510554611df032ea023 + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + video_player_avfoundation: dd410b52df6d2466a42d28550e33e4146928280a + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..99b7492 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,743 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 0B9CC9A25C666A7F563BF428 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DED7782CEF273A95BF887953 /* Pods_RunnerTests.framework */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7F35478B9E4F74858F78DFCF /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C52A653951E0B42558DD134A /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 175F3E0C34D5EC8425FD7D8D /* 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; }; + 337040C53995A39C4FE52239 /* 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 = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 3BDBEE268A9AE01E4F41D2D9 /* 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 = ""; }; + 738C53128F0E1E92C051CFBA /* 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 = ""; }; + 8EB9610D6597FC7A482D1606 /* 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 = ""; }; + 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 = ""; }; + C52A653951E0B42558DD134A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DED7782CEF273A95BF887953 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F25346648819B28CECD62B80 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 855EADD30C26D2B7D0DEDE22 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0B9CC9A25C666A7F563BF428 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7F35478B9E4F74858F78DFCF /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 6F2D6E664FE70BE6967B2B7C /* Frameworks */ = { + isa = PBXGroup; + children = ( + C52A653951E0B42558DD134A /* Pods_Runner.framework */, + DED7782CEF273A95BF887953 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + 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 */, + BC3E02F563A8EBB9C52CB127 /* Pods */, + 6F2D6E664FE70BE6967B2B7C /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + BC3E02F563A8EBB9C52CB127 /* Pods */ = { + isa = PBXGroup; + children = ( + 337040C53995A39C4FE52239 /* Pods-Runner.debug.xcconfig */, + 3BDBEE268A9AE01E4F41D2D9 /* Pods-Runner.release.xcconfig */, + F25346648819B28CECD62B80 /* Pods-Runner.profile.xcconfig */, + 8EB9610D6597FC7A482D1606 /* Pods-RunnerTests.debug.xcconfig */, + 738C53128F0E1E92C051CFBA /* Pods-RunnerTests.release.xcconfig */, + 175F3E0C34D5EC8425FD7D8D /* 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 = ( + 74491DF2E9EC3BEB038FC2A3 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 855EADD30C26D2B7D0DEDE22 /* 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 = ( + 18DB9BA14C321BAE6259714B /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + 38380B0F0A71332BD02F1E6B /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 18DB9BA14C321BAE6259714B /* [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; + }; + 38380B0F0A71332BD02F1E6B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + inputPaths = ( + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + ); + 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"; + }; + 74491DF2E9EC3BEB038FC2A3 /* [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; + }; + 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"; + }; +/* 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 = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = F38D68G8T6; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.jinzhi.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8EB9610D6597FC7A482D1606 /* 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.jinzhi.app.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 = 738C53128F0E1E92C051CFBA /* 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.jinzhi.app.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 = 175F3E0C34D5EC8425FD7D8D /* 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.jinzhi.app.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 = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = F38D68G8T6; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.jinzhi.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = F38D68G8T6; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.jinzhi.app; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 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..4b1f270 --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,536 @@ +{ + "images": [ + { + "filename": "Icon-App-20x20@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "20x20", + "platform": "ios" + }, + { + "filename": "Icon-App-20x20@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "20x20", + "platform": "ios" + }, + { + "filename": "Icon-App-29x29@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "29x29", + "platform": "ios" + }, + { + "filename": "Icon-App-29x29@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "29x29", + "platform": "ios" + }, + { + "filename": "Icon-App-38x38@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "38x38", + "platform": "ios" + }, + { + "filename": "Icon-App-38x38@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "38x38", + "platform": "ios" + }, + { + "filename": "Icon-App-40x40@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "40x40", + "platform": "ios" + }, + { + "filename": "Icon-App-40x40@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "40x40", + "platform": "ios" + }, + { + "filename": "Icon-App-60x60@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "60x60", + "platform": "ios" + }, + { + "filename": "Icon-App-60x60@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "60x60", + "platform": "ios" + }, + { + "filename": "Icon-App-64x64@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "64x64", + "platform": "ios" + }, + { + "filename": "Icon-App-64x64@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "64x64", + "platform": "ios" + }, + { + "filename": "Icon-App-68x68@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "68x68", + "platform": "ios" + }, + { + "filename": "Icon-App-76x76@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "76x76", + "platform": "ios" + }, + { + "filename": "Icon-App-83.5x83.5@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "83.5x83.5", + "platform": "ios" + }, + { + "filename": "Icon-App-1024x1024@1x.png", + "idiom": "universal", + "scale": "1x", + "size": "1024x1024", + "platform": "ios" + }, + { + "filename": "Icon-App-Dark-20x20@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "20x20", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-20x20@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "20x20", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-29x29@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "29x29", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-29x29@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "29x29", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-38x38@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "38x38", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-38x38@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "38x38", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-40x40@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "40x40", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-40x40@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "40x40", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-60x60@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "60x60", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-60x60@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "60x60", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-64x64@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "64x64", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-64x64@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "64x64", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-68x68@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "68x68", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-76x76@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "76x76", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-83.5x83.5@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "83.5x83.5", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Dark-1024x1024@1x.png", + "idiom": "universal", + "scale": "1x", + "size": "1024x1024", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ] + }, + { + "filename": "Icon-App-Tinted-20x20@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "20x20", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-20x20@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "20x20", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-29x29@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "29x29", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-29x29@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "29x29", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-38x38@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "38x38", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-38x38@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "38x38", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-40x40@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "40x40", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-40x40@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "40x40", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-60x60@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "60x60", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-60x60@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "60x60", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-64x64@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "64x64", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-64x64@3x.png", + "idiom": "universal", + "scale": "3x", + "size": "64x64", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-68x68@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "68x68", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-76x76@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "76x76", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-83.5x83.5@2x.png", + "idiom": "universal", + "scale": "2x", + "size": "83.5x83.5", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + }, + { + "filename": "Icon-App-Tinted-1024x1024@1x.png", + "idiom": "universal", + "scale": "1x", + "size": "1024x1024", + "platform": "ios", + "appearances": [ + { + "appearance": "luminosity", + "value": "tinted" + } + ] + } + ], + "info": { + "author": "icons_launcher", + "version": 1 + } +} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..571048c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..a0c25b3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..c33b77c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..1886f9f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..84a18c9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..27f7043 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..e09ba49 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@2x.png new file mode 100644 index 0000000..92294f5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@3x.png new file mode 100644 index 0000000..21cf895 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..be32638 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..592e198 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..c0a1b56 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..c0a1b56 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..bce7ce8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@2x.png new file mode 100644 index 0000000..7336ed1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@3x.png new file mode 100644 index 0000000..56e37e8 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-68x68@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-68x68@2x.png new file mode 100644 index 0000000..863bb52 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-68x68@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..fa9e418 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..cbfa9d5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..1de0163 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-1024x1024@1x.png new file mode 100644 index 0000000..b5515a7 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-20x20@2x.png new file mode 100644 index 0000000..115b01c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-20x20@3x.png new file mode 100644 index 0000000..a4dca00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-29x29@2x.png new file mode 100644 index 0000000..f1277bd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-29x29@3x.png new file mode 100644 index 0000000..a9577a3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-38x38@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-38x38@2x.png new file mode 100644 index 0000000..1305d60 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-38x38@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-38x38@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-38x38@3x.png new file mode 100644 index 0000000..773dfb3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-38x38@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-40x40@2x.png new file mode 100644 index 0000000..f5cac04 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-40x40@3x.png new file mode 100644 index 0000000..d91b4bd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-60x60@2x.png new file mode 100644 index 0000000..d91b4bd Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-60x60@3x.png new file mode 100644 index 0000000..23e58b2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-64x64@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-64x64@2x.png new file mode 100644 index 0000000..9729981 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-64x64@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-64x64@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-64x64@3x.png new file mode 100644 index 0000000..c146868 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-64x64@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-68x68@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-68x68@2x.png new file mode 100644 index 0000000..221db58 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-68x68@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-76x76@2x.png new file mode 100644 index 0000000..8b7e3ec Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-83.5x83.5@2x.png new file mode 100644 index 0000000..a96f9a2 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Dark-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-1024x1024@1x.png new file mode 100644 index 0000000..8d16807 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-20x20@2x.png new file mode 100644 index 0000000..260ca6e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-20x20@3x.png new file mode 100644 index 0000000..628357e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-29x29@2x.png new file mode 100644 index 0000000..367932d Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-29x29@3x.png new file mode 100644 index 0000000..953f92a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-38x38@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-38x38@2x.png new file mode 100644 index 0000000..2ba5327 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-38x38@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-38x38@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-38x38@3x.png new file mode 100644 index 0000000..6af16e3 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-38x38@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-40x40@2x.png new file mode 100644 index 0000000..bb1e95e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-40x40@3x.png new file mode 100644 index 0000000..6fdea67 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-60x60@2x.png new file mode 100644 index 0000000..6fdea67 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-60x60@3x.png new file mode 100644 index 0000000..d9079c0 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-64x64@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-64x64@2x.png new file mode 100644 index 0000000..4ec32cb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-64x64@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-64x64@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-64x64@3x.png new file mode 100644 index 0000000..4895fd1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-64x64@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-68x68@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-68x68@2x.png new file mode 100644 index 0000000..84ddc4f Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-68x68@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-76x76@2x.png new file mode 100644 index 0000000..675526e Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-83.5x83.5@2x.png new file mode 100644 index 0000000..33e8708 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-Tinted-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json new file mode 100644 index 0000000..8bb185b --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "filename" : "background.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "darkbackground.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png new file mode 100644 index 0000000..8e21404 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.png new file mode 100644 index 0000000..8e1c00e Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/darkbackground.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..f3387d4 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,56 @@ +{ + "images" : [ + { + "filename" : "LaunchImage.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LaunchImageDark.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "LaunchImage@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LaunchImageDark@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "LaunchImage@3x.png", + "idiom" : "universal", + "scale" : "3x" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "LaunchImageDark@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} 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..46e07f9 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..456eaa1 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..5b77eba Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png new file mode 100644 index 0000000..46e07f9 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png new file mode 100644 index 0000000..456eaa1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png new file mode 100644 index 0000000..5b77eba Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImageDark@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..afbfd28 --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..00af064 --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,60 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + 金值 + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + 金值 + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsArbitraryLoadsInWebContent + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIStatusBarHidden + + UIViewControllerBasedStatusBarAppearance + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/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/app/app.dart b/lib/app/app.dart new file mode 100644 index 0000000..b0b3cdb --- /dev/null +++ b/lib/app/app.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../common/router/app_router.dart'; +import '../common/theme/jinzhi_theme.dart'; +import '../common/theme/spacing.dart'; +import '../common/theme/theme_controller.dart'; + +class JinzhiApp extends ConsumerWidget { + const JinzhiApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final router = ref.watch(routerProvider); + final mode = ref.watch(themeModeControllerProvider); + // DM Sans 作主字体(拉丁/数字),中文走系统级联(iOS 苹方 / Android 厂商 + // 中文字体)——主流做法,不显式点名、不内置字体。 + final dmSansFamily = GoogleFonts.dmSans().fontFamily; + final textTheme = ShadTextTheme(family: dmSansFamily); + return ShadApp.router( + title: '金值', + themeMode: mode, + theme: ShadThemeData( + brightness: Brightness.light, + colorScheme: jinzhiLightScheme, + textTheme: textTheme, + radius: BorderRadius.circular(Spacing.radiusBase), + ), + darkTheme: ShadThemeData( + brightness: Brightness.dark, + colorScheme: jinzhiDarkScheme, + textTheme: textTheme, + radius: BorderRadius.circular(Spacing.radiusBase), + ), + routerConfig: router, + ); + } +} diff --git a/lib/app/bootstrap.dart b/lib/app/bootstrap.dart new file mode 100644 index 0000000..cba7b64 --- /dev/null +++ b/lib/app/bootstrap.dart @@ -0,0 +1,27 @@ +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/widgets.dart'; + +import '../common/services/device_header_service.dart'; +import '../common/storage/storage_service.dart'; +import 'app.dart'; + +Future bootstrap() async { + WidgetsFlutterBinding.ensureInitialized(); + await StorageService.instance; + if (!kIsWeb) { + if (Platform.isAndroid) { + // Before consent this only initializes app version and channel. The + // service resumes device identity initialization after consent. + await DeviceHeaderService.to.ensureInitialized(); + } else { + await DeviceHeaderService.to.ensureInitialized(); + } + } + // if (!kIsWeb) { + // await ShorebirdService.instance.initCurrentPatch(); + // ShorebirdService.instance.checkForUpdateInBackground(); + // } + return const JinzhiApp(); +} diff --git a/lib/common/config/app_config.dart b/lib/common/config/app_config.dart new file mode 100644 index 0000000..118476e --- /dev/null +++ b/lib/common/config/app_config.dart @@ -0,0 +1,11 @@ +class AppConfig { + const AppConfig({required this.apiBaseUrl}); + final String apiBaseUrl; + factory AppConfig.fromEnvironment() { + const url = String.fromEnvironment( + 'JINZHI_API_BASE_URL', + defaultValue: 'https://jinzhi.api.local', + ); + return const AppConfig(apiBaseUrl: url); + } +} diff --git a/lib/common/config/app_h5_urls.dart b/lib/common/config/app_h5_urls.dart new file mode 100644 index 0000000..4dbb80c --- /dev/null +++ b/lib/common/config/app_h5_urls.dart @@ -0,0 +1,37 @@ +class AppH5Urls { + const AppH5Urls._(); + + static const baseUrl = String.fromEnvironment( + 'JINZHI_H5_BASE_URL', + defaultValue: 'https://jinzhi.h5.local', + ); + + static String get privacyUrl => '$baseUrl/beginner/privacyPolicy.html'; + static String get userProtocolUrl => '$baseUrl/beginner/user_agreement.html'; +} + +class AppH5UrlsHelper { + const AppH5UrlsHelper._(); + + static String buildUrl(String url, [Map? params]) { + if (params == null || params.isEmpty) return url; + final uri = Uri.parse(url); + final merged = { + ...uri.queryParameters, + ...params.map((key, value) => MapEntry(key, value.toString())), + }; + return uri.replace(queryParameters: merged).toString(); + } + + static String buildUrlWithNight(String url, bool isDark) { + if (!isDark) return url; + return buildUrl(url, {'isNight': '1'}); + } + + static String withCacheBuster(String url) { + final uri = Uri.parse(url); + final params = Map.from(uri.queryParameters); + params['_cb'] = DateTime.now().microsecondsSinceEpoch.toString(); + return uri.replace(queryParameters: params).toString(); + } +} diff --git a/lib/common/config/shorebird_service.dart b/lib/common/config/shorebird_service.dart new file mode 100644 index 0000000..06120f2 --- /dev/null +++ b/lib/common/config/shorebird_service.dart @@ -0,0 +1,45 @@ +import 'package:flutter/foundation.dart'; +import 'package:shorebird_code_push/shorebird_code_push.dart'; + +class ShorebirdService { + ShorebirdService._(); + + static final ShorebirdService instance = ShorebirdService._(); + final ShorebirdUpdater _updater = ShorebirdUpdater(); + Patch? currentPatch; + + bool get isAvailable => _updater.isAvailable; + + Future readCurrentPatch() => _updater.readCurrentPatch(); + + Future initCurrentPatch() async { + try { + currentPatch = await _updater.readCurrentPatch(); + } catch (error) { + debugPrint('Error reading current patch: $error'); + } + } + + Future checkForUpdate({ + UpdateTrack track = UpdateTrack.stable, + }) { + return _updater.checkForUpdate(track: track); + } + + Future downloadUpdate({UpdateTrack track = UpdateTrack.stable}) { + return _updater.update(track: track).then((_) async { + await initCurrentPatch(); + }); + } + + Future checkForUpdateInBackground({ + UpdateTrack track = UpdateTrack.stable, + }) async { + if (!isAvailable) return; + try { + await _updater.checkForUpdate(track: track); + } catch (error) { + debugPrint('Shorebird background check failed: $error'); + } + } +} diff --git a/lib/common/config/umeng_config.dart b/lib/common/config/umeng_config.dart new file mode 100644 index 0000000..35b1618 --- /dev/null +++ b/lib/common/config/umeng_config.dart @@ -0,0 +1,25 @@ +// lib/common/config/umeng_config.dart +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +class UmengConfig { + // Keys loaded from .env file + static String _androidAppKey = + '6a27b1f46f259537c7b616be'; // Default placeholder + static String _iosAppKey = '6a27b24e6f259537c7b617a8'; // Default placeholder + static String _androidPushSecret = 'xxx'; // Default placeholder + + static Future load() async { + await dotenv.load( + fileName: "assets/env/.env", + ); // Load from packaged asset path + + _androidAppKey = dotenv.env['UMENG_ANDROID_APP_KEY'] ?? _androidAppKey; + _iosAppKey = dotenv.env['UMENG_IOS_APP_KEY'] ?? _iosAppKey; + _androidPushSecret = + dotenv.env['UMENG_ANDROID_PUSH_SECRET'] ?? _androidPushSecret; + } + + static String get androidAppKey => _androidAppKey; + static String get iosAppKey => _iosAppKey; + static String get androidPushSecret => _androidPushSecret; +} diff --git a/lib/common/domain/metal_type.dart b/lib/common/domain/metal_type.dart new file mode 100644 index 0000000..1b76625 --- /dev/null +++ b/lib/common/domain/metal_type.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +enum MetalType { + gold('gold', '黄金', '金', Color(0xFFB5862A)), + platinum('platinum', '铂金', '铂', Color(0xFF7D8792)), + silver('silver', '白银', '银', Color(0xFF9DA3AA)); + + const MetalType(this.id, this.label, this.shortLabel, this.color); + + final String id; + final String label; + final String shortLabel; + final Color color; +} diff --git a/lib/common/domain/money.dart b/lib/common/domain/money.dart new file mode 100644 index 0000000..27e6768 --- /dev/null +++ b/lib/common/domain/money.dart @@ -0,0 +1,11 @@ +String formatCny(num value) { + final fixed = value.toStringAsFixed(2); + final parts = fixed.split('.'); + final whole = parts.first.replaceAllMapped( + RegExp(r'(\d)(?=(\d{3})+(?!\d))'), + (match) => '${match[1]},', + ); + return '¥$whole.${parts.last}'; +} + +String formatGram(num value) => '${value.toStringAsFixed(2)}g'; diff --git a/lib/common/network/api_error_message.dart b/lib/common/network/api_error_message.dart new file mode 100644 index 0000000..65d342d --- /dev/null +++ b/lib/common/network/api_error_message.dart @@ -0,0 +1,20 @@ +import 'package:dio/dio.dart'; + +String? kratosReason(Object e) { + if (e is! DioException) return null; + final body = e.response?.data; + if (body is Map && body['reason'] is String) return body['reason'] as String; + return null; +} + +String kratosDisplayMessage(Object e, {required String fallback}) { + if (kratosReason(e) == 'INTERNAL_ERROR') return fallback; + if (e is DioException) { + final body = e.response?.data; + if (body is Map && body['message'] is String) { + final m = body['message'] as String; + if (m.isNotEmpty) return m; + } + } + return fallback; +} diff --git a/lib/common/network/auth_interceptor.dart b/lib/common/network/auth_interceptor.dart new file mode 100644 index 0000000..d3793c4 --- /dev/null +++ b/lib/common/network/auth_interceptor.dart @@ -0,0 +1,28 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import '../services/device_header_service.dart'; + +const _kJwtKey = 'jwt_token'; +const _storage = FlutterSecureStorage(); + +Future saveJwt(String token) => + _storage.write(key: _kJwtKey, value: token); +Future clearJwt() => _storage.delete(key: _kJwtKey); +Future loadJwt() => _storage.read(key: _kJwtKey); + +class AuthInterceptor extends Interceptor { + @override + Future onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + await DeviceHeaderService.to.ensureInitialized(); + options.headers.addAll(DeviceHeaderService.to.commonHeaders()); + options.headers.addAll(DeviceHeaderService.to.platformIdentityHeaders()); + final jwt = await loadJwt(); + if (jwt != null && jwt.isNotEmpty) { + options.headers['Authorization'] = 'Bearer $jwt'; + } + handler.next(options); + } +} diff --git a/lib/common/network/dio_provider.dart b/lib/common/network/dio_provider.dart new file mode 100644 index 0000000..295b525 --- /dev/null +++ b/lib/common/network/dio_provider.dart @@ -0,0 +1,41 @@ +import 'package:dio/dio.dart'; +import 'package:fancy_dio_inspector/fancy_dio_inspector.dart'; +import 'package:flutter/foundation.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../config/app_config.dart'; +import 'auth_interceptor.dart'; +import 'kratos_error_interceptor.dart'; + +part 'dio_provider.g.dart'; + +const _protobufContentType = 'application/x-protobuf'; + +@Riverpod(keepAlive: true) +AppConfig appConfig(Ref ref) => AppConfig.fromEnvironment(); + +@Riverpod(keepAlive: true) +Dio dio(Ref ref) { + final config = ref.watch(appConfigProvider); + final d = Dio( + BaseOptions( + baseUrl: config.apiBaseUrl, + connectTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 30), + responseType: ResponseType.bytes, + contentType: _protobufContentType, + headers: const { + 'Accept': _protobufContentType, + 'Content-Type': _protobufContentType, + }, + ), + ); + d.interceptors.add(AuthInterceptor()); + d.interceptors.add(KratosErrorInterceptor()); + if (!kReleaseMode) { + d.interceptors.add(LogInterceptor(requestBody: false, responseBody: false)); + } + d.interceptors.add(FancyDioInterceptor()); + return d; +} diff --git a/lib/common/network/kratos_error_interceptor.dart b/lib/common/network/kratos_error_interceptor.dart new file mode 100644 index 0000000..6be0acd --- /dev/null +++ b/lib/common/network/kratos_error_interceptor.dart @@ -0,0 +1,8 @@ +import 'package:dio/dio.dart'; + +class KratosErrorInterceptor extends Interceptor { + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + handler.next(err); + } +} diff --git a/lib/common/router/app_router.dart b/lib/common/router/app_router.dart new file mode 100644 index 0000000..d9f927a --- /dev/null +++ b/lib/common/router/app_router.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../features/assets/presentation/assets_page.dart'; +import '../../features/auth/presentation/delete_account_page.dart'; +import '../../features/auth/presentation/login_page.dart'; +import '../../features/debug/presentation/debug_info_page.dart'; +import '../../features/debug/presentation/debug_page.dart'; +import '../../features/debug/presentation/debug_shorebird_page.dart'; +import '../../features/debug/presentation/fancy_dio_inspector_page.dart'; +import '../../features/debug/presentation/typography_test_page.dart'; +import '../../features/market/presentation/market_page.dart'; +import '../../features/news/presentation/news_page.dart'; +import '../../features/profile/presentation/profile_page.dart'; +import '../../features/settings/presentation/settings_page.dart'; +import '../../features/settings/presentation/privacy_consent_page.dart'; +import '../services/device_header_service.dart'; +import 'tab_shell.dart'; + +abstract final class AppRoutes { + static const assets = '/assets'; + static const market = '/market'; + static const news = '/news'; + static const profile = '/profile'; + static const login = '/login'; + static const settings = '/settings'; + static const deleteAccount = '/delete-account'; + static const deleteAccountCompleted = '/delete-account-completed'; + static const debug = '/debug'; + static const debugInfo = '/debug/info'; + static const debugShorebird = '/debug/shorebird'; + static const debugTypography = '/debug/typography'; + static const fancyDioInspector = '/debug/fancy_dio_inspector'; + static const privacyConsent = '/privacy-consent'; +} + +final routerProvider = Provider((ref) { + final startupListenable = ValueNotifier(0); + return GoRouter( + initialLocation: AppRoutes.assets, + refreshListenable: startupListenable, + redirect: (context, state) { + final loc = state.matchedLocation; + final onConsent = loc == AppRoutes.privacyConsent; + final showStartup = + DeviceHeaderService.to.privacyConsentRequired || + DeviceHeaderService.to.iosStartupRequired; + if (showStartup) { + if (!onConsent) return AppRoutes.privacyConsent; + } else if (onConsent) { + return AppRoutes.assets; + } + return null; + }, + routes: [ + ShellRoute( + builder: (context, state, child) => + TabShell(location: state.matchedLocation, child: child), + routes: [ + GoRoute( + path: AppRoutes.assets, + builder: (ctx, s) => const AssetsPage(), + ), + GoRoute( + path: AppRoutes.market, + builder: (ctx, s) => const MarketPage(), + ), + GoRoute(path: AppRoutes.news, builder: (ctx, s) => const NewsPage()), + GoRoute( + path: AppRoutes.profile, + builder: (ctx, s) => const ProfilePage(), + ), + ], + ), + GoRoute(path: AppRoutes.debug, builder: (_, s) => const DebugPage()), + GoRoute( + path: AppRoutes.login, + builder: (_, s) => LoginPage(next: s.uri.queryParameters['next']), + ), + GoRoute( + path: AppRoutes.settings, + builder: (_, s) => const SettingsPage(), + ), + GoRoute( + path: AppRoutes.deleteAccount, + builder: (_, s) => const DeleteAccountPage(), + ), + GoRoute( + path: AppRoutes.deleteAccountCompleted, + builder: (_, s) => const DeleteAccountSuccessPage(), + ), + GoRoute( + path: AppRoutes.debugInfo, + builder: (_, s) => const DebugInfoPage(), + ), + GoRoute( + path: AppRoutes.debugShorebird, + builder: (_, s) => const DebugShorebirdPage(), + ), + GoRoute( + path: AppRoutes.debugTypography, + builder: (_, s) => const TypographyTestPage(), + ), + GoRoute( + path: AppRoutes.fancyDioInspector, + builder: (_, s) => const FancyDioInspectorPage(), + ), + GoRoute( + path: AppRoutes.privacyConsent, + builder: (_, s) => const PrivacyConsentPage(), + ), + ], + ); +}); diff --git a/lib/common/router/tab_origin.dart b/lib/common/router/tab_origin.dart new file mode 100644 index 0000000..7086d7d --- /dev/null +++ b/lib/common/router/tab_origin.dart @@ -0,0 +1,7 @@ +String tabFallbackForPath(String path) { + if (path.startsWith('/assets')) return '/assets'; + if (path.startsWith('/market')) return '/market'; + if (path.startsWith('/news')) return '/news'; + if (path.startsWith('/profile')) return '/profile'; + return '/assets'; +} diff --git a/lib/common/router/tab_shell.dart b/lib/common/router/tab_shell.dart new file mode 100644 index 0000000..8eb1f5c --- /dev/null +++ b/lib/common/router/tab_shell.dart @@ -0,0 +1,255 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../theme/app_icons.dart'; +import '../widgets/adaptive.dart'; +import 'app_router.dart'; + +class TabShell extends StatefulWidget { + const TabShell({super.key, required this.child, required this.location}); + + final Widget child; + final String location; + + @override + State createState() => _TabShellState(); +} + +class _TabShellState extends State { + static const double _compactHeaderThreshold = 34; + + bool _showCompactHeader = false; + + static const _tabs = ['/assets', '/market', '/news', '/profile']; + + int get _index => + _tabs.indexWhere((t) => widget.location.startsWith(t)).clamp(0, 3); + + @override + void didUpdateWidget(covariant TabShell oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.location != widget.location && _showCompactHeader) { + _showCompactHeader = false; + } + } + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final header = _headerForPath(widget.location); + final canPop = GoRouter.of(context).canPop(); + + return Scaffold( + backgroundColor: cs.background, + resizeToAvoidBottomInset: false, + body: Stack( + children: [ + Scaffold( + backgroundColor: cs.background, + resizeToAvoidBottomInset: false, + appBar: AppBar( + backgroundColor: cs.background, + surfaceTintColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, + toolbarHeight: 32, + leading: canPop + ? IconButton( + onPressed: () => context.pop(), + icon: const Icon(Icons.chevron_left, size: 20), + ) + : null, + title: AnimatedOpacity( + opacity: _showCompactHeader ? 1 : 0, + duration: const Duration(milliseconds: 160), + curve: Curves.easeOut, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + header.title, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: cs.foreground, + height: 1.1, + letterSpacing: 0, + ), + ), + if (header.subtitle != null) ...[ + const SizedBox(height: 2), + Text( + header.subtitle!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w400, + color: cs.mutedForeground, + height: 1.1, + letterSpacing: 0, + ), + ), + ], + ], + ), + ), + ), + body: ColoredBox( + color: cs.background, + child: NotificationListener( + onNotification: _handleScrollNotification, + child: Stack(children: [Positioned.fill(child: widget.child)]), + ), + ), + bottomNavigationBar: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DecoratedBox( + decoration: BoxDecoration( + color: cs.background, + border: Border( + top: BorderSide(color: cs.border, width: 1), + ), + ), + child: SizedBox( + height: 62, + // 平板:tab 行限宽 600 居中(demo .nav .tabs) + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: Adaptive.tabsMaxWidth, + ), + child: Row( + children: [ + for (var i = 0; i < _tabs.length; i++) + Expanded( + child: _TabButton( + icon: i == _index + ? _filledIcons[i] + : _outlineIcons[i], + label: _labels[i], + selected: i == _index, + onTap: () => context.go(_tabs[i]), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + if (!kReleaseMode) + Positioned( + right: 16, + bottom: 88, + child: FilledButton( + onPressed: () => context.push(AppRoutes.fancyDioInspector), + style: FilledButton.styleFrom( + backgroundColor: cs.primary, + foregroundColor: cs.primaryForeground, + minimumSize: const Size(60, 60), + shape: const CircleBorder(), + padding: EdgeInsets.zero, + ), + child: const Text('请求'), + ), + ), + ], + ), + ); + } + + bool _handleScrollNotification(ScrollNotification notification) { + if (notification.metrics.axis != Axis.vertical) { + return false; + } + final next = notification.metrics.pixels > _compactHeaderThreshold; + if (next != _showCompactHeader && mounted) { + setState(() => _showCompactHeader = next); + } + return false; + } +} + +class _TabButton extends StatelessWidget { + const _TabButton({ + required this.icon, + required this.label, + required this.selected, + required this.onTap, + }); + + final IconData icon; + final String label; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final color = selected ? cs.foreground : cs.mutedForeground; + return InkWell( + onTap: onTap, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 22, color: color), + const SizedBox(height: 4), + Text( + label, + style: TextStyle( + fontSize: 11, + fontWeight: selected ? FontWeight.w600 : FontWeight.w400, + color: color, + letterSpacing: 0, + ), + ), + ], + ), + ); + } +} + +const _filledIcons = [ + AppIcons.assetsFill, + AppIcons.marketFill, + AppIcons.newsFill, + AppIcons.meFill, +]; + +const _outlineIcons = [ + AppIcons.assetsLine, + AppIcons.marketLine, + AppIcons.newsLine, + AppIcons.meLine, +]; + +const _labels = ['资产', '行情', '资讯', '我的']; + +_ShellHeader _headerForPath(String path) { + return switch (path) { + '/market' => const _ShellHeader('行情', ''), + '/news' => const _ShellHeader('资讯', ''), + '/profile' => const _ShellHeader('我的'), + _ => const _ShellHeader('金值', ''), + }; +} + +class _ShellHeader { + const _ShellHeader(this.title, [this.subtitle]); + + final String title; + final String? subtitle; +} diff --git a/lib/common/services/device_header_service.dart b/lib/common/services/device_header_service.dart new file mode 100644 index 0000000..65002e9 --- /dev/null +++ b/lib/common/services/device_header_service.dart @@ -0,0 +1,274 @@ +import 'dart:io' show Platform; + +import 'package:android_id/android_id.dart'; +import 'package:android_oaid/android_oaid.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter_paid/flutter_paid.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:platform_metadata/platform_metadata.dart'; +import 'package:uuid/uuid.dart'; + +import '../storage/storage_keys.dart'; +import '../storage/storage_service.dart'; + +/// 请求公共 + 平台身份 header 来源(完全对齐研值契约)。 +/// AuthInterceptor 每个请求注入 commonHeaders() + platformIdentityHeaders(), +/// token 另由拦截器注入。空值 header 一律过滤。 +/// +/// 注:`User-Agent` 这里不表示浏览器 UA,而是沿用后端请求头契约中的平台标识, +/// 值同:iOS/Android/Web/Other。 +/// +/// 广告/设备标识(OAID/IMEI/MAC/IDFA/Paid/DeviceToken)由各自 SDK/ATT 流程 +/// 写入 StorageService,这里读出注入;Android 标识仅在用户同意隐私后采集。 +class DeviceHeaderService { + DeviceHeaderService._(); + static final DeviceHeaderService to = DeviceHeaderService._(); + static const _storage = FlutterSecureStorage(); + static const _kDeviceIdKey = 'device_id'; + // static const _platformMetadataChannel = MethodChannel('platform_metadata'); + + // 渠道:优先编译期 --dart-define=UMENG_CHANNEL=xxx;Android 其次读 manifest, + static const _umengChannel = String.fromEnvironment('UMENG_CHANNEL'); + + String _deviceId = ''; + String _version = ''; + String _build = ''; + String _deviceModel = ''; + String _userChannel = ''; + String _iosIdfv = ''; + String _iosPaid = ''; + String _androidId = ''; + String _androidOaid = ''; + String _androidImei = ''; + String _androidMac = ''; + Future? _baseInitialization; + Future? _identityInitialization; + + Future ensureInitialized() async { + _baseInitialization ??= _initializeBase(); + await _baseInitialization; + + // Align with tradingagents-flutter-app: Android only initializes device + // information after the user has accepted the privacy policy. + if (!kIsWeb && + Platform.isAndroid && + !StorageService.to.getBool(storagePrivacyAgreed)) { + return; + } + + _identityInitialization ??= _initializeIdentity(); + await _identityInitialization; + } + + Future _initializeBase() async { + try { + final info = await PackageInfo.fromPlatform(); + _version = info.version; + _build = info.buildNumber; + } catch (_) {} + _userChannel = await _resolveUserChannel(); + } + + Future _initializeIdentity() async { + _deviceId = await _resolveDeviceId(); + _androidOaid = StorageService.to.getString(storageAndroidOaid) ?? ''; + _androidImei = StorageService.to.getString(storageAndroidImei) ?? ''; + _androidMac = StorageService.to.getString(storageAndroidMac) ?? ''; + _androidId = StorageService.to.getString(storageAndroidId) ?? ''; + _iosPaid = StorageService.to.getString(storageIosPaid) ?? ''; + await _resolvePlatformIdentity(); + } + + /// X-Device-Id:iOS 优先 IDFV(per-vendor 稳定),否则 UUIDv4; + /// 首次生成后写入 secure_storage,后续复用。 + Future _resolveDeviceId() async { + final stored = await _storage.read(key: _kDeviceIdKey); + if (stored != null && stored.isNotEmpty) return stored; + var id = ''; + if (!kIsWeb && Platform.isIOS) { + id = await _resolveIdfv(); + } + if (id.isEmpty) id = const Uuid().v4(); + await _storage.write(key: _kDeviceIdKey, value: id); + return id; + } + + Future _resolveIdfv() async { + try { + final idfv = (await DeviceInfoPlugin().iosInfo).identifierForVendor; + return idfv ?? ''; + } catch (_) { + return ''; + } + } + + /// 设备型号 + 平台身份标识。Android 标识需用户同意隐私后才采集。 + Future _resolvePlatformIdentity() async { + if (kIsWeb) return; + try { + final plugin = DeviceInfoPlugin(); + if (Platform.isAndroid) { + _deviceModel = (await plugin.androidInfo).model; + await _collectAndroidIdIfAgreed(); + await _collectAndroidOaidIfAgreed(); + await _collectAndroidImeiIfAgreed(); + await _collectAndroidMacIfAgreed(); + } else if (Platform.isIOS) { + _deviceModel = (await plugin.iosInfo).utsname.machine; + _iosIdfv = await _resolveIdfv(); + await _collectIosPaidIfMissing(); + } + } catch (_) {} + } + + Future _collectAndroidIdIfAgreed() async { + if (_androidId.isNotEmpty) return; + if (!StorageService.to.getBool(storagePrivacyAgreed)) return; // 隐私同意后才采集 + try { + _androidId = await const AndroidId().getId() ?? ''; + if (_androidId.isNotEmpty) { + await StorageService.to.setString(storageAndroidId, _androidId); + } + } catch (_) {} + } + + Future _collectAndroidOaidIfAgreed() async { + if (_androidOaid.isNotEmpty) return; + if (!StorageService.to.getBool(storagePrivacyAgreed)) return; + try { + _androidOaid = await const AndroidOaidId().getOaid() ?? ''; + if (_androidOaid.isNotEmpty) { + await StorageService.to.setString(storageAndroidOaid, _androidOaid); + } + } catch (_) {} + } + + Future _collectAndroidImeiIfAgreed() async { + if (_androidImei.isNotEmpty) return; + if (!StorageService.to.getBool(storagePrivacyAgreed)) return; + try { + _androidImei = await const AndroidId().getImei() ?? ''; + if (_androidImei.isNotEmpty) { + await StorageService.to.setString(storageAndroidImei, _androidImei); + } + } catch (_) {} + } + + Future _collectAndroidMacIfAgreed() async { + if (_androidMac.isNotEmpty) return; + if (!StorageService.to.getBool(storagePrivacyAgreed)) return; + try { + _androidMac = await const AndroidId().getMaAddress() ?? ''; + if (_androidMac.isNotEmpty) { + await StorageService.to.setString(storageAndroidMac, _androidMac); + } + } catch (_) {} + } + + Future _collectIosPaidIfMissing() async { + if (_iosPaid.isNotEmpty) return; + try { + _iosPaid = await FlutterPaid().paid() ?? ''; + if (_iosPaid.isNotEmpty) { + await StorageService.to.setString(storageIosPaid, _iosPaid); + } + } catch (_) {} + } + + /// 用户同意隐私协议后调用。设备信息由随后调用的 ensureInitialized 补采。 + Future markPrivacyAgreed() async { + await StorageService.to.setBool(storagePrivacyAgreed, true); + } + + /// 是否需要首启隐私合规确认。**仅 Android**(合规要求:同意前不得采集设备 + /// 标识)。由 go_router redirect 调用,未同意则强制跳隐私页。 + bool get privacyConsentRequired => + !kIsWeb && + Platform.isAndroid && + !StorageService.to.getBool(storagePrivacyAgreed); + + /// iOS 首次启动页:仅在首次安装后展示一次。 + bool get iosStartupRequired => + !kIsWeb && + Platform.isIOS && + !StorageService.to.getBool(storageIosStartupSeen); + + Future markIosStartupSeen() async { + if (kIsWeb || !Platform.isIOS) return; + await StorageService.to.setBool(storageIosStartupSeen, true); + } + + String get deviceId => _deviceId; + + // User-Platform:平台标识(替代研值的 User-Agent)。 + String _platform() { + if (kIsWeb) return 'Web'; + if (Platform.isIOS) return 'iOS'; + if (Platform.isAndroid) return 'Android'; + return 'Other'; + } + + Future _resolveUserChannel() async { + if (_umengChannel.isNotEmpty) return _umengChannel; + + if (!kIsWeb && Platform.isAndroid) { + try { + final manifestChannel = await PlatformMetadata.getMetaDataValue( + 'UMENG_CHANNEL', + ); + if (manifestChannel != null && manifestChannel.isNotEmpty) { + return manifestChannel; + } + } catch (_) {} + } + if (kIsWeb) return 'web'; + if (Platform.isIOS) return 'ios'; + if (Platform.isAndroid) return 'android'; + return 'other'; + } + + String _s(String key) => StorageService.to.getString(key) ?? ''; + + /// 公共 header(不含 token)。11 + Map commonHeaders() => _compact({ + 'X-Device-Id': _deviceId, + 'User-Version': _version, + 'User-BuildID': _build, + 'User-Platform': _platform(), + 'User-Channel': _userChannel, + 'User-DeviceType': _deviceModel, + 'User-DeviceToken': privacyConsentRequired ? '' : _s(storageDeviceToken), + }); + + /// 平台身份 / 广告标识 header(按平台)。 + Map platformIdentityHeaders() { + if (kIsWeb) return const {}; + if (Platform.isAndroid) { + if (!StorageService.to.getBool(storagePrivacyAgreed)) return const {}; + return _compact({ + 'User-Android-ID': _androidId, + 'User-Android-OAID': _androidOaid, + 'User-Android-IMEI': _androidImei, + 'User-Android-Mac': _androidMac, + }); + } + if (Platform.isIOS) { + return _compact({ + 'User-iOS-IDFV': _iosIdfv, + 'User-iOS-IDFA': _s(storageIosIdfa), + 'User-iOS-Paid': _iosPaid, + }); + } + return const {}; + } +} + +Map _compact(Map headers) { + final out = {}; + headers.forEach((k, v) { + if (v.trim().isNotEmpty) out[k] = v; + }); + return out; +} diff --git a/lib/common/storage/storage_keys.dart b/lib/common/storage/storage_keys.dart new file mode 100644 index 0000000..4f48447 --- /dev/null +++ b/lib/common/storage/storage_keys.dart @@ -0,0 +1,19 @@ +const storageThemeMode = 'theme_mode'; +const storagePrivacyAgreed = 'privacy_agreed'; // Android 首启隐私同意(设备采集门控) +const storageIosStartupSeen = 'ios_startup_seen'; // iOS 首次启动页已展示 +const storageJinzhiHoldings = 'jinzhi_holdings'; +const storageJinzhiAmountHidden = 'jinzhi_amount_hidden'; +const storageJinzhiRecycleDiscounts = 'jinzhi_recycle_discounts'; +const storageJinzhiPurityPresets = 'jinzhi_purity_presets'; +const storageJinzhiPriceColorMode = 'jinzhi_price_color_mode'; +const storageJinzhiMockLoggedIn = 'jinzhi_mock_logged_in'; + +// 设备/广告标识。由各自的 SDK/ATT 流程写入,DeviceHeaderService 读出注入 +// header;未写入则为空、注入时被过滤。 +const storageDeviceToken = 'device_token'; // 推送 token +const storageAndroidId = 'android_id'; +const storageAndroidOaid = 'android_oaid'; +const storageAndroidImei = 'android_imei'; +const storageAndroidMac = 'android_mac'; +const storageIosIdfa = 'ios_idfa'; +const storageIosPaid = 'ios_paid'; diff --git a/lib/common/storage/storage_service.dart b/lib/common/storage/storage_service.dart new file mode 100644 index 0000000..c7390e5 --- /dev/null +++ b/lib/common/storage/storage_service.dart @@ -0,0 +1,20 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class StorageService { + StorageService._(this._prefs); + final SharedPreferences _prefs; + static StorageService? _instance; + static StorageService get to => _instance!; + + static Future get instance async { + if (_instance != null) return _instance!; + final prefs = await SharedPreferences.getInstance(); + _instance = StorageService._(prefs); + return _instance!; + } + + bool getBool(String key, {bool def = false}) => _prefs.getBool(key) ?? def; + Future setBool(String key, bool v) => _prefs.setBool(key, v); + String? getString(String key) => _prefs.getString(key); + Future setString(String key, String v) => _prefs.setString(key, v); +} diff --git a/lib/common/theme/app_icons.dart b/lib/common/theme/app_icons.dart new file mode 100644 index 0000000..8e604e1 --- /dev/null +++ b/lib/common/theme/app_icons.dart @@ -0,0 +1,15 @@ +import 'package:flutter_remix/flutter_remix.dart'; + +// flutter_remix 0.0.3: icon class is FlutterRemix +class AppIcons { + static const assetsFill = FlutterRemix.wallet_3_fill; + static const assetsLine = FlutterRemix.wallet_3_line; + static const marketFill = FlutterRemix.line_chart_fill; + static const marketLine = FlutterRemix.line_chart_line; + static const newsFill = FlutterRemix.article_fill; + static const newsLine = FlutterRemix.article_line; + static const meFill = FlutterRemix.user_3_fill; + static const meLine = FlutterRemix.user_3_line; + static const arrowRight = FlutterRemix.arrow_right_s_line; + static const arrowLeft = FlutterRemix.arrow_left_s_line; +} diff --git a/lib/common/theme/app_text.dart b/lib/common/theme/app_text.dart new file mode 100644 index 0000000..01bc406 --- /dev/null +++ b/lib/common/theme/app_text.dart @@ -0,0 +1,57 @@ +import 'package:flutter/widgets.dart'; + +/// 字号刻度常量。 +/// 颜色在用处通过 ShadTheme.of(context).colorScheme 取,此处只定义尺寸/粗细/行高。 +abstract class AppText { + /// App 大标题: 34px / w800 / lh 1.15 + static const TextStyle appTitle = TextStyle( + fontSize: 34, + fontWeight: FontWeight.w800, + height: 1.15, + ); + + /// 区块标题: 22px / w700 + static const TextStyle sectionTitle = TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + ); + + /// 卡片标题: 19px / w620 / lh 1.4 + static const TextStyle cardTitle = TextStyle( + fontSize: 19, + fontWeight: FontWeight.w600, // 最接近 w620 + height: 1.4, + ); + + /// 列表标题: 16.5px / w600 + static const TextStyle listTitle = TextStyle( + fontSize: 16.5, + fontWeight: FontWeight.w600, + ); + + /// 正文: 15px / w400 / lh 1.6 + static const TextStyle body = TextStyle( + fontSize: 15, + fontWeight: FontWeight.w400, + height: 1.6, + ); + + /// meta 信息: 13px / w400 + static const TextStyle meta = TextStyle( + fontSize: 13, + fontWeight: FontWeight.w400, + ); + + /// chip 筛选: 15px / w500 + static const TextStyle chip = TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + ); + + /// badge 标签: 12px / w500 + static const TextStyle badge = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + height: 1.5, + ); +} diff --git a/lib/common/theme/jinzhi_theme.dart b/lib/common/theme/jinzhi_theme.dart new file mode 100644 index 0000000..ef6f592 --- /dev/null +++ b/lib/common/theme/jinzhi_theme.dart @@ -0,0 +1,81 @@ +import 'package:flutter/widgets.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +const jinzhiLightScheme = ShadColorScheme( + background: Color(0xFFFBFAF7), + foreground: Color(0xFF1A1A17), + card: Color(0xFFFFFFFF), + cardForeground: Color(0xFF1A1A17), + popover: Color(0xFFFFFFFF), + popoverForeground: Color(0xFF1A1A17), + primary: Color(0xFFB5862A), + primaryForeground: Color(0xFFFFFFFF), + secondary: Color(0xFFF6ECD6), + secondaryForeground: Color(0xFF7A5B12), + muted: Color(0xFFF1EFE9), + mutedForeground: Color(0xFF8A8780), + accent: Color(0xFFF6ECD6), + accentForeground: Color(0xFF7A5B12), + destructive: Color(0xFFC0392B), + destructiveForeground: Color(0xFFFFFFFF), + border: Color(0xFFECEAE3), + input: Color(0xFFECEAE3), + ring: Color(0xFFB5862A), + selection: Color(0xFF1A1A17), +); + +/// Hero 卡描边 = demo `--brand-soft-border`(比 brand-soft 填充更深一档的绿)。 +/// ShadColorScheme 无对应 slot,单列于此(色值仍集中在 theme,widget 不写 hex); +/// widget 按 brightness 取值。light=oklch(0.9 0.07 128)、dark=oklch(0.42 0.07 131)。 +const jinzhiBrandSoftBorderLight = Color(0xFFE3D6B4); +const jinzhiBrandSoftBorderDark = Color(0xFF5E4A22); + +Color brandSoftBorder(Brightness b) => b == Brightness.dark + ? jinzhiBrandSoftBorderDark + : jinzhiBrandSoftBorderLight; + +/// Hero(brand-soft 绿底)上的副/弱文本色。 +/// 规范:次级文本是**中性 muted**,不挪用语义色(accentForeground 是链接/强调专用)。 +/// 浅绿底上灰 muted(#8E8E93) 对比不足,故用 foreground 降透明的**中性深灰** +/// (叠在绿底上自然带轻微绿调、和谐且达 AA);深色底仍用灰 muted(已够对比)。 +Color brandSoftMutedForeground(Brightness b, ShadColorScheme cs) => + b == Brightness.dark + ? cs.mutedForeground + : cs.foreground.withValues(alpha: 0.7); + +/// 危险警示卡 soft-tint(demo `.da-warn`,与 brand-soft 同语言的 destructive 变体)。 +const jinzhiDestructiveSoftLight = Color(0xFFFBEDEA); +const jinzhiDestructiveSoftBorderLight = Color(0xFFE8C7BF); +const jinzhiDestructiveSoftDark = Color(0x1AEF4444); +const jinzhiDestructiveSoftBorderDark = Color(0x52EF4444); + +Color destructiveSoft(Brightness b) => b == Brightness.dark + ? jinzhiDestructiveSoftDark + : jinzhiDestructiveSoftLight; + +Color destructiveSoftBorder(Brightness b) => b == Brightness.dark + ? jinzhiDestructiveSoftBorderDark + : jinzhiDestructiveSoftBorderLight; + +const jinzhiDarkScheme = ShadColorScheme( + background: Color(0xFF0F0F0F), + foreground: Color(0xFFF0F0F0), + card: Color(0xFF1A1A1A), + cardForeground: Color(0xFFF0F0F0), + popover: Color(0xFF1A1A1A), + popoverForeground: Color(0xFFF0F0F0), + primary: Color(0xFFD9B45D), + primaryForeground: Color(0xFF1A1A17), + secondary: Color(0xFF27272A), + secondaryForeground: Color(0xFFF0F0F0), + muted: Color(0xFF262626), + mutedForeground: Color(0xFFA1A1AA), + accent: Color(0xFF3A2A0D), + accentForeground: Color(0xFFD9B45D), + destructive: Color(0xFFEF4444), + destructiveForeground: Color(0xFFFFFFFF), + border: Color(0x1FFFFFFF), + input: Color(0x26FFFFFF), + ring: Color(0xFFD9B45D), + selection: Color(0xFFF0F0F0), +); diff --git a/lib/common/theme/spacing.dart b/lib/common/theme/spacing.dart new file mode 100644 index 0000000..e6dfa52 --- /dev/null +++ b/lib/common/theme/spacing.dart @@ -0,0 +1,10 @@ +class Spacing { + static const page = 20.0; + static const cardPadding = 18.0; + static const cardGap = 14.0; + static const sectionGap = 30.0; + static const radiusBase = 7.2; // 0.45rem --radius + static const radiusBadge = 3.2; // --radius-sm (badge / tag) + static const radiusBtn = 5.2; // --radius-md (button / input) + static const radiusCard = 11.2; // --radius-xl (card / hero 容器) +} diff --git a/lib/common/theme/theme_controller.dart b/lib/common/theme/theme_controller.dart new file mode 100644 index 0000000..86c39ea --- /dev/null +++ b/lib/common/theme/theme_controller.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import '../storage/storage_service.dart'; +import '../storage/storage_keys.dart'; + +part 'theme_controller.g.dart'; + +@riverpod +class ThemeModeController extends _$ThemeModeController { + @override + ThemeMode build() { + final v = StorageService.to.getString(storageThemeMode); + return switch (v) { + 'light' => ThemeMode.light, + 'dark' => ThemeMode.dark, + _ => ThemeMode.system, + }; + } + + Future set(ThemeMode mode) async { + state = mode; + await StorageService.to.setString(storageThemeMode, mode.name); + } +} diff --git a/lib/common/util/external_link.dart b/lib/common/util/external_link.dart new file mode 100644 index 0000000..0939999 --- /dev/null +++ b/lib/common/util/external_link.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../widgets/app_toast.dart'; +import '../widgets/outbound_service_sheet.dart'; + +Future openExternalUrl( + BuildContext context, + String url, { + String title = '即将打开外部服务', + String description = '你将离开金值,进入第三方服务页面。外部内容由对应服务提供。\n价格与资讯仅供参考,不构成投资建议。', + String fallbackToast = '链接即将上线', +}) async { + if (url.isEmpty) { + toastInfo(msg: fallbackToast); + return; + } + + final uri = Uri.tryParse(url); + if (uri == null || !uri.hasScheme) { + toastInfo(msg: fallbackToast); + return; + } + + final ok = await showOutboundServiceSheet( + context, + title: title, + description: description, + ); + if (ok != true || !context.mounted) return; + + final launched = await launchUrl(uri, mode: LaunchMode.externalApplication); + if (!launched && context.mounted) { + toastInfo(msg: fallbackToast); + } +} diff --git a/lib/common/utils/cjk_spacing.dart b/lib/common/utils/cjk_spacing.dart new file mode 100644 index 0000000..c85a687 --- /dev/null +++ b/lib/common/utils/cjk_spacing.dart @@ -0,0 +1,53 @@ +/// 中西文混排自动加间隙(W3C clreq §3.2.2)。 +/// +/// 规范要求:横排时汉字与西文字母/数字之间留约 1/4 em 间隙。CSS 有 +/// `text-autospace`,Flutter 没有对应能力,因此在文本层插入 +/// U+2009 THIN SPACE(约 1/5 em,视觉接近规范建议,且远比全角空格克制)。 +/// +/// 规则(与 pangu 习惯一致): +/// - 汉字 ↔ 字母/数字 相邻 → 插 thin space(「下降2.2%」→「下降 2.2%」); +/// - 西文 run 末尾的 % ‰ 视作 run 的一部分(「2.2%的降幅」→「2.2% 的降幅」); +/// - 全角标点(,。:等)自带空隙,不加; +/// - markdown 版本额外处理强调/行内码边界(「至**138美元**」→「至 **138美元**」), +/// 并跳过 fenced code block;表格管道符、链接 URL 天然不受影响。 +library; + +const String _thin = '\u2009'; // THIN SPACE + +// CJK 统一表意文字(基本区 + 扩展 A + 兼容区) +const String _han = r'㐀-䶿一-鿿豈-﫿'; +// 西文边界字符(字母/数字);西文侧在右边界额外允许 % ‰ 收尾 +const String _west = r'A-Za-z0-9'; + +final RegExp _hanWest = RegExp('([$_han])([$_west])'); +final RegExp _westHan = RegExp('([$_west%‰])([$_han])'); +// 强调/行内码标记夹在边界中间:汉**西 / 西**汉 +final RegExp _hanMarkWest = RegExp('([$_han])(\\*{1,3}|`)([$_west])'); +final RegExp _westMarkHan = RegExp('([$_west%‰])(\\*{1,3}|`)([$_han])'); + +/// 纯文本版:用于标题、副题、简介等 Text 控件。 +String cjkAutoSpace(String text) { + var s = text; + s = s.replaceAllMapped(_hanWest, (m) => '${m[1]}$_thin${m[2]}'); + s = s.replaceAllMapped(_westHan, (m) => '${m[1]}$_thin${m[2]}'); + return s; +} + +/// markdown 版:跳过 fenced code block,并处理强调/行内码边界。 +String cjkMarkdownAutoSpace(String md) { + final lines = md.split('\n'); + var inFence = false; + for (var i = 0; i < lines.length; i++) { + final t = lines[i].trimLeft(); + if (t.startsWith('```') || t.startsWith('~~~')) { + inFence = !inFence; + continue; + } + if (inFence) continue; + var s = lines[i]; + s = s.replaceAllMapped(_hanMarkWest, (m) => '${m[1]}$_thin${m[2]}${m[3]}'); + s = s.replaceAllMapped(_westMarkHan, (m) => '${m[1]}${m[2]}$_thin${m[3]}'); + lines[i] = cjkAutoSpace(s); + } + return lines.join('\n'); +} diff --git a/lib/common/widgets/adaptive.dart b/lib/common/widgets/adaptive.dart new file mode 100644 index 0000000..97c7f36 --- /dev/null +++ b/lib/common/widgets/adaptive.dart @@ -0,0 +1,86 @@ +import 'dart:math'; + +import 'package:flutter/widgets.dart'; + +import '../theme/spacing.dart'; + +/// iPad 响应式布局: +/// 宽 ≥ [breakpoint] 进入平板布局——屏边距 36、内容列统一限宽 920 居中 +/// (`padding: max(36, (宽-920)/2)`)、卡片列表两列(列距 18、行内等高)、 +/// 底部 tab 行限宽 600 居中。手机布局完全不变。 +abstract class Adaptive { + /// 平板断点(iPhone 最宽 ~440,iPad 11″ 竖屏 834 起) + static const double breakpoint = 700; + + /// 平板屏边距(demo `--pad-screen: 36px`) + static const double padTablet = 36; + + /// 内容列限宽(demo 所有页统一 920 居中) + static const double contentMaxWidth = 920; + + /// 两列列表的列间距(demo `column-gap: 18px`) + static const double columnGap = 18; + + /// 底部 tab 行限宽(demo `.nav .tabs{max-width:600px}`) + static const double tabsMaxWidth = 600; + + static bool isTablet(BuildContext context) => + MediaQuery.sizeOf(context).width >= breakpoint; + + /// 屏水平边距:手机 [Spacing.page];平板 `max(36, (宽-920)/2)`, + /// 即内容超 920 时左右对称留白实现居中限宽。 + static double hPad(BuildContext context) { + final w = MediaQuery.sizeOf(context).width; + if (w < breakpoint) return Spacing.page; + return max(padTablet, (w - contentMaxWidth) / 2); + } + + static EdgeInsets screenPadding( + BuildContext context, { + double top = 0, + double bottom = 0, + }) { + final h = hPad(context); + return EdgeInsets.fromLTRB(h, top, h, bottom); + } +} + +/// 卡片列表自适应 sliver:手机单列(卡间距 [Spacing.cardGap]); +/// 平板两列——卡片两两成行、行内等高(IntrinsicHeight + stretch, +/// 对应 demo CSS grid `1fr 1fr` 的等高行行为)。 +SliverList adaptiveCardSliver( + BuildContext context, { + required int itemCount, + required Widget Function(BuildContext, int) itemBuilder, + double rowGap = Spacing.cardGap, +}) { + if (!Adaptive.isTablet(context)) { + return SliverList.separated( + itemCount: itemCount, + separatorBuilder: (_, _) => SizedBox(height: rowGap), + itemBuilder: itemBuilder, + ); + } + final rows = (itemCount + 1) ~/ 2; + return SliverList.separated( + itemCount: rows, + separatorBuilder: (_, _) => SizedBox(height: rowGap), + itemBuilder: (c, r) { + final rightIndex = r * 2 + 1; + return IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded(child: itemBuilder(c, r * 2)), + const SizedBox(width: Adaptive.columnGap), + Expanded( + child: rightIndex < itemCount + ? itemBuilder(c, rightIndex) + : const SizedBox(), + ), + ], + ), + ); + }, + ); +} diff --git a/lib/common/widgets/app_state_card.dart b/lib/common/widgets/app_state_card.dart new file mode 100644 index 0000000..26f980c --- /dev/null +++ b/lib/common/widgets/app_state_card.dart @@ -0,0 +1,112 @@ +import 'package:flutter/material.dart'; +import '../theme/app_text.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +enum AppStateButtonStyle { filled, outline } + +class AppStateCard extends StatelessWidget { + const AppStateCard({ + super.key, + required this.icon, + required this.title, + required this.subtitle, + this.buttonLabel, + this.onButtonPressed, + this.buttonStyle = AppStateButtonStyle.outline, + }); + + final IconData icon; + final String title; + final String subtitle; + final String? buttonLabel; + final VoidCallback? onButtonPressed; + final AppStateButtonStyle buttonStyle; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final hasAction = buttonLabel != null && onButtonPressed != null; + + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 320), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 104, + height: 104, + alignment: Alignment.center, + decoration: BoxDecoration( + color: cs.secondary, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 46, color: cs.mutedForeground), + ), + const SizedBox(height: 22), + Text( + title, + textAlign: TextAlign.center, + style: AppText.cardTitle.copyWith( + fontSize: 22, + fontWeight: FontWeight.w700, + color: cs.foreground, + height: 1.2, + ), + ), + const SizedBox(height: 10), + Text( + subtitle, + textAlign: TextAlign.center, + style: AppText.body.copyWith( + fontSize: 15, + color: cs.mutedForeground, + height: 1.5, + ), + ), + if (hasAction) ...[ + const SizedBox(height: 20), + SizedBox( + height: 42, + child: buttonStyle == AppStateButtonStyle.filled + ? FilledButton( + onPressed: onButtonPressed, + style: FilledButton.styleFrom( + backgroundColor: cs.foreground, + foregroundColor: cs.background, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(horizontal: 18), + textStyle: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + child: Text(buttonLabel!), + ) + : OutlinedButton( + onPressed: onButtonPressed, + style: OutlinedButton.styleFrom( + foregroundColor: cs.foreground, + side: BorderSide(color: cs.border), + backgroundColor: cs.background, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + padding: const EdgeInsets.symmetric(horizontal: 18), + textStyle: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + child: Text(buttonLabel!), + ), + ), + ], + ], + ), + ); + } +} diff --git a/lib/common/widgets/app_toast.dart b/lib/common/widgets/app_toast.dart new file mode 100644 index 0000000..b9ab864 --- /dev/null +++ b/lib/common/widgets/app_toast.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +// void showAppToast(BuildContext context, String message) { +// ScaffoldMessenger.of(context) +// ..hideCurrentSnackBar() +// ..showSnackBar( +// SnackBar(content: Text(message), duration: const Duration(seconds: 2)), +// ); +// } + +Future toastInfo({ + required String msg, + Color backgroundColor = const Color(0xCC111111), + Color textColor = Colors.white, +}) { + return Fluttertoast.showToast( + msg: msg, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: backgroundColor, + textColor: textColor, + fontSize: 16, + ); +} diff --git a/lib/common/widgets/circle_back_button.dart b/lib/common/widgets/circle_back_button.dart new file mode 100644 index 0000000..94b3d30 --- /dev/null +++ b/lib/common/widgets/circle_back_button.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +/// 圆形返回钮(对齐 demo `.back`:38×38 圆 + 1px 描边 + chevron)。 +/// 左对齐到 12px(demo `.dhead` padding-left),用作各页 AppBar 的 leading。 +class CircleBackButton extends StatelessWidget { + const CircleBackButton({ + super.key, + this.onTap, + this.fallbackLocation = '/assets', + }); + + final VoidCallback? onTap; + final String fallbackLocation; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Align( + alignment: Alignment.centerLeft, + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: + onTap ?? + () { + if (context.canPop()) { + context.pop(); + } else { + context.go(fallbackLocation); + } + }, + child: Container( + width: 38, + height: 38, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: cs.border, width: 1), + ), + child: Icon( + FlutterRemix.arrow_left_s_line, + size: 22, + color: cs.foreground, + ), + ), + ), + ), + ); + } +} diff --git a/lib/common/widgets/detail_kit.dart b/lib/common/widgets/detail_kit.dart new file mode 100644 index 0000000..b1e8c1b --- /dev/null +++ b/lib/common/widgets/detail_kit.dart @@ -0,0 +1,254 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../theme/app_text.dart'; +import '../theme/spacing.dart'; + +// ───────────────────────────────────────────────────────────────────────── +// 详情/主页通用卡片语言。 +// ───────────────────────────────────────────────────────────────────────── + +/// 白底描边卡片(demo `.mcard`)。 +class DetailCard extends StatelessWidget { + const DetailCard({super.key, required this.child, this.color}); + final Widget child; + + /// 卡片底色,默认白 `card`。 + final Color? color; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Container( + width: double.infinity, + padding: const EdgeInsets.all(Spacing.cardPadding), + decoration: BoxDecoration( + color: color ?? cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(Spacing.radiusCard), + ), + child: child, + ); + } +} + +/// 卡片头部:编号圆角块(或图标)+ 标题(demo `.mh > .mnum + h2`)。 +class DetailSectionHeader extends StatelessWidget { + const DetailSectionHeader({ + super.key, + this.number, + this.icon, + required this.title, + }); + final int? number; + final IconData? icon; + final String title; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Row( + children: [ + Container( + width: 26, + height: 26, + alignment: Alignment.center, + decoration: BoxDecoration( + color: cs.accent, + borderRadius: BorderRadius.circular(7), + ), + child: number != null + ? Text( + '$number', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: cs.accentForeground, + ), + ) + : Icon(icon, size: 15, color: cs.accentForeground), + ), + const SizedBox(width: 10), + Expanded( + child: Text( + title, + style: AppText.listTitle.copyWith( + fontSize: 17, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + ), + ], + ); + } +} + +/// 徽标(demo `.badge` secondary / outline / 实底)。 +class DetailBadge extends StatelessWidget { + const DetailBadge( + this.label, { + super.key, + this.bg, + this.fg, + this.outline = false, + this.icon, + }); + final String label; + final Color? bg; + final Color? fg; + final bool outline; + final IconData? icon; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 3), + decoration: BoxDecoration( + color: outline ? Colors.transparent : (bg ?? cs.secondary), + borderRadius: BorderRadius.circular(Spacing.radiusBadge), + // 实底也带 1px 透明描边,与 outline 徽标等高等宽 + border: outline + ? Border.all(color: cs.border) + : Border.all(color: Colors.transparent), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon( + icon, + size: 10, + color: outline ? cs.mutedForeground : (fg ?? cs.foreground), + ), + const SizedBox(width: 3), + ], + Text( + label, + style: AppText.badge.copyWith( + color: outline ? cs.mutedForeground : (fg ?? cs.foreground), + ), + ), + ], + ), + ); + } +} + +/// 键值行(demo `.crow > .cl + .cv`)。value 可选链接样式 + 右上箭头,点击回调。 +class DetailKeyValueRow extends StatelessWidget { + const DetailKeyValueRow({ + super.key, + required this.label, + required this.value, + this.link = false, + this.onTap, + }); + final String label; + final String value; + final bool link; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final valueWidget = Row( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: Text( + value, + textAlign: TextAlign.right, + style: AppText.meta.copyWith( + fontSize: 13.5, + // 链接用深绿 accentForeground(lime 只做底色,不做文字色) + color: link ? cs.accentForeground : cs.foreground, + fontWeight: FontWeight.w500, + ), + ), + ), + if (link) ...[ + const SizedBox(width: 3), + Icon( + FlutterRemix.external_link_line, + size: 13, + color: cs.accentForeground, + ), + ], + ], + ); + return Container( + padding: const EdgeInsets.symmetric(vertical: 11), + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: cs.border)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: AppText.meta.copyWith( + fontSize: 13.5, + color: cs.mutedForeground, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Align( + alignment: Alignment.centerRight, + child: link && onTap != null + ? GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: valueWidget, + ) + : valueWidget, + ), + ), + ], + ), + ); + } +} + +/// 合规提示条(demo `.disc`:⚠ 图标 + 灰字)。 +class DetailDisclaimer extends StatelessWidget { + const DetailDisclaimer({super.key, required this.text}); + final String text; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + decoration: BoxDecoration( + color: cs.secondary, + borderRadius: BorderRadius.circular(Spacing.radiusBase), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + FlutterRemix.error_warning_line, + size: 15, + color: cs.mutedForeground, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + text, + style: AppText.meta.copyWith( + fontSize: 12, + color: cs.mutedForeground, + height: 1.5, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/common/widgets/login_required_dialog.dart b/lib/common/widgets/login_required_dialog.dart new file mode 100644 index 0000000..3c746ce --- /dev/null +++ b/lib/common/widgets/login_required_dialog.dart @@ -0,0 +1,121 @@ +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../theme/app_text.dart'; +import '../theme/spacing.dart'; + +Future showLoginRequiredDialog( + BuildContext context, { + required String title, + required String description, + String cancelLabel = '取消', + String confirmLabel = '去登录', +}) { + final cs = ShadTheme.of(context).colorScheme; + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + barrierColor: Colors.black.withValues(alpha: 0.5), + builder: (ctx) => Container( + width: double.infinity, + decoration: BoxDecoration( + color: cs.background, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(Spacing.radiusCard), + topRight: Radius.circular(Spacing.radiusCard), + ), + border: Border.all(color: cs.border), + ), + padding: const EdgeInsets.fromLTRB(20, 12, 20, 20), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(9999), + ), + ), + ), + Text( + title, + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 8), + Text( + description, + style: AppText.body.copyWith( + fontSize: 13, + height: 1.6, + color: cs.mutedForeground, + ), + ), + const SizedBox(height: 22), + _LoginSheetButton( + label: confirmLabel, + primary: true, + onTap: () => Navigator.of(ctx).pop(true), + ), + const SizedBox(height: 12), + _LoginSheetButton( + label: cancelLabel, + primary: false, + onTap: () => Navigator.of(ctx).pop(false), + ), + ], + ), + ), + ), + ); +} + +class _LoginSheetButton extends StatelessWidget { + const _LoginSheetButton({ + required this.label, + required this.primary, + required this.onTap, + }); + + final String label; + final bool primary; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + width: double.infinity, + height: 52, + alignment: Alignment.center, + decoration: BoxDecoration( + color: primary ? cs.primary : cs.secondary, + borderRadius: BorderRadius.circular(12), + border: primary ? null : Border.all(color: cs.border), + ), + child: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: primary ? cs.primaryForeground : cs.foreground, + ), + ), + ), + ); + } +} diff --git a/lib/common/widgets/markdown_style_utils.dart b/lib/common/widgets/markdown_style_utils.dart new file mode 100644 index 0000000..5a57696 --- /dev/null +++ b/lib/common/widgets/markdown_style_utils.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +/// Keep markdown horizontal rules subtle so backend `---` separators read as +/// a light divider instead of a heavy line. +MarkdownStyleSheet withMarkdownHairlineRule( + MarkdownStyleSheet base, + Color ruleColor, +) { + return base.copyWith( + horizontalRuleDecoration: BoxDecoration( + border: Border( + top: BorderSide(color: ruleColor.withValues(alpha: 0.85), width: 0.5), + ), + ), + ); +} + +MarkdownStyleSheet appMarkdownStyle(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return withMarkdownHairlineRule( + MarkdownStyleSheet( + // 正文:CJK 适宜行高 1.75,块间统一节奏 + p: TextStyle(fontSize: 14.5, height: 1.75, color: cs.foreground), + pPadding: EdgeInsets.zero, + blockSpacing: 14, + // 标题层级 + 上方留白形成分节(中文排版重段落分隔) + h1: TextStyle( + fontSize: 20, + height: 1.35, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + h1Padding: const EdgeInsets.only(top: 6, bottom: 2), + h2: TextStyle( + fontSize: 17, + height: 1.4, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + h2Padding: const EdgeInsets.only(top: 16, bottom: 2), + h3: TextStyle( + fontSize: 15, + height: 1.45, + fontWeight: FontWeight.w600, + color: cs.foreground, + ), + h3Padding: const EdgeInsets.only(top: 10), + // 列表:缩进 + 项目符号对齐正文 + listIndent: 22, + listBullet: TextStyle(fontSize: 14.5, height: 1.75, color: cs.foreground), + listBulletPadding: const EdgeInsets.only(right: 6), + // 强调:加粗醒目;中文不用斜体(合成斜体难看),em 走常规体 + strong: TextStyle(fontWeight: FontWeight.w700, color: cs.foreground), + em: TextStyle(fontStyle: FontStyle.normal, color: cs.foreground), + // 链接:accentForeground + 下划线(浅色深绿提升对比,深色仍 lime) + a: TextStyle( + color: cs.accentForeground, + decoration: TextDecoration.underline, + decorationColor: cs.accentForeground, + ), + // 表格:表头加粗、单元格留白、宽表横向滚动(IntrinsicColumnWidth 触发)、 + // 外角小圆角(嵌在 11.2 圆角卡片内,用更小一档的 8) + tableHead: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 13, + color: cs.foreground, + ), + // 表格数据用等宽数字(规范 3.1:tabular-nums),多行数值纵向对齐 + tableBody: TextStyle( + fontSize: 13, + height: 1.5, + color: cs.foreground, + fontFeatures: const [FontFeature.tabularFigures()], + ), + tableHeadAlign: TextAlign.left, + tableColumnWidth: const IntrinsicColumnWidth(), + tableCellsPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + tablePadding: const EdgeInsets.only(bottom: 4), + tableBorder: TableBorder.all( + color: cs.border, + width: 1, + borderRadius: BorderRadius.circular(8), + ), + // 引用块:中文不用斜体;浅底 + 主色左条 + 内距 + 右侧小圆角 + blockquoteDecoration: BoxDecoration( + color: cs.muted, + border: Border(left: BorderSide(color: cs.primary, width: 3)), + borderRadius: const BorderRadius.only( + topRight: Radius.circular(6), + bottomRight: Radius.circular(6), + ), + ), + blockquotePadding: const EdgeInsets.fromLTRB(14, 10, 12, 10), + blockquote: TextStyle( + fontSize: 13.5, + height: 1.7, + color: cs.mutedForeground, + ), + // 行内 code(如 `机构名`):用正文字体(DM Sans),不用等宽 Menlo—— + // 机构名/英文术语用等宽会显得突兀(demo 已改为正文字体)。仅保留浅底标记。 + code: TextStyle( + fontSize: 13.5, + height: 1.75, + color: cs.foreground, + backgroundColor: cs.muted, + fontFamilyFallback: const ['PingFang SC', 'Noto Sans CJK SC'], + ), + codeblockDecoration: BoxDecoration( + color: cs.muted, + borderRadius: BorderRadius.circular(8), + ), + ), + cs.border, + ); +} diff --git a/lib/common/widgets/markdown_table.dart b/lib/common/widgets/markdown_table.dart new file mode 100644 index 0000000..39982ad --- /dev/null +++ b/lib/common/widgets/markdown_table.dart @@ -0,0 +1,299 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../util/external_link.dart'; + +/// GFM 表格的「填满 / 滚动」渲染。 +/// +/// flutter_markdown_plus 的内置 `_buildTable()` 二选一:FlexColumnWidth 填满但 +/// 折行;IntrinsicColumnWidth 不折行但横向滚动里无法填满。自定义 builder 又被 +/// 包强制覆盖。故把表格从 markdown 抽出,用本组件接管: +/// - 量出各列内容固有宽度; +/// - 总宽 ≤ 可用宽 → 按比例放大列宽**填满整行**(不滚动、不折行); +/// - 总宽 > 可用宽 → 用固有列宽 + 横向滚动查看(不折行)。 +/// 单元格支持 `**加粗**` 与 `[文字](链接)`(点击走外部承接弹窗)。 +class MarkdownTable extends StatelessWidget { + const MarkdownTable({ + super.key, + required this.header, + required this.rows, + required this.aligns, + }); + + final List header; + final List> rows; + final List aligns; + + static const double _hPad = 12; + static const double _vPad = 8; + static const double _border = 1; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final n = header.length; + // 阅读字号倍率(表格在 ReaderScaled 子树内):量宽必须用同一 textScaler, + // 否则缩放后列宽测算与实际渲染不符。 + final textScaler = MediaQuery.textScalerOf(context); + + final headStyle = TextStyle( + fontWeight: FontWeight.w600, + fontSize: 13, + color: cs.foreground, + ); + final bodyStyle = TextStyle( + fontSize: 13, + height: 1.5, + color: cs.foreground, + fontFeatures: const [FontFeature.tabularFigures()], + ); + + // 各列固有宽 = 该列最宽单元格(含表头) + 左右内距;按加粗量(偏宽)更稳妥 + final intrinsic = List.filled(n, 0); + void acc(List cells) { + for (var c = 0; c < n && c < cells.length; c++) { + final w = _measure( + _stripMd(cells[c]), + bodyStyle.copyWith(fontWeight: FontWeight.w700), + textScaler, + ); + if (w > intrinsic[c]) intrinsic[c] = w; + } + } + + acc(header); + for (final r in rows) { + acc(r); + } + for (var c = 0; c < n; c++) { + intrinsic[c] += _hPad * 2; + } + + return LayoutBuilder( + builder: (context, constraints) { + final avail = constraints.maxWidth; + final sum = + intrinsic.fold(0, (a, b) => a + b) + (n + 1) * _border; + + List widths; + bool scroll; + if (sum <= avail) { + // 够宽:把多余宽度按列固有宽比例分摊,填满整行 + final extra = avail - sum; + final total = intrinsic.fold(0, (a, b) => a + b); + widths = [ + for (final w in intrinsic) + w + (total > 0 ? extra * (w / total) : extra / n), + ]; + scroll = false; + } else { + widths = intrinsic; + scroll = true; + } + + final table = Table( + defaultColumnWidth: const IntrinsicColumnWidth(), + columnWidths: { + for (var c = 0; c < n; c++) c: FixedColumnWidth(widths[c]), + }, + border: TableBorder.all( + color: cs.border, + width: _border, + borderRadius: BorderRadius.circular(8), + ), + children: [ + _row(context, header, headStyle, cs, isHeader: true), + for (final r in rows) _row(context, r, bodyStyle, cs), + ], + ); + + if (!scroll) return table; + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: avail), + child: table, + ), + ); + }, + ); + } + + TableRow _row( + BuildContext context, + List cells, + TextStyle style, + ShadColorScheme cs, { + bool isHeader = false, + }) { + return TableRow( + decoration: isHeader ? BoxDecoration(color: cs.muted) : null, + children: [ + for (var c = 0; c < header.length; c++) + Padding( + padding: const EdgeInsets.symmetric( + horizontal: _hPad, + vertical: _vPad, + ), + child: Align( + alignment: _alignment(c), + child: Text.rich( + _inlineSpans( + context, + c < cells.length ? cells[c] : '', + style, + cs, + ), + textAlign: c < aligns.length ? aligns[c] : TextAlign.left, + softWrap: false, + overflow: TextOverflow.visible, + ), + ), + ), + ], + ); + } + + Alignment _alignment(int c) { + final a = c < aligns.length ? aligns[c] : TextAlign.left; + return switch (a) { + TextAlign.center => Alignment.center, + TextAlign.right => Alignment.centerRight, + _ => Alignment.centerLeft, + }; + } + + static double _measure(String text, TextStyle style, TextScaler textScaler) { + final tp = TextPainter( + text: TextSpan(text: text, style: style), + maxLines: 1, + textDirection: TextDirection.ltr, + textScaler: textScaler, + )..layout(); + return tp.width; + } +} + +// ── 解析:从 markdown 抽出表格块,其余按原文保留 ────────────────────────── + +class MdSegment { + const MdSegment.text(this.text) : table = null; + const MdSegment.table(this.table) : text = ''; + final String text; + final MarkdownTable? table; + bool get isTable => table != null; +} + +final RegExp _sepLine = RegExp(r'^\s*\|?[\s:|-]*-[\s:|-]*\|?\s*$'); + +bool _isRowLine(String l) => l.contains('|'); +bool _isSepLine(String l) => + l.contains('-') && l.contains('|') && _sepLine.hasMatch(l); + +List _splitCells(String line) { + var t = line.trim(); + if (t.startsWith('|')) t = t.substring(1); + if (t.endsWith('|')) t = t.substring(0, t.length - 1); + return t.split('|').map((s) => s.trim()).toList(); +} + +/// 把 markdown 拆成「文本段」与「表格段」,顺序保留。 +List splitMarkdownTables(String md) { + final lines = md.split('\n'); + final out = []; + final buf = []; + void flush() { + if (buf.isNotEmpty) { + out.add(MdSegment.text(buf.join('\n'))); + buf.clear(); + } + } + + var i = 0; + while (i < lines.length) { + final isHeader = + _isRowLine(lines[i]) && + i + 1 < lines.length && + _isSepLine(lines[i + 1]); + if (!isHeader) { + buf.add(lines[i]); + i++; + continue; + } + flush(); + final headerCells = _splitCells(lines[i]); + // demo:忽略 markdown 列对齐,表格内容统一左对齐 + // (右对齐会让右列大片留白只显几个字)。 + final aligns = List.filled(headerCells.length, TextAlign.left); + final rows = >[]; + var j = i + 2; + while (j < lines.length && _isRowLine(lines[j]) && !_isSepLine(lines[j])) { + final cs = _splitCells(lines[j]); + rows.add([ + for (var c = 0; c < headerCells.length; c++) c < cs.length ? cs[c] : '', + ]); + j++; + } + out.add( + MdSegment.table( + MarkdownTable(header: headerCells, rows: rows, aligns: aligns), + ), + ); + i = j; + } + flush(); + return out; +} + +// ── 单元格内联:**加粗** 与 [文字](链接) ──────────────────────────────── + +final RegExp _inlineToken = RegExp(r'\*\*(.+?)\*\*|\[([^\]]+)\]\(([^)]+)\)'); + +String _stripMd(String s) => s + .replaceAllMapped(_inlineToken, (m) => m.group(1) ?? m.group(2) ?? '') + .replaceAll('*', ''); + +InlineSpan _inlineSpans( + BuildContext context, + String raw, + TextStyle base, + ShadColorScheme cs, +) { + final spans = []; + var last = 0; + for (final m in _inlineToken.allMatches(raw)) { + if (m.start > last) { + spans.add(TextSpan(text: raw.substring(last, m.start), style: base)); + } + if (m.group(1) != null) { + spans.add( + TextSpan( + text: m.group(1), + style: base.copyWith(fontWeight: FontWeight.w700), + ), + ); + } else { + final text = m.group(2)!; + final url = m.group(3)!; + spans.add( + TextSpan( + text: text, + style: base.copyWith( + color: cs.accentForeground, + decoration: TextDecoration.underline, + decorationColor: cs.accentForeground, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => + openExternalUrl(context, url, fallbackToast: '链接即将上线'), + ), + ); + } + last = m.end; + } + if (last < raw.length) { + spans.add(TextSpan(text: raw.substring(last), style: base)); + } + return TextSpan(children: spans); +} diff --git a/lib/common/widgets/outbound_service_sheet.dart b/lib/common/widgets/outbound_service_sheet.dart new file mode 100644 index 0000000..c960fef --- /dev/null +++ b/lib/common/widgets/outbound_service_sheet.dart @@ -0,0 +1,113 @@ +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../theme/app_text.dart'; + +Future showOutboundServiceSheet( + BuildContext context, { + String title = '即将打开外部服务', + String description = '你将离开金值,进入第三方服务页面。外部内容由对应服务提供。\n价格与资讯仅供参考,不构成投资建议。', + String cancelLabel = '取消', + String confirmLabel = '继续前往', +}) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: true, + enableDrag: true, + backgroundColor: Colors.transparent, + barrierColor: Colors.black.withValues(alpha: 0.45), + builder: (_) => OutboundServiceSheet( + title: title, + description: description, + cancelLabel: cancelLabel, + confirmLabel: confirmLabel, + ), + ); +} + +class OutboundServiceSheet extends StatelessWidget { + const OutboundServiceSheet({ + super.key, + this.title = '即将打开外部服务', + this.description = '你将离开金值,进入第三方服务页面。外部内容由对应服务提供。\n价格与资讯仅供参考,不构成投资建议。', + this.cancelLabel = '取消', + this.confirmLabel = '继续前往', + }); + + final String title; + final String description; + final String cancelLabel; + final String confirmLabel; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final bottomInset = MediaQuery.viewPaddingOf(context).bottom; + return Container( + width: double.infinity, + decoration: BoxDecoration( + color: cs.background, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(24), + topRight: Radius.circular(24), + ), + border: Border.all(color: cs.border), + ), + padding: EdgeInsets.fromLTRB(22, 12, 22, 22 + bottomInset), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 70, + height: 5, + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(999), + ), + ), + ), + const SizedBox(height: 26), + Text( + title, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 12), + Text( + description, + style: AppText.body.copyWith( + fontSize: 14, + height: 1.65, + color: cs.mutedForeground, + ), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: ShadButton.outline( + foregroundColor: cs.foreground, + onPressed: () => Navigator.of(context).pop(false), + child: Text(cancelLabel), + ), + ), + const SizedBox(width: 12), + Expanded( + child: ShadButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(confirmLabel), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/common/widgets/placeholder_tab_page.dart b/lib/common/widgets/placeholder_tab_page.dart new file mode 100644 index 0000000..c4f620a --- /dev/null +++ b/lib/common/widgets/placeholder_tab_page.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../theme/spacing.dart'; +import 'adaptive.dart'; + +class PlaceholderTabPage extends StatelessWidget { + const PlaceholderTabPage({ + super.key, + required this.title, + required this.description, + }); + + final String title; + final String description; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return SafeArea( + top: false, + child: ListView( + padding: const EdgeInsets.fromLTRB(18, 20, 18, 24), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: Adaptive.contentMaxWidth, + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(Spacing.radiusCard), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: cs.foreground, + letterSpacing: 0, + ), + ), + const SizedBox(height: 10), + Text( + description, + style: TextStyle( + fontSize: 14, + height: 1.6, + color: cs.mutedForeground, + letterSpacing: 0, + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/common/widgets/screen_header.dart b/lib/common/widgets/screen_header.dart new file mode 100644 index 0000000..09be198 --- /dev/null +++ b/lib/common/widgets/screen_header.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; +import '../theme/spacing.dart'; +import '../theme/app_text.dart'; +import 'adaptive.dart'; + +/// 各 tab 屏顶部标题区(对齐 demo `.apphead`)。 +/// 大标题 34/800/1.15 + 可选副标题 15/muted。 +class ScreenHeader extends StatelessWidget { + const ScreenHeader({super.key, required this.title, this.subtitle}); + + final String title; + final String? subtitle; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Padding( + padding: Adaptive.screenPadding(context, top: 10, bottom: 18), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: AppText.appTitle.copyWith(color: cs.foreground)), + if (subtitle != null) ...[ + const SizedBox(height: 8), + Text( + subtitle!, + style: AppText.body.copyWith( + color: cs.mutedForeground, + height: 1.4, + ), + ), + ], + ], + ), + ); + } +} + +/// 分区标题(对齐 demo `.section-title`):22/700 + 可选右尖角。 +class SectionTitle extends StatelessWidget { + const SectionTitle(this.title, {super.key, this.showChevron = false}); + + final String title; + final bool showChevron; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Padding( + padding: Adaptive.screenPadding( + context, + top: Spacing.sectionGap, + bottom: 16, + ), + child: Row( + children: [ + Text( + title, + style: AppText.sectionTitle.copyWith(color: cs.foreground), + ), + if (showChevron) ...[ + const SizedBox(width: 6), + Icon(Icons.chevron_right, size: 18, color: cs.mutedForeground), + ], + ], + ), + ); + } +} diff --git a/lib/common/widgets/skeleton_card.dart b/lib/common/widgets/skeleton_card.dart new file mode 100644 index 0000000..63f4f05 --- /dev/null +++ b/lib/common/widgets/skeleton_card.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; +import 'package:shimmer/shimmer.dart'; + +import '../theme/spacing.dart'; + +class SkeletonLineCard extends StatelessWidget { + const SkeletonLineCard({ + super.key, + this.lineWidths = const [0.4, 1, 0.8, 0.6], + this.height, + }); + + final List lineWidths; + final double? height; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Container( + height: height, + width: double.infinity, + padding: const EdgeInsets.all(Spacing.cardPadding), + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border, width: 1), + borderRadius: BorderRadius.circular(Spacing.radiusCard), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var i = 0; i < lineWidths.length; i++) ...[ + _SkeletonLine(widthFactor: lineWidths[i]), + if (i != lineWidths.length - 1) const SizedBox(height: 10), + ], + ], + ), + ); + } +} + +class SkeletonInstitutionCard extends StatelessWidget { + const SkeletonInstitutionCard({super.key}); + + @override + Widget build(BuildContext context) { + return const SkeletonLineCard(lineWidths: [0.4, 1, 0.6]); + } +} + +class SkeletonListenCard extends StatelessWidget { + const SkeletonListenCard({super.key}); + + @override + Widget build(BuildContext context) { + return const SkeletonLineCard(lineWidths: [0.4, 1, 0.6]); + } +} + +class SkeletonSectionCard extends StatelessWidget { + const SkeletonSectionCard({ + super.key, + required this.roman, + required this.lineWidths, + this.baseColor, + this.highlightColor, + }); + + final String roman; + final List lineWidths; + final Color? baseColor; + final Color? highlightColor; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final brightness = ShadTheme.of(context).brightness; + final resolvedBaseColor = + baseColor ?? + (brightness == Brightness.dark + ? cs.secondary + : cs.border.withValues(alpha: 0.95)); + final resolvedHighlightColor = + highlightColor ?? + (brightness == Brightness.dark + ? cs.card + : cs.secondary.withValues(alpha: 0.95)); + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: Spacing.cardPadding, + vertical: 16, + ), + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border, width: 1), + borderRadius: BorderRadius.circular(Spacing.radiusCard), + ), + child: Shimmer.fromColors( + baseColor: resolvedBaseColor, + highlightColor: resolvedHighlightColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var i = 0; i < lineWidths.length; i++) ...[ + _SkeletonLine( + widthFactor: lineWidths[i], + height: i == 0 ? 14 : 12, + ), + if (i != lineWidths.length - 1) const SizedBox(height: 10), + ], + const SizedBox(height: 2), + ], + ), + ), + ); + } +} + +class _SkeletonLine extends StatelessWidget { + const _SkeletonLine({required this.widthFactor, this.height = 12}); + + final double widthFactor; + final double height; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return FractionallySizedBox( + widthFactor: widthFactor, + child: Shimmer.fromColors( + baseColor: cs.secondary, + highlightColor: cs.muted, + child: Container( + height: height, + decoration: BoxDecoration( + color: cs.secondary, + borderRadius: BorderRadius.circular(6), + ), + ), + ), + ); + } +} + +class SectionSkeletonList extends StatelessWidget { + const SectionSkeletonList({ + super.key, + required this.count, + required this.lineWidthsForIndex, + this.romanLabels, + }); + + final int count; + final List Function(int index) lineWidthsForIndex; + final List? romanLabels; + + @override + Widget build(BuildContext context) { + final labels = + romanLabels ?? + List.generate(count, (index) => _roman(index + 1)); + return SliverPadding( + padding: const EdgeInsets.fromLTRB(Spacing.page, 4, Spacing.page, 24), + sliver: SliverList.separated( + itemCount: count, + separatorBuilder: (_, _) => const SizedBox(height: Spacing.cardGap), + itemBuilder: (context, index) => SkeletonSectionCard( + roman: labels[index], + lineWidths: lineWidthsForIndex(index), + ), + ), + ); + } +} + +class SectionSkeletonListView extends StatelessWidget { + const SectionSkeletonListView({ + super.key, + required this.count, + required this.lineWidthsForIndex, + this.romanLabels, + }); + + final int count; + final List Function(int index) lineWidthsForIndex; + final List? romanLabels; + + @override + Widget build(BuildContext context) { + final labels = + romanLabels ?? + List.generate(count, (index) => _roman(index + 1)); + return ListView.separated( + padding: const EdgeInsets.only(bottom: 72), + itemCount: count, + separatorBuilder: (_, _) => const SizedBox(height: Spacing.cardGap), + itemBuilder: (context, index) => SkeletonSectionCard( + roman: labels[index], + lineWidths: lineWidthsForIndex(index), + ), + ); + } +} + +String _roman(int value) { + const numerals = ['I.', 'II.', 'III.', 'IV.', 'V.', 'VI.', 'VII.', 'VIII.']; + if (value <= 0 || value > numerals.length) return '$value.'; + return numerals[value - 1]; +} diff --git a/lib/features/assets/application/asset_portfolio_controller.dart b/lib/features/assets/application/asset_portfolio_controller.dart new file mode 100644 index 0000000..faaefb3 --- /dev/null +++ b/lib/features/assets/application/asset_portfolio_controller.dart @@ -0,0 +1,122 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../common/domain/metal_type.dart'; +import '../../market/data/mock_market_repository.dart'; +import '../data/asset_models.dart'; +import '../data/mock_asset_repository.dart'; + +final assetPortfolioControllerProvider = + StateNotifierProvider((ref) { + final holdings = ref.watch(mockAssetRepositoryProvider).loadHoldings(); + final market = ref.watch(mockMarketRepositoryProvider); + return AssetPortfolioController( + holdings: holdings, + spotPriceFor: (metal) => market.quoteFor(metal).spotPrice, + changeFor: (metal) => market.quoteFor(metal).changeAmount, + ); + }); + +class AssetPortfolioController extends StateNotifier { + AssetPortfolioController({ + required List holdings, + required double Function(MetalType metal) spotPriceFor, + required double Function(MetalType metal) changeFor, + this.recycleDiscount = 0.97, + }) : _holdings = holdings, + _spotPriceFor = spotPriceFor, + _changeFor = changeFor, + super( + _buildSummary( + holdings: holdings, + spotPriceFor: spotPriceFor, + changeFor: changeFor, + recycleDiscount: recycleDiscount, + ), + ); + + final List _holdings; + final double Function(MetalType metal) _spotPriceFor; + final double Function(MetalType metal) _changeFor; + final double recycleDiscount; + + void addHolding(GoldAssetHolding holding) { + _holdings.add(holding); + state = _buildSummary( + holdings: _holdings, + spotPriceFor: _spotPriceFor, + changeFor: _changeFor, + recycleDiscount: recycleDiscount, + ); + } + + static PortfolioSummary _buildSummary({ + required List holdings, + required double Function(MetalType metal) spotPriceFor, + required double Function(MetalType metal) changeFor, + required double recycleDiscount, + }) { + final active = holdings + .where((holding) => holding.status == HoldingStatus.active) + .toList(growable: false); + final valuations = [ + for (final holding in active) + HoldingValuation( + holding: holding, + materialValue: holding.materialValue(spotPriceFor(holding.metal)), + todayChange: + holding.weightGram * holding.purity * changeFor(holding.metal), + ), + ]; + + final totalValue = valuations.fold( + 0, + (sum, item) => sum + item.materialValue, + ); + final todayChange = valuations.fold( + 0, + (sum, item) => sum + item.todayChange, + ); + final totalCost = active.fold( + 0, + (sum, holding) => sum + (holding.costAmount ?? 0), + ); + final totalWeight = active.fold( + 0, + (sum, holding) => sum + holding.weightGram, + ); + + return PortfolioSummary( + totalValue: totalValue, + todayChangeAmount: todayChange, + todayChangePercent: totalValue == 0 ? 0 : todayChange / totalValue * 100, + totalCost: totalCost, + totalWeightGram: totalWeight, + recycleReferenceValue: totalValue * recycleDiscount, + breakdowns: [ + for (final metal in MetalType.values) + _breakdownFor(metal, active, spotPriceFor), + ].where((item) => item.count > 0).toList(growable: false), + holdings: valuations, + ); + } + + static MetalBreakdown _breakdownFor( + MetalType metal, + List holdings, + double Function(MetalType metal) spotPriceFor, + ) { + final items = holdings.where((holding) => holding.metal == metal).toList(); + return MetalBreakdown( + metal: metal, + value: items.fold( + 0, + (sum, holding) => sum + holding.materialValue(spotPriceFor(metal)), + ), + weightGram: items.fold( + 0, + (sum, holding) => sum + holding.weightGram, + ), + count: items.length, + ); + } +} diff --git a/lib/features/assets/data/asset_models.dart b/lib/features/assets/data/asset_models.dart new file mode 100644 index 0000000..c2e00a6 --- /dev/null +++ b/lib/features/assets/data/asset_models.dart @@ -0,0 +1,102 @@ +import '../../../common/domain/metal_type.dart'; + +enum AssetCategory { + bar('金条'), + necklace('项链'), + ring('戒指'), + bracelet('手镯'), + bean('金豆'), + earring('耳环'), + silverware('银饰'), + platinumPiece('铂金件'); + + const AssetCategory(this.label); + + final String label; +} + +enum HoldingStatus { active, sold, gifted } + +class GoldAssetHolding { + const GoldAssetHolding({ + required this.id, + required this.name, + required this.metal, + required this.category, + required this.purity, + required this.purityLabel, + required this.weightGram, + required this.createdAt, + required this.updatedAt, + this.costAmount, + this.purchaseDate, + this.channel, + this.note, + this.status = HoldingStatus.active, + }); + + final String id; + final String name; + final MetalType metal; + final AssetCategory category; + final double purity; + final String purityLabel; + final double weightGram; + final double? costAmount; + final DateTime? purchaseDate; + final String? channel; + final String? note; + final DateTime createdAt; + final DateTime updatedAt; + final HoldingStatus status; + + double materialValue(double spotPrice) => weightGram * purity * spotPrice; +} + +class HoldingValuation { + const HoldingValuation({ + required this.holding, + required this.materialValue, + required this.todayChange, + }); + + final GoldAssetHolding holding; + final double materialValue; + final double todayChange; +} + +class MetalBreakdown { + const MetalBreakdown({ + required this.metal, + required this.value, + required this.weightGram, + required this.count, + }); + + final MetalType metal; + final double value; + final double weightGram; + final int count; +} + +class PortfolioSummary { + const PortfolioSummary({ + required this.totalValue, + required this.todayChangeAmount, + required this.todayChangePercent, + required this.totalCost, + required this.totalWeightGram, + required this.recycleReferenceValue, + required this.breakdowns, + required this.holdings, + }); + + final double totalValue; + final double todayChangeAmount; + final double todayChangePercent; + final double totalCost; + final double totalWeightGram; + final double recycleReferenceValue; + final List breakdowns; + final List holdings; +} diff --git a/lib/features/assets/data/mock_asset_repository.dart b/lib/features/assets/data/mock_asset_repository.dart new file mode 100644 index 0000000..455f47d --- /dev/null +++ b/lib/features/assets/data/mock_asset_repository.dart @@ -0,0 +1,73 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../common/domain/metal_type.dart'; +import 'asset_models.dart'; + +final mockAssetRepositoryProvider = Provider( + (ref) => MockAssetRepository(), +); + +class MockAssetRepository { + List loadHoldings() { + final now = DateTime(2026, 6, 18, 16, 11); + return [ + GoldAssetHolding( + id: 'holding_gold_bar_001', + name: '投资金条', + metal: MetalType.gold, + category: AssetCategory.bar, + purity: 0.9999, + purityLabel: '足金9999', + weightGram: 50, + costAmount: 43200, + purchaseDate: DateTime(2025, 10, 12), + channel: '银行', + createdAt: now.subtract(const Duration(days: 249)), + updatedAt: now, + ), + GoldAssetHolding( + id: 'holding_gold_ring_001', + name: '素圈戒指', + metal: MetalType.gold, + category: AssetCategory.ring, + purity: 0.999, + purityLabel: '足金999', + weightGram: 8.6, + costAmount: 7820, + purchaseDate: DateTime(2024, 12, 4), + channel: '金店', + createdAt: now.subtract(const Duration(days: 196)), + updatedAt: now, + ), + GoldAssetHolding( + id: 'holding_platinum_001', + name: 'PT950 项链', + metal: MetalType.platinum, + category: AssetCategory.platinumPiece, + purity: 0.95, + purityLabel: 'PT950', + weightGram: 12.3, + costAmount: 5200, + purchaseDate: DateTime(2023, 8, 21), + channel: '金店', + createdAt: now.subtract(const Duration(days: 667)), + updatedAt: now, + ), + GoldAssetHolding( + id: 'holding_silver_001', + name: '银手镯', + metal: MetalType.silver, + category: AssetCategory.bracelet, + purity: 0.999, + purityLabel: '999银', + weightGram: 31.8, + costAmount: 980, + purchaseDate: DateTime(2024, 4, 7), + channel: '金店', + note: '材料价值偏低', + createdAt: now.subtract(const Duration(days: 802)), + updatedAt: now, + ), + ]; + } +} diff --git a/lib/features/assets/presentation/assets_page.dart b/lib/features/assets/presentation/assets_page.dart new file mode 100644 index 0000000..ff2226c --- /dev/null +++ b/lib/features/assets/presentation/assets_page.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../../common/domain/money.dart'; +import '../../../common/widgets/adaptive.dart'; +import '../application/asset_portfolio_controller.dart'; + +class AssetsPage extends ConsumerWidget { + const AssetsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final summary = ref.watch(assetPortfolioControllerProvider); + final cs = ShadTheme.of(context).colorScheme; + return SafeArea( + top: false, + child: ListView( + padding: const EdgeInsets.fromLTRB(18, 18, 18, 24), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: Adaptive.contentMaxWidth, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('我的贵金属总估值', style: _mutedStyle(cs)), + const SizedBox(height: 10), + Text( + formatCny(summary.totalValue), + style: TextStyle( + fontSize: 34, + fontWeight: FontWeight.w800, + color: cs.foreground, + letterSpacing: 0, + ), + ), + const SizedBox(height: 8), + Text( + '今日 ${formatCny(summary.todayChangeAmount)} · ${summary.todayChangePercent.toStringAsFixed(2)}%', + style: TextStyle( + fontSize: 13, + color: summary.todayChangeAmount >= 0 + ? const Color(0xFFC0392B) + : const Color(0xFF2E8B6F), + ), + ), + ], + ), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final item in summary.breakdowns) + Chip( + label: Text( + '${item.metal.shortLabel} ${formatCny(item.value)}', + ), + avatar: CircleAvatar( + backgroundColor: item.metal.color, + ), + ), + ], + ), + const SizedBox(height: 12), + _Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '我的持仓 · ${summary.holdings.length} 件 · ${formatGram(summary.totalWeightGram)}', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 12), + for (final item in summary.holdings) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Expanded( + child: Text( + '${item.holding.name} · ${item.holding.purityLabel} · ${formatGram(item.holding.weightGram)}', + style: TextStyle(color: cs.foreground), + ), + ), + Text(formatCny(item.materialValue)), + ], + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _Card extends StatelessWidget { + const _Card({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return DecoratedBox( + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(12), + ), + child: Padding(padding: const EdgeInsets.all(16), child: child), + ); + } +} + +TextStyle _mutedStyle(ShadColorScheme cs) { + return TextStyle(fontSize: 13, color: cs.mutedForeground, letterSpacing: 0); +} diff --git a/lib/features/auth/application/auth_controller.dart b/lib/features/auth/application/auth_controller.dart new file mode 100644 index 0000000..cd0a6c9 --- /dev/null +++ b/lib/features/auth/application/auth_controller.dart @@ -0,0 +1,68 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../common/network/auth_interceptor.dart'; +import '../../../common/storage/storage_keys.dart'; +import '../../../common/storage/storage_service.dart'; +import '../data/auth_models.dart'; + +final authProvider = + StateNotifierProvider>( + (ref) => AuthController(), + ); + +class AuthController extends StateNotifier> { + AuthController() : super(const AsyncLoading()) { + _bootstrap(); + } + + Future _bootstrap() async { + final token = await loadJwt(); + if (token == null || token.isEmpty) { + state = const AsyncData(GuestAuthState()); + return; + } + final phone = StorageService.to.getString(storageJinzhiMockLoggedIn) ?? ''; + state = AsyncData( + LoggedInAuthState( + user: JinzhiUser( + phone: phone.isEmpty ? '13800000000' : phone, + nickname: '金值用户', + createdAt: DateTime(2026, 6, 18), + ), + token: token, + ), + ); + } + + Future sendCode(String phone) async { + await Future.delayed(const Duration(milliseconds: 200)); + } + + Future verifyCode(String phone, String code) async { + final token = 'mock-jinzhi-token-$phone-$code'; + await saveJwt(token); + await StorageService.to.setString(storageJinzhiMockLoggedIn, phone); + state = AsyncData( + LoggedInAuthState( + user: JinzhiUser( + phone: phone, + nickname: '金值用户', + createdAt: DateTime.now(), + ), + token: token, + ), + ); + } + + Future logout() async { + await clearJwt(); + await StorageService.to.setString(storageJinzhiMockLoggedIn, ''); + state = const AsyncData(GuestAuthState()); + } + + Future deleteAccount([String? code]) async { + await logout(); + } + + bool get isLoggedIn => state.valueOrNull is LoggedInAuthState; +} diff --git a/lib/features/auth/data/auth_models.dart b/lib/features/auth/data/auth_models.dart new file mode 100644 index 0000000..d5ffda4 --- /dev/null +++ b/lib/features/auth/data/auth_models.dart @@ -0,0 +1,26 @@ +class JinzhiUser { + const JinzhiUser({ + required this.phone, + required this.nickname, + required this.createdAt, + }); + + final String phone; + final String nickname; + final DateTime createdAt; +} + +sealed class AuthState { + const AuthState(); +} + +class GuestAuthState extends AuthState { + const GuestAuthState(); +} + +class LoggedInAuthState extends AuthState { + const LoggedInAuthState({required this.user, required this.token}); + + final JinzhiUser user; + final String token; +} diff --git a/lib/features/auth/presentation/delete_account_page.dart b/lib/features/auth/presentation/delete_account_page.dart new file mode 100644 index 0000000..7267826 --- /dev/null +++ b/lib/features/auth/presentation/delete_account_page.dart @@ -0,0 +1,916 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../../common/network/api_error_message.dart'; +import '../../../common/router/app_router.dart'; +import '../../../common/theme/app_text.dart'; +import '../../../common/theme/jinzhi_theme.dart'; +import '../../../common/theme/spacing.dart'; +import '../../../common/widgets/circle_back_button.dart'; +import '../application/auth_controller.dart'; +import '../data/auth_models.dart'; + +class DeleteAccountPage extends ConsumerWidget { + const DeleteAccountPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final authAsync = ref.watch(authProvider); + final cs = ShadTheme.of(context).colorScheme; + + return authAsync.when( + loading: () => Scaffold( + backgroundColor: cs.background, + body: const Center(child: CircularProgressIndicator()), + ), + error: (e, _) => Scaffold( + backgroundColor: cs.background, + body: Center(child: Text('加载失败:$e')), + ), + data: (authState) { + if (authState is! LoggedInAuthState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!context.mounted) return; + context.go( + '${AppRoutes.login}?next=${Uri.encodeComponent(AppRoutes.deleteAccount)}', + ); + }); + return Scaffold( + backgroundColor: cs.background, + body: const Center(child: CircularProgressIndicator()), + ); + } + + return Scaffold( + backgroundColor: cs.background, + appBar: AppBar( + backgroundColor: cs.background, + surfaceTintColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, + title: Text( + '注销账号', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + leading: CircleBackButton( + onTap: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(AppRoutes.settings); + } + }, + ), + ), + body: SafeArea( + top: false, + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB( + Spacing.page, + 8, + Spacing.page, + 28, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const _WarnCard(), + const SizedBox(height: 28), + Text( + '注销前请确认', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 14), + const _DeleteCheckList(), + const SizedBox(height: 28), + _DangerButton( + label: '开始注销', + onPressed: () => + _showVerifyDialog(context, ref, authState.user.phone), + ), + const SizedBox(height: 14), + Text( + '注销后,账号资料与历史内容将无法恢复。', + textAlign: TextAlign.center, + style: AppText.meta.copyWith( + fontSize: 11.5, + color: cs.mutedForeground, + height: 1.6, + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Future _showVerifyDialog( + BuildContext context, + WidgetRef ref, + String phone, + ) async { + final cs = ShadTheme.of(context).colorScheme; + final completed = await showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + enableDrag: false, + backgroundColor: cs.card, + barrierColor: Colors.black.withValues(alpha: 0.5), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(Spacing.radiusCard), + ), + ), + builder: (_) => _VerifySheet(phone: phone), + ); + if (!context.mounted || completed != true) return; + context.go(AppRoutes.deleteAccountCompleted); + } +} + +String _maskPhone(String phone) { + if (phone.length < 7) return phone; + return '${phone.substring(0, 3)}****${phone.substring(phone.length - 4)}'; +} + +/// 危险主按钮(demo `.da-btn`:destructive 实底 pill、52 高、16/600)。 +class _DangerButton extends StatelessWidget { + const _DangerButton({required this.label, required this.onPressed}); + + final String label; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return GestureDetector( + onTap: onPressed, + behavior: HitTestBehavior.opaque, + child: Container( + width: double.infinity, + height: 52, + alignment: Alignment.center, + decoration: BoxDecoration( + color: cs.destructive, + borderRadius: BorderRadius.circular(9999), + ), + child: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: cs.destructiveForeground, + ), + ), + ), + ); + } +} + +/// sheet 内操作按钮(demo `.lo-btn`:52 高、radius 12、16/600; +/// primary=lime / danger=destructive / cancel=secondary+描边)。 +enum _SheetBtnStyle { primary, danger, cancel } + +class _SheetActionButton extends StatelessWidget { + const _SheetActionButton({ + required this.label, + required this.style, + required this.onTap, + this.enabled = true, + }); + + final String label; + final _SheetBtnStyle style; + final VoidCallback onTap; + final bool enabled; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final Color bg; + final Color fg; + Border? border; + switch (style) { + case _SheetBtnStyle.primary: + bg = cs.primary; + fg = cs.primaryForeground; + case _SheetBtnStyle.danger: + bg = cs.destructive; + fg = cs.destructiveForeground; + case _SheetBtnStyle.cancel: + bg = cs.secondary; + fg = cs.foreground; + border = Border.all(color: cs.border); + } + return GestureDetector( + onTap: enabled ? onTap : null, + behavior: HitTestBehavior.opaque, + child: Opacity( + opacity: enabled ? 1 : 0.55, + child: Container( + width: double.infinity, + height: 52, + alignment: Alignment.center, + decoration: BoxDecoration( + color: bg, + borderRadius: BorderRadius.circular(12), + border: border, + ), + child: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: fg, + ), + ), + ), + ), + ); + } +} + +class _PinInput extends StatefulWidget { + const _PinInput({ + required this.controller, + required this.onCompleted, + this.enabled = true, + }); + + final TextEditingController controller; + final ValueChanged onCompleted; + final bool enabled; + + static const length = 6; + + @override + State<_PinInput> createState() => _PinInputState(); +} + +class _PinInputState extends State<_PinInput> { + final _focusNode = FocusNode(); + String _lastText = ''; + + @override + void initState() { + super.initState(); + widget.controller.addListener(_onChanged); + } + + @override + void didUpdateWidget(covariant _PinInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.enabled && !oldWidget.enabled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _focusNode.requestFocus(); + }); + } + } + + @override + void dispose() { + widget.controller.removeListener(_onChanged); + _focusNode.dispose(); + super.dispose(); + } + + void _onChanged() { + final value = widget.controller.text; + if (value == _lastText) return; + _lastText = value; + setState(() {}); + if (value.length == _PinInput.length) { + FocusScope.of(context).unfocus(); + widget.onCompleted(value); + } + } + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final filled = widget.controller.text; + return GestureDetector( + onTap: widget.enabled ? () => _focusNode.requestFocus() : null, + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: 56, + child: Stack( + alignment: Alignment.center, + children: [ + Row( + children: List.generate(_PinInput.length, (i) { + final hasChar = i < filled.length; + final isFocus = i == filled.length && widget.enabled; + return Expanded( + child: Padding( + padding: EdgeInsets.only( + right: i == _PinInput.length - 1 ? 0 : 12, + ), + child: _PinCell( + char: hasChar ? filled[i] : '', + focused: isFocus, + cs: cs, + ), + ), + ); + }), + ), + Positioned.fill( + child: Opacity( + opacity: 0, + child: TextField( + controller: widget.controller, + focusNode: _focusNode, + enabled: widget.enabled, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(_PinInput.length), + ], + showCursor: false, + autofocus: false, + decoration: const InputDecoration( + border: InputBorder.none, + counterText: '', + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +class _PinCell extends StatelessWidget { + const _PinCell({required this.char, required this.focused, required this.cs}); + + final String char; + final bool focused; + final ShadColorScheme cs; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 140), + height: 56, + decoration: BoxDecoration( + color: cs.background, + borderRadius: BorderRadius.circular(Spacing.radiusBtn), + border: Border.all( + color: focused ? cs.primary : cs.border, + width: focused ? 1.4 : 1, + ), + ), + alignment: Alignment.center, + child: Text( + char, + style: AppText.sectionTitle.copyWith( + fontSize: 19, + color: cs.foreground, + height: 1, + ), + ), + ); + } +} + +class _WarnCard extends StatelessWidget { + const _WarnCard(); + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final b = ShadTheme.of(context).brightness; + return Container( + padding: const EdgeInsets.all(Spacing.cardPadding), + decoration: BoxDecoration( + color: destructiveSoft(b), + border: Border.all(color: destructiveSoftBorder(b)), + borderRadius: BorderRadius.circular(Spacing.radiusCard), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 42, + height: 42, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: cs.destructive, + ), + child: Icon( + FlutterRemix.alert_fill, + size: 21, + color: cs.destructiveForeground, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + height: 1.3, + color: cs.foreground, + ), + children: [ + const TextSpan(text: '此操作'), + TextSpan( + text: '不可恢复', + style: TextStyle(color: cs.destructive), + ), + ], + ), + ), + const SizedBox(height: 6), + Text( + '注销后,账号、阅读历史、个人数据将永久失效且无法找回。', + style: TextStyle( + fontSize: 13, + height: 1.6, + color: cs.mutedForeground, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _DeleteCheckList extends StatelessWidget { + const _DeleteCheckList(); + + static const _items = [ + ('账户处于安全状态', '您的账户处于安全状态,无异常登录。'), + ('账户信息将被清空', '您的个人账户相关信息将被清空且无法恢复。'), + ('浏览记录将被清空', '您最近阅读的文章将被清空且无法恢复。'), + ]; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Container( + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(Spacing.radiusCard), + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + for (var i = 0; i < _items.length; i++) ...[ + if (i > 0) Divider(height: 1, thickness: 1, color: cs.border), + _DeleteCheckItem( + number: i + 1, + title: _items[i].$1, + desc: _items[i].$2, + ), + ], + ], + ), + ); + } +} + +class _DeleteCheckItem extends StatelessWidget { + const _DeleteCheckItem({ + required this.number, + required this.title, + required this.desc, + }); + + final int number; + final String title; + final String desc; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.fromLTRB(16, 15, 16, 15), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 24, + height: 24, + margin: const EdgeInsets.only(top: 1), + alignment: Alignment.center, + decoration: BoxDecoration( + color: cs.accent, + borderRadius: BorderRadius.circular(7), + ), + child: Text( + '$number', + style: AppText.meta.copyWith( + fontSize: 13, + fontWeight: FontWeight.w700, + color: cs.accentForeground, + height: 1, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: cs.foreground, + height: 1.4, + ), + ), + const SizedBox(height: 3), + Text( + desc, + style: TextStyle( + fontSize: 13, + color: cs.mutedForeground, + height: 1.55, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +/// 注销短信验证底部 sheet(对齐 demo `deleteVerifySheet`): +/// 未发送(说明 + 发送验证码[primary]/取消)→ 已发送(OTP 6 格 + 重发倒计时 + +/// 确认注销[danger]/取消)→ 提交中(spinner)。 +class _VerifySheet extends ConsumerStatefulWidget { + const _VerifySheet({required this.phone}); + + final String phone; + + @override + ConsumerState<_VerifySheet> createState() => _VerifySheetState(); +} + +class _VerifySheetState extends ConsumerState<_VerifySheet> { + final _otpController = TextEditingController(); + Timer? _timer; + int _countdown = 0; + bool _codeSent = false; + bool _sending = false; + bool _submitting = false; + String? _error; + + @override + void dispose() { + _otpController.dispose(); + _timer?.cancel(); + super.dispose(); + } + + void _startCountdown() { + _timer?.cancel(); + setState(() => _countdown = 60); + _timer = Timer.periodic(const Duration(seconds: 1), (t) { + if (!mounted) { + t.cancel(); + return; + } + if (_countdown <= 1) { + t.cancel(); + setState(() => _countdown = 0); + } else { + setState(() => _countdown -= 1); + } + }); + } + + Future _sendCode() async { + if (_sending || _countdown > 0) return; + setState(() { + _sending = true; + _error = null; + }); + try { + await ref.read(authProvider.notifier).sendCode(widget.phone); + if (!mounted) return; + setState(() { + _codeSent = true; + _otpController.clear(); + }); + _startCountdown(); + } catch (e) { + if (!mounted) return; + setState(() { + _error = kratosDisplayMessage(e, fallback: '发送失败,请稍后重试'); + }); + } finally { + if (mounted) setState(() => _sending = false); + } + } + + Future _confirm() async { + final code = _otpController.text.trim().replaceAll(' ', ''); + if (code.length != 6) { + setState(() => _error = '请输入6位验证码'); + return; + } + setState(() { + _submitting = true; + _error = null; + }); + try { + await ref.read(authProvider.notifier).deleteAccount(code); + if (!mounted) return; + Navigator.of(context).pop(true); + } catch (e) { + if (!mounted) return; + setState(() { + _error = kratosDisplayMessage(e, fallback: '注销失败,请重试'); + }); + } finally { + if (mounted) setState(() => _submitting = false); + } + } + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Padding( + // 键盘弹出时 sheet 跟随上移 + padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), + child: SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // grip + Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(9999), + ), + ), + ), + Text( + '短信验证', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 8), + if (_submitting) + ..._buildSubmitting(cs) + else if (!_codeSent) + ..._buildSendStep(cs) + else + ..._buildCodeStep(cs), + ], + ), + ), + ), + ); + } + + List _buildSubmitting(ShadColorScheme cs) { + return [ + Text( + '正在清除账户数据,请稍候', + style: TextStyle( + fontSize: 13, + height: 1.55, + color: cs.mutedForeground, + ), + ), + const SizedBox(height: 28), + Center( + child: SizedBox( + width: 32, + height: 32, + child: CircularProgressIndicator(strokeWidth: 2.4, color: cs.primary), + ), + ), + const SizedBox(height: 28), + ]; + } + + List _buildSendStep(ShadColorScheme cs) { + return [ + Text( + '为保障账户安全,注销前需验证您的手机号。验证码将发送至 ${_maskPhone(widget.phone)}。', + style: TextStyle( + fontSize: 13, + height: 1.55, + color: cs.mutedForeground, + ), + ), + if (_error != null) ...[ + const SizedBox(height: 10), + Text(_error!, style: TextStyle(fontSize: 13, color: cs.destructive)), + ], + const SizedBox(height: 22), + _SheetActionButton( + label: _sending ? '发送中…' : '发送验证码', + style: _SheetBtnStyle.primary, + enabled: !_sending, + onTap: _sendCode, + ), + const SizedBox(height: 12), + _SheetActionButton( + label: '取消', + style: _SheetBtnStyle.cancel, + enabled: !_sending, + onTap: () => Navigator.of(context).pop(false), + ), + ]; + } + + List _buildCodeStep(ShadColorScheme cs) { + return [ + Text( + '验证码已发送至 ${_maskPhone(widget.phone)}', + style: TextStyle( + fontSize: 13, + height: 1.55, + color: cs.mutedForeground, + ), + ), + const SizedBox(height: 20), + _PinInput( + controller: _otpController, + enabled: !_submitting, + onCompleted: (_) => _confirm(), + ), + if (_error != null) ...[ + const SizedBox(height: 10), + Text(_error!, style: TextStyle(fontSize: 13, color: cs.destructive)), + ], + const SizedBox(height: 14), + Center( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _countdown > 0 || _sending ? null : _sendCode, + child: Text( + _countdown > 0 ? '$_countdown s 后可重新发送' : '重新发送', + style: TextStyle( + fontSize: 13, + color: _countdown > 0 ? cs.mutedForeground : cs.primary, + ), + ), + ), + ), + const SizedBox(height: 18), + _SheetActionButton( + label: '确认注销', + style: _SheetBtnStyle.danger, + enabled: !_submitting, + onTap: _confirm, + ), + const SizedBox(height: 12), + _SheetActionButton( + label: '取消', + style: _SheetBtnStyle.cancel, + enabled: !_submitting, + onTap: () => Navigator.of(context).pop(false), + ), + ]; + } +} + +class DeleteAccountSuccessPage extends ConsumerWidget { + const DeleteAccountSuccessPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final cs = ShadTheme.of(context).colorScheme; + return Scaffold( + backgroundColor: cs.background, + body: SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB( + Spacing.page, + 72, + Spacing.page, + 28, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: Container( + width: 72, + height: 72, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: cs.primary, + ), + child: Icon( + FlutterRemix.check_line, + size: 34, + color: cs.primaryForeground, + ), + ), + ), + const SizedBox(height: 24), + Text( + '账户已注销', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w700, + height: 1.4, + color: cs.foreground, + ), + ), + const SizedBox(height: 10), + Text( + '您的账号及相关数据已清除完毕\n感谢您曾使用研听', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 13, + color: cs.mutedForeground, + height: 1.7, + ), + ), + const SizedBox(height: 48), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () async { + await ref.read(authProvider.notifier).logout(); + if (context.mounted) context.go(AppRoutes.profile); + }, + child: Container( + width: double.infinity, + height: 52, + alignment: Alignment.center, + decoration: BoxDecoration( + color: cs.primary, + borderRadius: BorderRadius.circular(9999), + ), + child: Text( + '返回首页', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: cs.primaryForeground, + ), + ), + ), + ), + const Spacer(), + Text( + '如未来需再次使用,可重新注册账号\n但原历史数据无法迁移恢复', + textAlign: TextAlign.center, + style: AppText.meta.copyWith( + fontSize: 11, + color: cs.mutedForeground, + height: 1.7, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/auth/presentation/login_page.dart b/lib/features/auth/presentation/login_page.dart new file mode 100644 index 0000000..4243850 --- /dev/null +++ b/lib/features/auth/presentation/login_page.dart @@ -0,0 +1,702 @@ +import 'dart:async'; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import '../../../common/config/app_h5_urls.dart'; +import '../../../common/network/api_error_message.dart'; +import '../../../common/router/app_router.dart'; +import '../../../common/theme/spacing.dart'; +import '../../../common/widgets/circle_back_button.dart'; +import '../application/auth_controller.dart'; + +class LoginPage extends HookConsumerWidget { + const LoginPage({super.key, this.next}); + final String? next; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final cs = ShadTheme.of(context).colorScheme; + final phone = useTextEditingController(); + final codeCtrl = useTextEditingController(); + final codeFocus = useFocusNode(); + final onCodeStep = useState(false); + final agreed = useState(false); + final sending = useState(false); + final verifying = useState(false); + final countdown = useState(0); + final error = useState(null); + final timer = useRef(null); + useListenable(phone); + useListenable(codeCtrl); + useEffect( + () => + () => timer.value?.cancel(), + const [], + ); + + String phoneDigits() => phone.text.replaceAll(RegExp(r'\D'), ''); + + Future returnAfterLogin() async { + final target = next?.trim(); + final router = GoRouter.of(context); + if (target == null || target.isEmpty) { + router.go(AppRoutes.profile); + return; + } + router.go(target); + } + + void startCountdown() { + countdown.value = 60; + timer.value?.cancel(); + timer.value = Timer.periodic(const Duration(seconds: 1), (t) { + if (countdown.value <= 1) { + t.cancel(); + countdown.value = 0; + } else { + countdown.value--; + } + }); + } + + Future doSend() async { + if (sending.value || countdown.value > 0) return; + if (phoneDigits().length != 11) { + error.value = '请输入正确的手机号'; + return; + } + sending.value = true; + error.value = null; + try { + await ref.read(authProvider.notifier).sendCode(phoneDigits()); + startCountdown(); + onCodeStep.value = true; + codeFocus.requestFocus(); + } catch (e) { + error.value = kratosDisplayMessage(e, fallback: '发送失败,请稍后重试'); + } finally { + sending.value = false; + } + } + + Future doVerify() async { + if (verifying.value) return; + final code = codeCtrl.text.trim(); + if (code.length != 6) { + error.value = '请输入 6 位验证码'; + return; + } + verifying.value = true; + error.value = null; + try { + await ref.read(authProvider.notifier).verifyCode(phoneDigits(), code); + if (!context.mounted) return; + await returnAfterLogin(); + } catch (e) { + error.value = kratosDisplayMessage(e, fallback: '验证失败,请重试'); + } finally { + verifying.value = false; + } + } + + void onBack() { + if (onCodeStep.value) { + onCodeStep.value = false; + error.value = null; + } else if (context.canPop()) { + context.pop(); + } else { + context.go(AppRoutes.profile); + } + } + + void showPrivacySheet(VoidCallback onAgreed) { + showModalBottomSheet( + context: context, + backgroundColor: cs.card, + barrierColor: Colors.black.withValues(alpha: 0.6), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(Spacing.radiusCard), + ), + ), + builder: (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(22, 28, 22, 24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + '请阅读并同意以下条款', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: cs.foreground, + ), + ), + const SizedBox(height: 14), + Text.rich( + textAlign: TextAlign.center, + _policySpan(context, cs, fontSize: 12.5), + ), + const SizedBox(height: 22), + _PrimaryButton( + label: '同意并继续', + enabled: true, + onTap: () { + Navigator.of(ctx).pop(); + onAgreed(); + }, + ), + ], + ), + ), + ), + ); + } + + void onSendTap() { + if (sending.value || countdown.value > 0) return; + if (phoneDigits().length != 11) { + error.value = '请输入正确的手机号'; + return; + } + error.value = null; + if (!agreed.value) { + showPrivacySheet(() { + agreed.value = true; + doSend(); + }); + return; + } + doSend(); + } + + final phoneReady = phoneDigits().length == 11; + final codeReady = codeCtrl.text.trim().length == 6; + + return Scaffold( + backgroundColor: cs.background, + appBar: AppBar( + backgroundColor: cs.background, + surfaceTintColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + automaticallyImplyLeading: false, + toolbarHeight: 46, + titleSpacing: 0, + title: CircleBackButton(onTap: onBack), + ), + body: SafeArea( + top: false, + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(22, 8, 22, 28), + child: onCodeStep.value + ? _CodeStep( + cs: cs, + phone: phoneDigits(), + codeCtrl: codeCtrl, + codeFocus: codeFocus, + countdown: countdown.value, + verifying: verifying.value, + codeReady: codeReady, + error: error.value, + onVerify: doVerify, + onResend: doSend, + ) + : _PhoneStep( + cs: cs, + phone: phone, + sending: sending.value, + countdown: countdown.value, + phoneReady: phoneReady, + error: error.value, + agreed: agreed.value, + onToggleAgreed: () => agreed.value = !agreed.value, + onSend: onSendTap, + ), + ), + ), + ); + } +} + +class _PhoneStep extends StatelessWidget { + const _PhoneStep({ + required this.cs, + required this.phone, + required this.sending, + required this.countdown, + required this.phoneReady, + required this.agreed, + required this.onToggleAgreed, + required this.error, + required this.onSend, + }); + + final ShadColorScheme cs; + final TextEditingController phone; + final bool sending; + final int countdown; + final bool phoneReady; + final bool agreed; + final VoidCallback onToggleAgreed; + final String? error; + final VoidCallback onSend; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SheetTitle('手机号登录', cs), + const SizedBox(height: 8), + _SheetSub('未注册的手机号验证后将自动创建研听账号。', cs), + const SizedBox(height: 18), + Row( + children: [ + SizedBox( + height: 50, + child: Center( + child: Text( + '+86', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: cs.foreground, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: _BoxedField(controller: phone, hint: '请输入手机号', cs: cs), + ), + ], + ), + if (error != null) ...[ + const SizedBox(height: 10), + _ErrorText(error!, cs), + ], + const SizedBox(height: 18), + _PrimaryButton( + label: countdown > 0 ? '$countdown s 后可重新发送' : '获取验证码', + enabled: !sending && countdown == 0, + loading: sending, + dim: !phoneReady, + onTap: onSend, + ), + const SizedBox(height: 18), + _AgreeRow(agreed: agreed, onToggle: onToggleAgreed, cs: cs), + ], + ); + } +} + +class _CodeStep extends StatelessWidget { + const _CodeStep({ + required this.cs, + required this.phone, + required this.codeCtrl, + required this.codeFocus, + required this.countdown, + required this.verifying, + required this.codeReady, + required this.error, + required this.onVerify, + required this.onResend, + }); + + final ShadColorScheme cs; + final String phone; + final TextEditingController codeCtrl; + final FocusNode codeFocus; + final int countdown; + final bool verifying; + final bool codeReady; + final String? error; + final VoidCallback onVerify; + final VoidCallback onResend; + + String get _masked => phone.length == 11 + ? '${phone.substring(0, 3)}****${phone.substring(7)}' + : phone; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _SheetTitle('输入验证码', cs), + const SizedBox(height: 8), + _SheetSub('验证码已发送至 +86 $_masked', cs), + const SizedBox(height: 20), + _OtpInput(controller: codeCtrl, focusNode: codeFocus, cs: cs), + if (error != null) ...[ + const SizedBox(height: 12), + _ErrorText(error!, cs), + ], + const SizedBox(height: 18), + _PrimaryButton( + label: '验证并登录', + enabled: !verifying, + loading: verifying, + dim: !codeReady, + onTap: onVerify, + ), + const SizedBox(height: 16), + Center( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: countdown > 0 ? null : onResend, + child: Text( + countdown > 0 ? '$countdown s 后可重新发送' : '重新发送', + style: TextStyle( + fontSize: 13, + color: countdown > 0 ? cs.mutedForeground : cs.primary, + ), + ), + ), + ), + ], + ); + } +} + +class _SheetTitle extends StatelessWidget { + const _SheetTitle(this.text, this.cs); + final String text; + final ShadColorScheme cs; + @override + Widget build(BuildContext context) => Text( + text, + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ); +} + +class _SheetSub extends StatelessWidget { + const _SheetSub(this.text, this.cs); + final String text; + final ShadColorScheme cs; + @override + Widget build(BuildContext context) => Text( + text, + style: TextStyle(fontSize: 13, height: 1.55, color: cs.mutedForeground), + ); +} + +class _ErrorText extends StatelessWidget { + const _ErrorText(this.text, this.cs); + final String text; + final ShadColorScheme cs; + @override + Widget build(BuildContext context) => + Text(text, style: TextStyle(fontSize: 13, color: cs.destructive)); +} + +class _BoxedField extends StatelessWidget { + const _BoxedField({ + required this.controller, + required this.hint, + required this.cs, + }); + final TextEditingController controller; + final String hint; + final ShadColorScheme cs; + + @override + Widget build(BuildContext context) { + return Container( + height: 50, + alignment: Alignment.center, + padding: const EdgeInsets.symmetric(horizontal: 14), + decoration: BoxDecoration( + color: cs.background, + borderRadius: BorderRadius.circular(Spacing.radiusBtn), + border: Border.all(color: cs.input, width: 1), + ), + child: TextField( + controller: controller, + keyboardType: TextInputType.phone, + inputFormatters: [_PhoneNumberFormatter()], + style: TextStyle( + fontSize: 16, + color: cs.foreground, + fontFeatures: const [FontFeature.tabularFigures()], + ), + cursorColor: cs.primary, + decoration: InputDecoration( + isCollapsed: true, + border: InputBorder.none, + hintText: hint, + hintStyle: TextStyle(fontSize: 15, color: cs.mutedForeground), + ), + ), + ); + } +} + +class _PhoneNumberFormatter extends TextInputFormatter { + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + var d = newValue.text.replaceAll(RegExp(r'\D'), ''); + if (d.length > 11) d = d.substring(0, 11); + final b = StringBuffer(); + for (var i = 0; i < d.length; i++) { + if (i == 3 || i == 7) b.write(' '); + b.write(d[i]); + } + final s = b.toString(); + return TextEditingValue( + text: s, + selection: TextSelection.collapsed(offset: s.length), + ); + } +} + +class _OtpInput extends StatelessWidget { + const _OtpInput({ + required this.controller, + required this.focusNode, + required this.cs, + }); + final TextEditingController controller; + final FocusNode focusNode; + final ShadColorScheme cs; + + @override + Widget build(BuildContext context) { + final code = controller.text; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: focusNode.requestFocus, + child: SizedBox( + height: 54, + child: Stack( + children: [ + Row( + children: [ + for (var i = 0; i < 6; i++) ...[ + if (i > 0) const SizedBox(width: 8), + Expanded( + child: _OtpCell( + digit: i < code.length ? code[i] : '', + filled: i < code.length, + cs: cs, + ), + ), + ], + ], + ), + Positioned.fill( + child: TextField( + controller: controller, + focusNode: focusNode, + keyboardType: TextInputType.number, + maxLength: 6, + showCursor: false, + autofocus: true, + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + style: const TextStyle(color: Colors.transparent), + decoration: const InputDecoration( + counterText: '', + border: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _OtpCell extends StatelessWidget { + const _OtpCell({required this.digit, required this.filled, required this.cs}); + final String digit; + final bool filled; + final ShadColorScheme cs; + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: cs.background, + borderRadius: BorderRadius.circular(Spacing.radiusBtn), + border: Border.all(color: filled ? cs.foreground : cs.input, width: 1), + ), + child: Text( + digit, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w600, + color: cs.foreground, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ), + ); + } +} + +class _AgreeRow extends StatelessWidget { + const _AgreeRow({ + required this.agreed, + required this.onToggle, + required this.cs, + }); + final bool agreed; + final VoidCallback onToggle; + final ShadColorScheme cs; + + @override + Widget build(BuildContext context) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onToggle, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 18, + height: 18, + margin: const EdgeInsets.only(top: 1), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(Spacing.radiusBadge), + border: Border.all( + color: agreed ? cs.primary : cs.border, + width: 1.2, + ), + color: agreed ? cs.primary : Colors.transparent, + ), + child: agreed + ? Icon( + FlutterRemix.check_line, + size: 13, + color: cs.primaryForeground, + ) + : null, + ), + const SizedBox(width: 8), + Expanded(child: Text.rich(_policySpan(context, cs))), + ], + ), + ); + } +} + +TextSpan _policySpan( + BuildContext context, + ShadColorScheme cs, { + double fontSize = 11.5, +}) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final privacyUrl = AppH5UrlsHelper.buildUrlWithNight( + AppH5Urls.privacyUrl, + isDark, + ); + final protocolUrl = AppH5UrlsHelper.buildUrlWithNight( + AppH5Urls.userProtocolUrl, + isDark, + ); + + TextSpan link(String text, String url) => TextSpan( + text: text, + style: TextStyle( + color: cs.foreground, + fontSize: fontSize, + decoration: TextDecoration.underline, + decorationColor: cs.foreground, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => launchUrlString( + AppH5UrlsHelper.withCacheBuster(url), + mode: LaunchMode.inAppBrowserView, + ), + ); + + return TextSpan( + style: TextStyle( + fontSize: fontSize, + height: 1.6, + color: cs.mutedForeground, + ), + children: [ + const TextSpan(text: '已阅读并同意 '), + link('《用户协议》', protocolUrl), + const TextSpan(text: ' 与 '), + link('《隐私政策》', privacyUrl), + ], + ); +} + +class _PrimaryButton extends StatelessWidget { + const _PrimaryButton({ + required this.label, + required this.enabled, + required this.onTap, + this.loading = false, + this.dim = false, + }); + final String label; + final bool enabled; + final bool loading; + final bool dim; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final active = enabled && !dim; + return SizedBox( + width: double.infinity, + child: GestureDetector( + onTap: enabled ? onTap : null, + behavior: HitTestBehavior.opaque, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 15), + alignment: Alignment.center, + decoration: BoxDecoration( + color: active ? cs.primary : cs.muted, + borderRadius: BorderRadius.circular(9999), + ), + child: loading + ? SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: active ? cs.primaryForeground : cs.mutedForeground, + ), + ) + : Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: active ? cs.primaryForeground : cs.mutedForeground, + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/debug/presentation/debug_info_page.dart b/lib/features/debug/presentation/debug_info_page.dart new file mode 100644 index 0000000..0082f13 --- /dev/null +++ b/lib/features/debug/presentation/debug_info_page.dart @@ -0,0 +1,176 @@ +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +import '../../../common/config/shorebird_service.dart'; +import '../../../common/network/auth_interceptor.dart'; +import '../../../common/network/dio_provider.dart'; +import '../../../common/router/app_router.dart'; +import '../../../common/services/device_header_service.dart'; +import '../../../common/storage/storage_keys.dart'; +import '../../../common/storage/storage_service.dart'; +import '../../../common/theme/theme_controller.dart'; + +class DebugInfoPage extends ConsumerWidget { + const DebugInfoPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appConfig = ref.watch(appConfigProvider); + final dio = ref.watch(dioProvider); + final themeMode = ref.watch(themeModeControllerProvider); + + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(AppRoutes.debug); + } + }, + icon: const Icon(Icons.arrow_back_ios_new), + ), + title: const Text('Info'), + ), + body: FutureBuilder<_DebugInfoSnapshot>( + future: _loadSnapshot( + apiBaseUrl: appConfig.apiBaseUrl, + themeMode: themeMode.name, + dio: dio, + ), + builder: (context, snapshot) { + final text = snapshot.data?.text ?? 'loading...'; + return Padding( + padding: const EdgeInsets.all(16), + child: SelectableText(text, style: const TextStyle(fontSize: 12)), + ); + }, + ), + ); + } +} + +Future<_DebugInfoSnapshot> _loadSnapshot({ + required String apiBaseUrl, + required String themeMode, + required Dio dio, +}) async { + await DeviceHeaderService.to.ensureInitialized(); + final packageInfo = await PackageInfo.fromPlatform(); + final deviceInfo = await _readDeviceInfo(); + final storage = StorageService.to; + final jwt = await loadJwt(); + final commonHeaders = DeviceHeaderService.to.commonHeaders(); + final platformHeaders = DeviceHeaderService.to.platformIdentityHeaders(); + final headers = { + ...commonHeaders, + ...platformHeaders, + ...dio.options.headers, + }; + final gitVersion = const String.fromEnvironment( + 'GITVERSION', + defaultValue: 'unknown', + ); + final patch = ShorebirdService.instance.currentPatch; + final jwtLine = jwt?.isNotEmpty == true ? 'Bearer ' : ''; + + final text = StringBuffer() + ..writeln('[build]') + ..writeln('app_name: ${packageInfo.appName}') + ..writeln('mode: ${kReleaseMode ? 'release' : 'debug'}') + ..writeln('package_name: ${packageInfo.packageName}') + ..writeln('version: ${packageInfo.version}') + ..writeln('build_number: ${packageInfo.buildNumber}') + ..writeln('theme_mode: $themeMode') + ..writeln('platform: ${defaultTargetPlatform.name}') + ..writeln('is_web: $kIsWeb') + ..writeln() + ..writeln('[environment]') + ..writeln('api_base: $apiBaseUrl') + ..writeln( + 'connect_timeout_ms: ${dio.options.connectTimeout?.inMilliseconds}', + ) + ..writeln( + 'receive_timeout_ms: ${dio.options.receiveTimeout?.inMilliseconds}', + ) + ..writeln('response_type: ${dio.options.responseType.name}') + ..writeln('git_version: $gitVersion') + ..writeln() + ..writeln('[storage]') + ..writeln('privacy_agreed: ${storage.getBool(storagePrivacyAgreed)}') + ..writeln('ios_startup_seen: ${storage.getBool(storageIosStartupSeen)}') + ..writeln('device_id: ${DeviceHeaderService.to.deviceId}') + ..writeln('android_id: ${storage.getString(storageAndroidId) ?? ''}') + ..writeln('device_token: ${storage.getString(storageDeviceToken) ?? ''}') + ..writeln('android_oaid: ${storage.getString(storageAndroidOaid) ?? ''}') + ..writeln('android_imei: ${storage.getString(storageAndroidImei) ?? ''}') + ..writeln('android_mac: ${storage.getString(storageAndroidMac) ?? ''}') + ..writeln('ios_idfa: ${storage.getString(storageIosIdfa) ?? ''}') + ..writeln('ios_paid: ${storage.getString(storageIosPaid) ?? ''}') + ..writeln() + ..writeln('[auth]') + ..writeln('jwt: $jwtLine') + ..writeln() + ..writeln('[headers]') + ..writeln('common: $commonHeaders') + ..writeln('platform: $platformHeaders') + ..writeln('dio: $headers') + ..writeln() + ..writeln('[shorebird]') + ..writeln('current_patch: ${patch?.number ?? 'none'}') + ..writeln('patch: ${patch ?? 'none'}') + ..writeln() + ..writeln('[device]') + ..writeln(deviceInfo); + + return _DebugInfoSnapshot(text.toString()); +} + +Future _readDeviceInfo() async { + try { + if (!kIsWeb && + defaultTargetPlatform == TargetPlatform.android && + !StorageService.to.getBool(storagePrivacyAgreed)) { + return 'privacy policy not accepted; device info not read'; + } + final plugin = DeviceInfoPlugin(); + if (kIsWeb) { + final info = await plugin.webBrowserInfo; + return info.data.toString(); + } + + switch (defaultTargetPlatform) { + case TargetPlatform.android: + final info = await plugin.androidInfo; + return info.data.toString(); + case TargetPlatform.iOS: + final info = await plugin.iosInfo; + return info.data.toString(); + case TargetPlatform.macOS: + final info = await plugin.macOsInfo; + return info.data.toString(); + case TargetPlatform.windows: + final info = await plugin.windowsInfo; + return info.data.toString(); + case TargetPlatform.linux: + final info = await plugin.linuxInfo; + return info.data.toString(); + case TargetPlatform.fuchsia: + return 'fuchsia'; + } + } catch (error) { + return 'unavailable: $error'; + } +} + +class _DebugInfoSnapshot { + const _DebugInfoSnapshot(this.text); + + final String text; +} diff --git a/lib/features/debug/presentation/debug_page.dart b/lib/features/debug/presentation/debug_page.dart new file mode 100644 index 0000000..a1d9b7c --- /dev/null +++ b/lib/features/debug/presentation/debug_page.dart @@ -0,0 +1,201 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../../common/router/app_router.dart'; +import '../../../common/theme/app_text.dart'; +import '../../../common/theme/spacing.dart'; + +class DebugPage extends StatelessWidget { + const DebugPage({super.key}); + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Scaffold( + appBar: AppBar( + backgroundColor: cs.background, + surfaceTintColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + title: Text( + '调试工具', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + leading: IconButton( + icon: Icon(Icons.arrow_back_ios_new, color: cs.foreground, size: 18), + onPressed: () => context.pop(), + ), + ), + body: ListView( + padding: const EdgeInsets.fromLTRB(Spacing.page, 8, Spacing.page, 24), + children: [ + _Section( + title: '状态', + child: _Card( + child: Text( + kReleaseMode ? 'release' : 'debug', + style: AppText.body.copyWith(color: cs.mutedForeground), + ), + ), + ), + _Section( + title: '入口', + child: _Card( + children: [ + _Row( + icon: Icons.data_object_outlined, + title: '请求', + subtitle: '查看 Dio 请求和响应', + onTap: () => context.push(AppRoutes.fancyDioInspector), + ), + _Row( + icon: Icons.info_outline, + title: '应用信息', + subtitle: '版本、包名和构建信息', + onTap: () => context.push(AppRoutes.debugInfo), + ), + _Row( + icon: Icons.text_fields, + title: 'Typography', + subtitle: '查看字号和文本样式基线', + onTap: () => context.push(AppRoutes.debugTypography), + ), + _Row( + icon: Icons.system_update_alt_outlined, + title: 'Shorebird', + subtitle: '查看补丁与更新状态', + onTap: () => context.push(AppRoutes.debugShorebird), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _Section extends StatelessWidget { + const _Section({required this.title, required this.child}); + + final String title; + final Widget child; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.only(bottom: Spacing.sectionGap), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 10), + child: Text( + title, + style: AppText.meta.copyWith( + fontSize: 13, + fontWeight: FontWeight.w600, + color: cs.mutedForeground, + ), + ), + ), + child, + ], + ), + ); + } +} + +class _Card extends StatelessWidget { + const _Card({this.child, this.children = const []}); + + final Widget? child; + final List children; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Container( + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(Spacing.radiusBase), + ), + clipBehavior: Clip.antiAlias, + child: + child ?? + Column( + children: [ + for (var i = 0; i < children.length; i++) ...[ + if (i > 0) Divider(height: 1, thickness: 1, color: cs.border), + children[i], + ], + ], + ), + ); + } +} + +class _Row extends StatelessWidget { + const _Row({ + required this.icon, + required this.title, + required this.subtitle, + required this.onTap, + }); + + final IconData icon; + final String title; + final String subtitle; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), + child: Row( + children: [ + Icon(icon, size: 20, color: cs.foreground), + const SizedBox(width: 13), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppText.body.copyWith( + color: cs.foreground, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + subtitle, + style: AppText.meta.copyWith( + color: cs.mutedForeground, + fontSize: 12.5, + ), + ), + ], + ), + ), + Icon(Icons.chevron_right, size: 18, color: cs.mutedForeground), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/debug/presentation/debug_shorebird_page.dart b/lib/features/debug/presentation/debug_shorebird_page.dart new file mode 100644 index 0000000..afec39a --- /dev/null +++ b/lib/features/debug/presentation/debug_shorebird_page.dart @@ -0,0 +1,420 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shorebird_code_push/shorebird_code_push.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../../common/config/shorebird_service.dart'; +import '../../../common/router/app_router.dart'; +import '../../../common/theme/app_text.dart'; +import '../../../common/theme/spacing.dart'; + +class DebugShorebirdPage extends StatefulWidget { + const DebugShorebirdPage({super.key}); + + @override + State createState() => _DebugShorebirdPageState(); +} + +class _DebugShorebirdPageState extends State { + final ShorebirdService _shorebird = ShorebirdService.instance; + late final bool _isUpdaterAvailable; + var _currentTrack = UpdateTrack.stable; + var _isCheckingForUpdates = false; + Patch? _currentPatch; + + @override + void initState() { + super.initState(); + _isUpdaterAvailable = _shorebird.isAvailable; + _loadCurrentPatch(); + } + + Future _loadCurrentPatch() async { + try { + await _shorebird.initCurrentPatch(); + if (!mounted) return; + setState(() => _currentPatch = _shorebird.currentPatch); + } catch (error) { + debugPrint('Error reading current patch: $error'); + } + } + + Future _checkForUpdate() async { + if (_isCheckingForUpdates) return; + + try { + setState(() => _isCheckingForUpdates = true); + final status = await _shorebird.checkForUpdate(track: _currentTrack); + if (!mounted) return; + switch (status) { + case UpdateStatus.upToDate: + _showNoUpdateAvailableBanner(); + break; + case UpdateStatus.outdated: + _showUpdateAvailableBanner(); + break; + case UpdateStatus.restartRequired: + _showRestartBanner(); + break; + case UpdateStatus.unavailable: + _showUnavailableBanner(); + break; + } + } catch (error) { + debugPrint('Error checking for update: $error'); + _showErrorBanner(error); + } finally { + if (mounted) { + setState(() => _isCheckingForUpdates = false); + } + } + } + + void _showDownloadingBanner() { + ScaffoldMessenger.of(context) + ..hideCurrentMaterialBanner() + ..showMaterialBanner( + const MaterialBanner( + content: Text('正在下载更新...'), + actions: [ + SizedBox(height: 14, width: 14, child: CircularProgressIndicator()), + ], + ), + ); + } + + void _showUpdateAvailableBanner() { + ScaffoldMessenger.of(context) + ..hideCurrentMaterialBanner() + ..showMaterialBanner( + MaterialBanner( + content: Text('当前 ${_currentTrack.name} track 有可用更新。'), + actions: [ + TextButton( + onPressed: () async { + ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); + await _downloadUpdate(); + if (!mounted) return; + ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); + }, + child: const Text('Download'), + ), + ], + ), + ); + } + + void _showNoUpdateAvailableBanner() { + ScaffoldMessenger.of(context) + ..hideCurrentMaterialBanner() + ..showMaterialBanner( + MaterialBanner( + content: Text('当前 ${_currentTrack.name} track 没有可用更新。'), + actions: [ + TextButton( + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); + }, + child: const Text('Dismiss'), + ), + ], + ), + ); + } + + void _showRestartBanner() { + ScaffoldMessenger.of(context) + ..hideCurrentMaterialBanner() + ..showMaterialBanner( + MaterialBanner( + content: const Text('新的 patch 已准备好,请重启应用。'), + actions: [ + TextButton( + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); + }, + child: const Text('Dismiss'), + ), + ], + ), + ); + } + + void _showUnavailableBanner() { + ScaffoldMessenger.of(context) + ..hideCurrentMaterialBanner() + ..showMaterialBanner( + MaterialBanner( + content: const Text('当前构建不可用 Shorebird。'), + actions: [ + TextButton( + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); + }, + child: const Text('Dismiss'), + ), + ], + ), + ); + } + + void _showErrorBanner(Object error) { + ScaffoldMessenger.of(context) + ..hideCurrentMaterialBanner() + ..showMaterialBanner( + MaterialBanner( + content: Text('检查更新时发生错误:$error'), + actions: [ + TextButton( + onPressed: () { + ScaffoldMessenger.of(context).hideCurrentMaterialBanner(); + }, + child: const Text('Dismiss'), + ), + ], + ), + ); + } + + Future _downloadUpdate() async { + _showDownloadingBanner(); + try { + await _shorebird.downloadUpdate(track: _currentTrack); + if (!mounted) return; + setState(() => _currentPatch = _shorebird.currentPatch); + _showRestartBanner(); + } on UpdateException catch (error) { + _showErrorBanner(error.message); + } catch (error) { + _showErrorBanner(error); + } + } + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Scaffold( + appBar: AppBar( + backgroundColor: cs.background, + surfaceTintColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + title: Text( + 'Shorebird', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + leading: IconButton( + icon: Icon(Icons.arrow_back_ios_new, color: cs.foreground, size: 18), + onPressed: () { + if (context.canPop()) { + context.pop(); + } else { + context.go(AppRoutes.debug); + } + }, + ), + ), + body: ListView( + padding: const EdgeInsets.fromLTRB(Spacing.page, 8, Spacing.page, 24), + children: [ + if (!_isUpdaterAvailable) ...[ + _Card( + child: Text( + '当前构建未接入 Shorebird。请确认应用是通过 `shorebird release` 生成的 release 包,然后再检查更新与 patch 状态。', + style: AppText.body.copyWith(color: cs.mutedForeground), + ), + ), + const SizedBox(height: Spacing.cardGap), + ], + _Section( + title: '当前状态', + child: _Card( + child: Column( + children: [ + _InfoTile( + title: '当前 patch 版本', + value: _currentPatch != null + ? '${_currentPatch!.number}' + : '未安装 patch', + ), + Divider(height: 1, thickness: 1, color: cs.border), + _InfoTile( + title: '更新器可用', + value: _isUpdaterAvailable ? '是' : '否', + ), + ], + ), + ), + ), + _Section( + title: 'Track 选择', + child: _Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '选择要检查和下载的 track。', + style: AppText.body.copyWith(color: cs.mutedForeground), + ), + const SizedBox(height: 12), + SegmentedButton( + segments: const [ + ButtonSegment( + label: Text('Stable'), + value: UpdateTrack.stable, + ), + ButtonSegment( + label: Text('Beta'), + value: UpdateTrack.beta, + ), + ButtonSegment( + label: Text('Staging'), + value: UpdateTrack.staging, + ), + ], + selected: {_currentTrack}, + onSelectionChanged: (tracks) { + setState(() => _currentTrack = tracks.single); + }, + ), + ], + ), + ), + ), + ), + _Section( + title: '操作', + child: _Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: FilledButton( + onPressed: _isCheckingForUpdates + ? null + : _checkForUpdate, + child: _isCheckingForUpdates + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Text('检查更新'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: OutlinedButton( + onPressed: _downloadUpdate, + child: const Text('下载更新'), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +class _Section extends StatelessWidget { + const _Section({required this.title, required this.child}); + + final String title; + final Widget child; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.only(bottom: Spacing.sectionGap), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 10), + child: Text( + title, + style: AppText.meta.copyWith( + fontSize: 13, + fontWeight: FontWeight.w600, + color: cs.mutedForeground, + ), + ), + ), + child, + ], + ), + ); + } +} + +class _Card extends StatelessWidget { + const _Card({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Container( + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(Spacing.radiusBase), + ), + clipBehavior: Clip.antiAlias, + child: child, + ); + } +} + +class _InfoTile extends StatelessWidget { + const _InfoTile({required this.title, required this.value}); + + final String title; + final String value; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: AppText.meta.copyWith(color: cs.mutedForeground), + ), + const SizedBox(height: 4), + Text( + value, + style: AppText.body.copyWith( + color: cs.foreground, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/debug/presentation/fancy_dio_inspector_page.dart b/lib/features/debug/presentation/fancy_dio_inspector_page.dart new file mode 100644 index 0000000..8d1497e --- /dev/null +++ b/lib/features/debug/presentation/fancy_dio_inspector_page.dart @@ -0,0 +1,24 @@ +import 'package:fancy_dio_inspector/fancy_dio_inspector.dart'; +import 'package:flutter/material.dart'; + +class FancyDioInspectorPage extends StatelessWidget { + const FancyDioInspectorPage({super.key}); + + @override + Widget build(BuildContext context) { + return ScaffoldMessenger( + child: Scaffold( + appBar: AppBar( + title: const Text('请求'), + leading: Navigator.of(context).canPop() + ? IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.chevron_left), + ) + : null, + ), + body: const FancyDioInspectorView(), + ), + ); + } +} diff --git a/lib/features/debug/presentation/typography_test_page.dart b/lib/features/debug/presentation/typography_test_page.dart new file mode 100644 index 0000000..dd51869 --- /dev/null +++ b/lib/features/debug/presentation/typography_test_page.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../../common/theme/app_text.dart'; +import '../../../common/theme/spacing.dart'; + +class TypographyTestPage extends StatelessWidget { + const TypographyTestPage({super.key}); + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + const samples = <_Sample>[ + _Sample('appTitle', AppText.appTitle), + _Sample('sectionTitle', AppText.sectionTitle), + _Sample('cardTitle', AppText.cardTitle), + _Sample('listTitle', AppText.listTitle), + _Sample('body', AppText.body), + _Sample('meta', AppText.meta), + _Sample('chip', AppText.chip), + _Sample('badge', AppText.badge), + ]; + + return Scaffold( + appBar: AppBar( + backgroundColor: cs.background, + surfaceTintColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + title: Text( + 'Typography', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + leading: IconButton( + icon: Icon(Icons.arrow_back_ios_new, color: cs.foreground, size: 18), + onPressed: () => context.pop(), + ), + ), + body: ListView( + padding: const EdgeInsets.fromLTRB(Spacing.page, 8, Spacing.page, 24), + children: [ + for (final sample in samples) ...[ + Container( + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(Spacing.radiusBase), + ), + clipBehavior: Clip.antiAlias, + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.only(bottom: Spacing.cardGap), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + sample.name, + style: AppText.meta.copyWith(color: cs.mutedForeground), + ), + const SizedBox(height: 8), + Text('金值 / Jinzhi / Typography', style: sample.style), + ], + ), + ), + ], + ], + ), + ); + } +} + +class _Sample { + const _Sample(this.name, this.style); + + final String name; + final TextStyle style; +} diff --git a/lib/features/market/application/exchange_calculator_controller.dart b/lib/features/market/application/exchange_calculator_controller.dart new file mode 100644 index 0000000..8b6f5c3 --- /dev/null +++ b/lib/features/market/application/exchange_calculator_controller.dart @@ -0,0 +1,123 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../common/domain/metal_type.dart'; +import '../data/market_models.dart'; +import '../data/mock_market_repository.dart'; +import 'market_controller.dart'; + +final exchangeCalculatorControllerProvider = + StateNotifierProvider< + ExchangeCalculatorController, + ExchangeCalculatorState + >((ref) { + final market = ref.watch(marketControllerProvider); + final repository = ref.watch(mockMarketRepositoryProvider); + return ExchangeCalculatorController( + repository: repository, + initialMetal: market.selectedMetal, + ); + }); + +class ExchangeCalculatorState { + const ExchangeCalculatorState({ + required this.metal, + required this.channels, + required this.selectedChannel, + required this.gram, + required this.amount, + required this.swapped, + }); + + final MetalType metal; + final List channels; + final ExchangeChannel selectedChannel; + final double gram; + final double amount; + final bool swapped; + + ExchangeCalculatorState copyWith({ + MetalType? metal, + List? channels, + ExchangeChannel? selectedChannel, + double? gram, + double? amount, + bool? swapped, + }) { + return ExchangeCalculatorState( + metal: metal ?? this.metal, + channels: channels ?? this.channels, + selectedChannel: selectedChannel ?? this.selectedChannel, + gram: gram ?? this.gram, + amount: amount ?? this.amount, + swapped: swapped ?? this.swapped, + ); + } +} + +class ExchangeCalculatorController + extends StateNotifier { + ExchangeCalculatorController({ + required MockMarketRepository repository, + required MetalType initialMetal, + }) : _repository = repository, + super(_initialState(repository, initialMetal)); + + final MockMarketRepository _repository; + + void selectMetal(MetalType metal) { + final channels = _repository.channelsFor(metal); + final selected = channels.first; + state = state.copyWith( + metal: metal, + channels: channels, + selectedChannel: selected, + amount: state.gram * selected.pricePerGram, + ); + } + + void selectChannel(String channelId) { + final selected = state.channels.firstWhere( + (channel) => channel.id == channelId, + orElse: () => state.channels.first, + ); + state = state.copyWith( + selectedChannel: selected, + amount: state.gram * selected.pricePerGram, + ); + } + + void setGram(double gram) { + state = state.copyWith( + gram: gram, + amount: gram * state.selectedChannel.pricePerGram, + ); + } + + void setAmount(double amount) { + state = state.copyWith( + amount: amount, + gram: amount / state.selectedChannel.pricePerGram, + ); + } + + void toggleSwapped() { + state = state.copyWith(swapped: !state.swapped); + } + + static ExchangeCalculatorState _initialState( + MockMarketRepository repository, + MetalType metal, + ) { + final channels = repository.channelsFor(metal); + final selected = channels.first; + const gram = 50.0; + return ExchangeCalculatorState( + metal: metal, + channels: channels, + selectedChannel: selected, + gram: gram, + amount: gram * selected.pricePerGram, + swapped: false, + ); + } +} diff --git a/lib/features/market/application/market_controller.dart b/lib/features/market/application/market_controller.dart new file mode 100644 index 0000000..157f488 --- /dev/null +++ b/lib/features/market/application/market_controller.dart @@ -0,0 +1,73 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../common/domain/metal_type.dart'; +import '../data/market_models.dart'; +import '../data/mock_market_repository.dart'; + +final marketControllerProvider = + StateNotifierProvider((ref) { + final repository = ref.watch(mockMarketRepositoryProvider); + return MarketController(repository); + }); + +class MarketState { + const MarketState({ + required this.selectedMetal, + required this.period, + required this.quote, + required this.points, + required this.quotes, + }); + + final MetalType selectedMetal; + final MarketPeriod period; + final MetalQuote quote; + final List points; + final List quotes; + + MarketState copyWith({ + MetalType? selectedMetal, + MarketPeriod? period, + MetalQuote? quote, + List? points, + List? quotes, + }) { + return MarketState( + selectedMetal: selectedMetal ?? this.selectedMetal, + period: period ?? this.period, + quote: quote ?? this.quote, + points: points ?? this.points, + quotes: quotes ?? this.quotes, + ); + } +} + +class MarketController extends StateNotifier { + MarketController(this._repository) + : super( + MarketState( + selectedMetal: MetalType.gold, + period: MarketPeriod.h24, + quote: _repository.quoteFor(MetalType.gold), + points: _repository.chartFor(MetalType.gold, MarketPeriod.h24), + quotes: _repository.getQuotes(), + ), + ); + + final MockMarketRepository _repository; + + void selectMetal(MetalType metal) { + state = state.copyWith( + selectedMetal: metal, + quote: _repository.quoteFor(metal), + points: _repository.chartFor(metal, state.period), + ); + } + + void selectPeriod(MarketPeriod period) { + state = state.copyWith( + period: period, + points: _repository.chartFor(state.selectedMetal, period), + ); + } +} diff --git a/lib/features/market/data/market_models.dart b/lib/features/market/data/market_models.dart new file mode 100644 index 0000000..ae43196 --- /dev/null +++ b/lib/features/market/data/market_models.dart @@ -0,0 +1,74 @@ +import '../../../common/domain/metal_type.dart'; + +enum MarketPeriod { + h24('24h', '24时'), + d5('5d', '5日'), + m1('1m', '1月'), + m3('3m', '3月'), + y1('1y', '1年'); + + const MarketPeriod(this.id, this.label); + + final String id; + final String label; +} + +enum ExchangeChannelKind { spot, bar, retail, td, recycle } + +class MetalQuote { + const MetalQuote({ + required this.metal, + required this.spotPrice, + required this.changeAmount, + required this.changePercent, + required this.updatedAt, + required this.sourceLabel, + required this.basisLabel, + }); + + final MetalType metal; + final double spotPrice; + final double changeAmount; + final double changePercent; + final DateTime updatedAt; + final String sourceLabel; + final String basisLabel; +} + +class MarketPoint { + const MarketPoint({ + required this.time, + required this.open, + required this.high, + required this.low, + required this.close, + }); + + final DateTime time; + final double open; + final double high; + final double low; + final double close; +} + +class ExchangeChannel { + const ExchangeChannel({ + required this.id, + required this.metal, + required this.name, + required this.kind, + required this.pricePerGram, + required this.source, + required this.updatedAt, + required this.hint, + }); + + final String id; + final MetalType metal; + final String name; + final ExchangeChannelKind kind; + final double pricePerGram; + final String source; + final DateTime updatedAt; + final String hint; +} diff --git a/lib/features/market/data/mock_market_repository.dart b/lib/features/market/data/mock_market_repository.dart new file mode 100644 index 0000000..be87ce1 --- /dev/null +++ b/lib/features/market/data/mock_market_repository.dart @@ -0,0 +1,216 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../common/domain/metal_type.dart'; +import 'market_models.dart'; + +final mockMarketRepositoryProvider = Provider( + (ref) => MockMarketRepository(), +); + +class MockMarketRepository { + MockMarketRepository(); + + final DateTime _updatedAt = DateTime(2026, 6, 18, 16, 11); + + List getQuotes() { + return [ + MetalQuote( + metal: MetalType.gold, + spotPrice: 943.05, + changeAmount: 8.12, + changePercent: 0.87, + updatedAt: _updatedAt, + sourceLabel: '行情聚合', + basisLabel: '上海金现货参考价', + ), + MetalQuote( + metal: MetalType.platinum, + spotPrice: 384.20, + changeAmount: -2.46, + changePercent: -0.64, + updatedAt: _updatedAt, + sourceLabel: '行情聚合', + basisLabel: '国际盘换算参考价', + ), + MetalQuote( + metal: MetalType.silver, + spotPrice: 15.75, + changeAmount: 0.18, + changePercent: 1.16, + updatedAt: _updatedAt, + sourceLabel: '行情聚合', + basisLabel: '上海银现货参考价', + ), + ]; + } + + MetalQuote quoteFor(MetalType metal) => + getQuotes().firstWhere((quote) => quote.metal == metal); + + List chartFor(MetalType metal, MarketPeriod period) { + final quote = quoteFor(metal); + final points = []; + final step = switch (period) { + MarketPeriod.h24 => const Duration(hours: 2), + MarketPeriod.d5 => const Duration(days: 1), + MarketPeriod.m1 => const Duration(days: 3), + MarketPeriod.m3 => const Duration(days: 9), + MarketPeriod.y1 => const Duration(days: 30), + }; + for (var i = 11; i >= 0; i--) { + final drift = (i - 5.5) * quote.changeAmount / 10; + final close = quote.spotPrice - drift; + points.add( + MarketPoint( + time: _updatedAt.subtract(step * i), + open: close - quote.changeAmount / 20, + high: close + quote.spotPrice * 0.003, + low: close - quote.spotPrice * 0.003, + close: close, + ), + ); + } + return points; + } + + List channelsFor(MetalType metal, {double recycle = 0.97}) { + final quote = quoteFor(metal); + return switch (metal) { + MetalType.gold => [ + _channel( + 'gold_spot', + metal, + '上海金现货', + ExchangeChannelKind.spot, + 943.05, + '上海金现货', + '材料价值口径 · 非金店零售 / 回收价', + ), + _channel( + 'gold_bar', + metal, + '投资金条', + ExchangeChannelKind.bar, + 955.00, + '银行 / 品牌金条参考', + '投资金条参考 · 通常含少量升水', + ), + _channel( + 'gold_ctf', + metal, + '周大福金店', + ExchangeChannelKind.retail, + 1238.00, + '品牌零售挂牌参考', + '含工费与品牌溢价 · 以门店为准', + ), + _channel( + 'gold_lfx', + metal, + '老凤祥金店', + ExchangeChannelKind.retail, + 1248.00, + '品牌零售挂牌参考', + '含工费与品牌溢价 · 以门店为准', + ), + _channel( + 'gold_td', + metal, + '黄金 T+D', + ExchangeChannelKind.td, + 919.50, + 'SGE 递延参考', + '交易品种参考 · 非实物购买价', + ), + _channel( + 'gold_recycle', + metal, + '回收参考', + ExchangeChannelKind.recycle, + quote.spotPrice * recycle, + '材料价值 × 回收折扣', + '实际以门店检测为准', + ), + ], + MetalType.platinum => [ + _channel( + 'platinum_spot', + metal, + '铂金现货', + ExchangeChannelKind.spot, + 384.20, + '国际盘换算参考', + '材料价值口径 · 非饰品零售价', + ), + _channel( + 'platinum_retail', + metal, + '铂金饰品', + ExchangeChannelKind.retail, + 498.00, + '零售挂牌参考', + '含工费与品牌溢价', + ), + _channel( + 'platinum_recycle', + metal, + '回收参考', + ExchangeChannelKind.recycle, + quote.spotPrice * recycle, + '材料价值 × 回收折扣', + '实际以门店检测为准', + ), + ], + MetalType.silver => [ + _channel( + 'silver_spot', + metal, + '白银现货', + ExchangeChannelKind.spot, + 15.75, + '上海银现货', + '材料价值口径 · 非饰品零售价', + ), + _channel( + 'silver_retail', + metal, + '白银饰品', + ExchangeChannelKind.retail, + 42.00, + '零售挂牌参考', + '含工费与品牌溢价', + ), + _channel( + 'silver_recycle', + metal, + '回收参考', + ExchangeChannelKind.recycle, + quote.spotPrice * recycle, + '材料价值 × 回收折扣', + '实际以门店检测为准', + ), + ], + }; + } + + ExchangeChannel _channel( + String id, + MetalType metal, + String name, + ExchangeChannelKind kind, + double price, + String source, + String hint, + ) { + return ExchangeChannel( + id: id, + metal: metal, + name: name, + kind: kind, + pricePerGram: price, + source: source, + updatedAt: _updatedAt, + hint: hint, + ); + } +} diff --git a/lib/features/market/presentation/market_page.dart b/lib/features/market/presentation/market_page.dart new file mode 100644 index 0000000..c0e8c3c --- /dev/null +++ b/lib/features/market/presentation/market_page.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../../common/domain/metal_type.dart'; +import '../../../common/domain/money.dart'; +import '../../../common/widgets/adaptive.dart'; +import '../application/exchange_calculator_controller.dart'; +import '../application/market_controller.dart'; +import '../data/market_models.dart'; + +class MarketPage extends ConsumerWidget { + const MarketPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final market = ref.watch(marketControllerProvider); + final exchange = ref.watch(exchangeCalculatorControllerProvider); + final cs = ShadTheme.of(context).colorScheme; + return SafeArea( + top: false, + child: ListView( + padding: const EdgeInsets.fromLTRB(18, 18, 18, 24), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: Adaptive.contentMaxWidth, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SegmentedButton( + segments: [ + for (final metal in MetalType.values) + ButtonSegment(value: metal, label: Text(metal.label)), + ], + selected: {market.selectedMetal}, + onSelectionChanged: (next) { + final metal = next.first; + ref + .read(marketControllerProvider.notifier) + .selectMetal(metal); + ref + .read(exchangeCalculatorControllerProvider.notifier) + .selectMetal(metal); + }, + ), + const SizedBox(height: 12), + _Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(market.quote.basisLabel, style: _mutedStyle(cs)), + const SizedBox(height: 8), + Text( + '${formatCny(market.quote.spotPrice)} / 克', + style: TextStyle( + fontSize: 30, + fontWeight: FontWeight.w800, + color: cs.foreground, + ), + ), + const SizedBox(height: 6), + Text( + '${market.quote.changeAmount >= 0 ? '+' : ''}${market.quote.changeAmount.toStringAsFixed(2)} · ${market.quote.changePercent.toStringAsFixed(2)}%', + style: TextStyle( + color: market.quote.changeAmount >= 0 + ? const Color(0xFFC0392B) + : const Color(0xFF2E8B6F), + ), + ), + const SizedBox(height: 14), + Wrap( + spacing: 8, + children: [ + for (final period in MarketPeriod.values) + ChoiceChip( + label: Text(period.label), + selected: period == market.period, + onSelected: (_) => ref + .read(marketControllerProvider.notifier) + .selectPeriod(period), + ), + ], + ), + const SizedBox(height: 14), + Text( + '走势图 mock 点位:${market.points.length} 个', + style: _mutedStyle(cs), + ), + ], + ), + ), + const SizedBox(height: 12), + _Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '兑换试算', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 10), + Text( + '${formatGram(exchange.gram)} ${exchange.metal.label} = ${formatCny(exchange.amount)}', + style: TextStyle(fontSize: 20, color: cs.foreground), + ), + const SizedBox(height: 8), + Text( + '1 克 ${exchange.metal.label} = ${formatCny(exchange.selectedChannel.pricePerGram)} · ${exchange.selectedChannel.source}', + style: _mutedStyle(cs), + ), + const SizedBox(height: 10), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final channel in exchange.channels) + ChoiceChip( + label: Text(channel.name), + selected: + channel.id == exchange.selectedChannel.id, + onSelected: (_) => ref + .read( + exchangeCalculatorControllerProvider + .notifier, + ) + .selectChannel(channel.id), + ), + ], + ), + const SizedBox(height: 8), + Text( + exchange.selectedChannel.hint, + style: _mutedStyle(cs), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _Card extends StatelessWidget { + const _Card({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return DecoratedBox( + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(12), + ), + child: Padding(padding: const EdgeInsets.all(16), child: child), + ); + } +} + +TextStyle _mutedStyle(ShadColorScheme cs) { + return TextStyle(fontSize: 13, color: cs.mutedForeground, letterSpacing: 0); +} diff --git a/lib/features/news/application/news_controller.dart b/lib/features/news/application/news_controller.dart new file mode 100644 index 0000000..3b655f5 --- /dev/null +++ b/lib/features/news/application/news_controller.dart @@ -0,0 +1,34 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../data/mock_news_repository.dart'; +import '../data/news_models.dart'; + +final newsControllerProvider = StateNotifierProvider( + (ref) => NewsController(ref.watch(mockNewsRepositoryProvider)), +); + +class NewsState { + const NewsState({required this.items, required this.refreshedAt}); + + final List items; + final DateTime refreshedAt; +} + +class NewsController extends StateNotifier { + NewsController(this._repository) + : super( + NewsState( + items: _repository.loadFeed(), + refreshedAt: DateTime(2026, 6, 18, 16, 11), + ), + ); + + final MockNewsRepository _repository; + + void refresh() { + state = NewsState( + items: _repository.loadFeed(), + refreshedAt: DateTime.now(), + ); + } +} diff --git a/lib/features/news/data/mock_news_repository.dart b/lib/features/news/data/mock_news_repository.dart new file mode 100644 index 0000000..0ca2417 --- /dev/null +++ b/lib/features/news/data/mock_news_repository.dart @@ -0,0 +1,53 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../common/domain/metal_type.dart'; +import 'news_models.dart'; + +final mockNewsRepositoryProvider = Provider( + (ref) => MockNewsRepository(), +); + +class MockNewsRepository { + List loadFeed() { + final base = DateTime(2026, 6, 18, 16, 11); + final items = [ + NewsItem( + id: 'news_gold_001', + metal: MetalType.gold, + title: '金价维持高位震荡,实物金溢价继续分化', + source: '金值整理', + publishedAt: base.subtract(const Duration(minutes: 18)), + summary: '现货价与品牌零售价之间仍有明显价差,持仓估值应优先看材料价值。', + body: const [ + '今日黄金现货价格维持高位震荡,品牌金店挂牌价与现货材料价值之间仍有明显差距。', + '对持有者而言,材料价值更适合用于日常估值;对购买者而言,需要额外关注工费、品牌溢价与回收折扣。', + ], + ), + NewsItem( + id: 'news_silver_001', + metal: MetalType.silver, + title: '白银跟随工业金属情绪回暖,短线波动放大', + source: '市场简报', + publishedAt: base.subtract(const Duration(hours: 1, minutes: 4)), + summary: '银价弹性较强,饰品材料价值和零售购买价差异更大。', + body: const [ + '白银价格今日跟随工业金属情绪回暖,短线波动较黄金更明显。', + '银饰通常包含较高加工与零售成本,材料价值占比可能低于用户直觉。', + ], + ), + NewsItem( + id: 'news_platinum_001', + metal: MetalType.platinum, + title: '铂金回收报价偏谨慎,饰品估值需看纯度', + source: '金属观察', + publishedAt: base.subtract(const Duration(hours: 2, minutes: 36)), + summary: 'PT950 饰品估值建议按克重、纯度和回收折扣分层查看。', + body: const [ + '铂金饰品回收报价通常更依赖门店检测与成色确认,报价口径比现货价格更谨慎。', + '记录持仓时建议保留纯度、克重、购买渠道和成本信息,便于后续估值与盈亏回看。', + ], + ), + ]; + return [...items]..sort((a, b) => b.publishedAt.compareTo(a.publishedAt)); + } +} diff --git a/lib/features/news/data/news_models.dart b/lib/features/news/data/news_models.dart new file mode 100644 index 0000000..a27513d --- /dev/null +++ b/lib/features/news/data/news_models.dart @@ -0,0 +1,21 @@ +import '../../../common/domain/metal_type.dart'; + +class NewsItem { + const NewsItem({ + required this.id, + required this.metal, + required this.title, + required this.source, + required this.publishedAt, + required this.summary, + required this.body, + }); + + final String id; + final MetalType metal; + final String title; + final String source; + final DateTime publishedAt; + final String summary; + final List body; +} diff --git a/lib/features/news/presentation/news_page.dart b/lib/features/news/presentation/news_page.dart new file mode 100644 index 0000000..38383e8 --- /dev/null +++ b/lib/features/news/presentation/news_page.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../../common/widgets/adaptive.dart'; +import '../application/news_controller.dart'; + +class NewsPage extends ConsumerWidget { + const NewsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final news = ref.watch(newsControllerProvider); + final cs = ShadTheme.of(context).colorScheme; + return SafeArea( + top: false, + child: ListView( + padding: const EdgeInsets.fromLTRB(18, 18, 18, 24), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: Adaptive.contentMaxWidth, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: Text( + '信息整理 · 非投资建议', + style: TextStyle( + color: cs.mutedForeground, + fontSize: 13, + ), + ), + ), + TextButton( + onPressed: () => + ref.read(newsControllerProvider.notifier).refresh(), + child: const Text('刷新'), + ), + ], + ), + const SizedBox(height: 8), + for (final item in news.items) + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: DecoratedBox( + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(12), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + decoration: BoxDecoration( + color: item.metal.color.withValues( + alpha: 0.12, + ), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + item.metal.shortLabel, + style: TextStyle( + color: item.metal.color, + fontWeight: FontWeight.w700, + ), + ), + ), + const SizedBox(width: 8), + Text( + '${item.source} · ${item.publishedAt.hour.toString().padLeft(2, '0')}:${item.publishedAt.minute.toString().padLeft(2, '0')}', + style: TextStyle( + color: cs.mutedForeground, + fontSize: 12, + ), + ), + ], + ), + const SizedBox(height: 10), + Text( + item.title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 8), + Text( + item.summary, + style: TextStyle( + color: cs.mutedForeground, + height: 1.5, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/features/profile/application/profile_settings_controller.dart b/lib/features/profile/application/profile_settings_controller.dart new file mode 100644 index 0000000..52d2a67 --- /dev/null +++ b/lib/features/profile/application/profile_settings_controller.dart @@ -0,0 +1,43 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import '../../../common/domain/metal_type.dart'; +import '../data/profile_models.dart'; + +final profileSettingsControllerProvider = + StateNotifierProvider( + (ref) => ProfileSettingsController(), + ); + +class ProfileSettingsController extends StateNotifier { + ProfileSettingsController() + : super( + const ProfileSettings( + amountHidden: false, + recycleDiscounts: { + MetalType.gold: 0.97, + MetalType.platinum: 0.93, + MetalType.silver: 0.88, + }, + purityPresets: { + '足金9999': 0.9999, + '足金999': 0.999, + 'PT950': 0.95, + '999银': 0.999, + }, + priceColorMode: PriceColorMode.redUpGreenDown, + mockLoggedIn: false, + ), + ); + + void toggleAmountHidden() { + state = state.copyWith(amountHidden: !state.amountHidden); + } + + void setMockLoggedIn(bool value) { + state = state.copyWith(mockLoggedIn: value); + } + + void setPriceColorMode(PriceColorMode mode) { + state = state.copyWith(priceColorMode: mode); + } +} diff --git a/lib/features/profile/data/profile_models.dart b/lib/features/profile/data/profile_models.dart new file mode 100644 index 0000000..74317a9 --- /dev/null +++ b/lib/features/profile/data/profile_models.dart @@ -0,0 +1,42 @@ +import '../../../common/domain/metal_type.dart'; + +enum PriceColorMode { + redUpGreenDown('红涨绿跌'), + greenUpRedDown('绿涨红跌'); + + const PriceColorMode(this.label); + + final String label; +} + +class ProfileSettings { + const ProfileSettings({ + required this.amountHidden, + required this.recycleDiscounts, + required this.purityPresets, + required this.priceColorMode, + required this.mockLoggedIn, + }); + + final bool amountHidden; + final Map recycleDiscounts; + final Map purityPresets; + final PriceColorMode priceColorMode; + final bool mockLoggedIn; + + ProfileSettings copyWith({ + bool? amountHidden, + Map? recycleDiscounts, + Map? purityPresets, + PriceColorMode? priceColorMode, + bool? mockLoggedIn, + }) { + return ProfileSettings( + amountHidden: amountHidden ?? this.amountHidden, + recycleDiscounts: recycleDiscounts ?? this.recycleDiscounts, + purityPresets: purityPresets ?? this.purityPresets, + priceColorMode: priceColorMode ?? this.priceColorMode, + mockLoggedIn: mockLoggedIn ?? this.mockLoggedIn, + ); + } +} diff --git a/lib/features/profile/presentation/profile_page.dart b/lib/features/profile/presentation/profile_page.dart new file mode 100644 index 0000000..0cceb65 --- /dev/null +++ b/lib/features/profile/presentation/profile_page.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; + +import '../../../common/router/app_router.dart'; +import '../../../common/widgets/adaptive.dart'; +import '../../auth/application/auth_controller.dart'; +import '../../auth/data/auth_models.dart'; +import '../../profile/application/profile_settings_controller.dart'; + +class ProfilePage extends ConsumerWidget { + const ProfilePage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(profileSettingsControllerProvider); + final auth = ref.watch(authProvider); + final cs = ShadTheme.of(context).colorScheme; + final loggedIn = auth.valueOrNull is LoggedInAuthState; + final currentUser = auth.valueOrNull is LoggedInAuthState + ? (auth.valueOrNull as LoggedInAuthState).user + : null; + + return SafeArea( + top: false, + child: ListView( + padding: const EdgeInsets.fromLTRB(18, 18, 18, 24), + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: Adaptive.contentMaxWidth, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _Card( + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + currentUser?.nickname ?? '未登录', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 6), + Text( + currentUser?.phone ?? '登录后可使用本地同步、云备份占位和账户管理。', + style: TextStyle(color: cs.mutedForeground), + ), + ], + ), + ), + FilledButton( + onPressed: () => context.push(AppRoutes.settings), + child: const Text('设置'), + ), + ], + ), + ), + const SizedBox(height: 12), + _Card( + child: Column( + children: [ + _SettingRow( + label: '隐藏金额', + value: settings.amountHidden ? '已隐藏' : '显示中', + trailing: Switch( + value: settings.amountHidden, + onChanged: (_) => ref + .read( + profileSettingsControllerProvider.notifier, + ) + .toggleAmountHidden(), + ), + ), + _SettingRow( + label: '回收折扣', + value: + '金 ${(settings.recycleDiscounts.values.first * 100).toStringAsFixed(0)}%', + ), + _SettingRow( + label: '涨跌颜色', + value: settings.priceColorMode.label, + ), + _SettingRow( + label: '纯度系数', + value: '${settings.purityPresets.length} 项', + ), + ], + ), + ), + const SizedBox(height: 12), + _Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FilledButton.tonal( + onPressed: loggedIn + ? () => context.push(AppRoutes.login) + : () => context.push(AppRoutes.login), + child: Text(loggedIn ? '切换账号' : '去登录'), + ), + const SizedBox(height: 10), + OutlinedButton( + onPressed: () => context.push(AppRoutes.settings), + child: const Text('打开设置'), + ), + ], + ), + ), + const SizedBox(height: 12), + _Card( + child: Text( + '风险提示 · 数据来源 · 免责\n价格、资讯与估值均为本地 mock 数据,仅用于产品原型验证,不构成投资建议。', + style: TextStyle( + color: cs.mutedForeground, + height: 1.6, + fontSize: 13, + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _SettingRow extends StatelessWidget { + const _SettingRow({required this.label, required this.value, this.trailing}); + + final String label; + final String value; + final Widget? trailing; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Row( + children: [ + Expanded( + child: Text( + label, + style: TextStyle(fontSize: 15, color: cs.foreground), + ), + ), + Text(value, style: TextStyle(color: cs.mutedForeground)), + if (trailing != null) ...[const SizedBox(width: 8), trailing!], + ], + ), + ); + } +} + +class _Card extends StatelessWidget { + const _Card({required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return DecoratedBox( + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(12), + ), + child: Padding(padding: const EdgeInsets.all(16), child: child), + ); + } +} diff --git a/lib/features/settings/presentation/privacy_consent_page.dart b/lib/features/settings/presentation/privacy_consent_page.dart new file mode 100644 index 0000000..36e6571 --- /dev/null +++ b/lib/features/settings/presentation/privacy_consent_page.dart @@ -0,0 +1,476 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:video_player/video_player.dart'; + +import '../../../common/config/app_h5_urls.dart'; +import '../../../common/services/device_header_service.dart'; +import '../../../common/theme/jinzhi_theme.dart'; +import '../../../common/widgets/adaptive.dart'; +import '../../../common/router/app_router.dart'; + +class PrivacyConsentPage extends StatefulWidget { + const PrivacyConsentPage({super.key}); + + @override + State createState() => _PrivacyConsentPageState(); +} + +class _PrivacyConsentPageState extends State { + bool _dialogShown = false; + late final VideoPlayerController _video; + bool _videoReady = false; + + @override + void initState() { + super.initState(); + _video = VideoPlayerController.asset('assets/launch/launch-bg.mp4') + ..setLooping(true) + ..setVolume(0) + ..initialize().then((_) { + if (!mounted) return; + _video.setPlaybackSpeed(0.5); + _video.play(); + setState(() => _videoReady = true); + }); + } + + @override + void dispose() { + _video.dispose(); + super.dispose(); + } + + Future _agree() async { + await DeviceHeaderService.to.markPrivacyAgreed(); + await DeviceHeaderService.to.ensureInitialized(); + if (mounted) context.go(AppRoutes.assets); + } + + Future _enter() async { + await DeviceHeaderService.to.markIosStartupSeen(); + if (mounted) context.go(AppRoutes.assets); + } + + @override + Widget build(BuildContext context) { + const cs = jinzhiDarkScheme; + final showAndroidConsent = DeviceHeaderService.to.privacyConsentRequired; + + if (showAndroidConsent && !_dialogShown) { + _dialogShown = true; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + showModalBottomSheet( + context: context, + isScrollControlled: true, + isDismissible: false, + enableDrag: false, + backgroundColor: Colors.transparent, + barrierColor: Colors.black.withValues(alpha: 0.55), + builder: (_) => _PrivacyConsentSheet(onAgree: _agree), + ); + }); + } + + return AnnotatedRegion( + value: SystemUiOverlayStyle.light, + child: Scaffold( + backgroundColor: cs.background, + body: Stack( + fit: StackFit.expand, + children: [ + if (_videoReady) + FittedBox( + fit: BoxFit.cover, + clipBehavior: Clip.hardEdge, + child: SizedBox( + width: _video.value.size.width, + height: _video.value.size.height, + child: VideoPlayer(_video), + ), + ) + else + Image.asset('assets/launch/launch-poster.jpg', fit: BoxFit.cover), + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + stops: const [0.0, 0.46, 0.88], + colors: [ + cs.background.withValues(alpha: 0.66), + cs.background.withValues(alpha: 0.82), + cs.background, + ], + ), + ), + ), + SafeArea( + child: Column( + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const _LaunchMark(size: 88), + const SizedBox(height: 26), + Text( + '研听', + style: TextStyle( + fontSize: 42, + fontWeight: FontWeight.w800, + color: cs.foreground, + height: 1.1, + ), + ), + const SizedBox(height: 12), + Text( + '读懂全球研报', + style: TextStyle( + fontSize: 16, + color: cs.foreground.withValues(alpha: 0.82), + letterSpacing: 2, + ), + ), + ], + ), + ), + if (!showAndroidConsent) + Padding( + padding: const EdgeInsets.fromLTRB(22, 0, 22, 20), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: Adaptive.isTablet(context) + ? 400 + : double.infinity, + ), + child: GestureDetector( + onTap: _enter, + behavior: HitTestBehavior.opaque, + child: Container( + width: double.infinity, + height: 52, + alignment: Alignment.center, + decoration: BoxDecoration( + color: cs.primary, + borderRadius: BorderRadius.circular(9999), + ), + child: Text( + '立即进入', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: cs.primaryForeground, + ), + ), + ), + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(bottom: 14), + child: Text( + '内容为公开研报的结构化解读,不构成投资建议', + style: TextStyle( + fontSize: 11.5, + color: cs.mutedForeground.withValues(alpha: 0.75), + ), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class _LaunchMark extends StatefulWidget { + const _LaunchMark({this.size = 88}); + + final double size; + + @override + State<_LaunchMark> createState() => _LaunchMarkState(); +} + +class _LaunchMarkState extends State<_LaunchMark> + with SingleTickerProviderStateMixin { + late final AnimationController _spin = AnimationController( + vsync: this, + duration: const Duration(seconds: 8), + )..repeat(); + + @override + void dispose() { + _spin.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final m = widget.size; + const cxf = 0.6758, cyf = 0.3242, sideF = 0.4121; + final side = m * sideF; + return SizedBox( + width: m, + height: m, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned.fill( + child: Image.asset('assets/launch/mark-grid-lime.png'), + ), + Positioned( + left: cxf * m - side / 2, + top: cyf * m - side / 2, + width: side, + height: side, + child: RotationTransition( + turns: _spin, + child: Image.asset('assets/launch/mark-star-lime.png'), + ), + ), + ], + ), + ); + } +} + +class _PrivacyConsentSheet extends StatefulWidget { + const _PrivacyConsentSheet({required this.onAgree}); + + final Future Function() onAgree; + + @override + State<_PrivacyConsentSheet> createState() => _PrivacyConsentSheetState(); +} + +class _PrivacyConsentSheetState extends State<_PrivacyConsentSheet> { + bool _verify = false; + + @override + Widget build(BuildContext context) { + const cs = jinzhiDarkScheme; + final privacyUrl = AppH5UrlsHelper.buildUrlWithNight( + AppH5Urls.privacyUrl, + false, + ); + final protocolUrl = AppH5UrlsHelper.buildUrlWithNight( + AppH5Urls.userProtocolUrl, + false, + ); + return SafeArea( + top: false, + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: cs.card, + borderRadius: const BorderRadius.vertical(top: Radius.circular(11.2)), + border: Border.all(color: cs.border), + ), + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + child: _verify + ? _verifyView(cs, protocolUrl, privacyUrl) + : _consentView(cs, protocolUrl, privacyUrl), + ), + ); + } + + Widget _grip(ShadColorScheme cs) => Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(9999), + ), + ), + ); + + Widget _consentView( + ShadColorScheme cs, + String protocolUrl, + String privacyUrl, + ) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _grip(cs), + Text( + '欢迎使用研听', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 12), + _policyBody(cs, protocolUrl: protocolUrl, privacyUrl: privacyUrl), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: _Btn( + label: '不同意', + primary: false, + cs: cs, + onTap: () => setState(() => _verify = true), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _Btn( + label: '同意', + primary: true, + cs: cs, + onTap: () async { + Navigator.of(context).pop(); + await widget.onAgree(); + }, + ), + ), + ], + ), + ], + ); + } + + Widget _verifyView( + ShadColorScheme cs, + String protocolUrl, + String privacyUrl, + ) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _grip(cs), + Text( + '确认放弃使用?', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 12), + _policyBody(cs, protocolUrl: protocolUrl, privacyUrl: privacyUrl), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: _Btn( + label: '不同意并退出', + primary: false, + cs: cs, + onTap: () => SystemNavigator.pop(), + ), + ), + const SizedBox(width: 12), + Expanded( + child: _Btn( + label: '同意', + primary: true, + cs: cs, + onTap: () async { + Navigator.of(context).pop(); + await widget.onAgree(); + }, + ), + ), + ], + ), + ], + ); + } + + Widget _policyBody( + ShadColorScheme cs, { + required String protocolUrl, + required String privacyUrl, + }) { + return Text.rich( + TextSpan( + style: TextStyle(fontSize: 13, height: 1.7, color: cs.mutedForeground), + children: [ + const TextSpan( + text: + '尊敬的用户:\n\n我们非常重视您的个人信息和隐私保护,为了更好的保障您的个人权益,请您在使用我们的产品前,仔细阅读并充分理解 ', + ), + _link('《用户协议》', protocolUrl, cs), + const TextSpan(text: ' 和 '), + _link('《隐私政策》', privacyUrl, cs), + const TextSpan( + text: + '内容。我们将按照该协议内容收集、使用和共享您的个人信息。\n为了保证业务安全风控,在您使用我们基本功能的过程中,我们会收集您的手机号码,以及硬件序列号或唯一设备识别码(如 AndroidID/MAC/OAID/IMEI/WIFI 的 BSSID)等信息。\n如您同意,请点击“同意”开始接受我们的服务。', + ), + ], + ), + ); + } + + TextSpan _link(String text, String url, ShadColorScheme cs) => TextSpan( + text: text, + style: TextStyle( + color: cs.accentForeground, + fontWeight: FontWeight.w500, + decoration: TextDecoration.underline, + decorationColor: cs.accentForeground, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => launchUrlString( + AppH5UrlsHelper.withCacheBuster(url), + mode: LaunchMode.inAppBrowserView, + ), + ); +} + +class _Btn extends StatelessWidget { + const _Btn({ + required this.label, + required this.primary, + required this.cs, + required this.onTap, + }); + + final String label; + final bool primary; + final ShadColorScheme cs; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + height: 52, + alignment: Alignment.center, + decoration: BoxDecoration( + color: primary ? cs.primary : cs.secondary, + borderRadius: BorderRadius.circular(12), + border: primary ? null : Border.all(color: cs.border), + ), + child: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: primary ? cs.primaryForeground : cs.foreground, + ), + ), + ), + ); + } +} diff --git a/lib/features/settings/presentation/settings_page.dart b/lib/features/settings/presentation/settings_page.dart new file mode 100644 index 0000000..95ed0f1 --- /dev/null +++ b/lib/features/settings/presentation/settings_page.dart @@ -0,0 +1,1231 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:shadcn_ui/shadcn_ui.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import '../../../common/config/app_h5_urls.dart'; +import '../../../common/router/app_router.dart'; +import '../../../common/theme/app_text.dart'; +import '../../../common/theme/spacing.dart'; +import '../../../common/theme/theme_controller.dart'; +import '../../../common/widgets/adaptive.dart'; +import '../../../common/widgets/app_toast.dart'; +import '../../../common/widgets/circle_back_button.dart'; +import '../../auth/application/auth_controller.dart'; +import '../../auth/data/auth_models.dart'; +import '../../profile/application/profile_settings_controller.dart'; +import '../../profile/data/profile_models.dart'; + +class SettingsPage extends HookConsumerWidget { + const SettingsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final cs = ShadTheme.of(context).colorScheme; + final themeMode = ref.watch(themeModeControllerProvider); + final profileSettings = ref.watch(profileSettingsControllerProvider); + final authAsync = ref.watch(authProvider); + final loggedIn = authAsync.valueOrNull is LoggedInAuthState; + final contactDebugTapCount = useState(0); + final contactLastTapAt = useRef(null); + + return Scaffold( + backgroundColor: cs.background, + appBar: AppBar( + backgroundColor: cs.background, + surfaceTintColor: Colors.transparent, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, + title: Text( + '设置', + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + leading: const CircleBackButton(), + ), + body: ListView( + padding: Adaptive.screenPadding(context, top: 8, bottom: 32), + children: [ + const _Section('外观'), + _ThemeSegmentCard( + themeMode: themeMode, + onChanged: (mode) => + ref.read(themeModeControllerProvider.notifier).set(mode), + ), + + const SizedBox(height: 22), + const _Section('资产参数'), + _Card( + children: [ + _SwitchRow( + title: '隐藏金额', + subtitle: '在资产页和持仓列表中隐藏金额信息', + value: profileSettings.amountHidden, + onChanged: (_) => ref + .read(profileSettingsControllerProvider.notifier) + .toggleAmountHidden(), + ), + Divider(height: 1, thickness: 1, color: cs.border), + _NavRow( + icon: FlutterRemix.line_chart_line, + label: '涨跌颜色', + trailing: profileSettings.priceColorMode.label, + onTap: () => _showPriceColorSheet(context, ref), + ), + Divider(height: 1, thickness: 1, color: cs.border), + _NavRow( + icon: FlutterRemix.coins_line, + label: '纯度系数', + trailing: '${profileSettings.purityPresets.length} 项', + onTap: () => _showPuritySheet(context, profileSettings), + ), + Divider(height: 1, thickness: 1, color: cs.border), + _NavRow( + icon: FlutterRemix.exchange_box_line, + label: '回收折扣', + trailing: '已设置', + onTap: () => _showRecycleSheet(context, profileSettings), + ), + ], + ), + + const SizedBox(height: 22), + const _Section('备份'), + _Card( + children: [ + _NavRow( + icon: FlutterRemix.download_2_line, + label: '本地导出备份', + trailing: '导出', + onTap: () => _showLocalBackupSheet(context), + ), + Divider(height: 1, thickness: 1, color: cs.border), + _NavRow( + icon: FlutterRemix.cloud_line, + label: '云备份', + trailing: '占位', + onTap: () => _showCloudBackupSheet(context), + ), + ], + ), + + const SizedBox(height: 22), + const _Section('工具'), + _Card( + children: [ + _NavRow( + icon: FlutterRemix.layout_grid_line, + label: '桌面小组件', + trailing: '占位', + onTap: () => _showWidgetSheet(context), + ), + if (kDebugMode) ...[ + Divider(height: 1, thickness: 1, color: cs.border), + _NavRow( + icon: Icons.bug_report_outlined, + label: '调试工具', + onTap: () => context.push(AppRoutes.debug), + ), + ], + ], + ), + + const SizedBox(height: 22), + const _Section('法律'), + _Card( + children: [ + _NavRow( + icon: FlutterRemix.file_list_3_line, + label: '用户协议', + onTap: () => launchUrlString( + AppH5UrlsHelper.withCacheBuster(AppH5Urls.userProtocolUrl), + mode: LaunchMode.inAppBrowserView, + ), + ), + Divider(height: 1, thickness: 1, color: cs.border), + _NavRow( + icon: FlutterRemix.shield_check_line, + label: '隐私政策', + onTap: () => launchUrlString( + AppH5UrlsHelper.withCacheBuster(AppH5Urls.privacyUrl), + mode: LaunchMode.inAppBrowserView, + ), + ), + Divider(height: 1, thickness: 1, color: cs.border), + _NavRow( + icon: FlutterRemix.alert_line, + label: '免责声明', + onTap: () => _showDisclaimerSheet(context), + ), + ], + ), + + if (loggedIn) ...[ + const SizedBox(height: 22), + const _Section('账户'), + _Card( + children: [ + _NavRow( + icon: FlutterRemix.logout_box_r_line, + label: '退出登录', + onTap: () => _confirmLogout(context, ref), + ), + Divider(height: 1, thickness: 1, color: cs.border), + _NavRow( + icon: FlutterRemix.delete_bin_line, + label: '注销账号', + destructive: true, + onTap: () => context.push(AppRoutes.deleteAccount), + ), + ], + ), + ], + + const SizedBox(height: 22), + const _Section('关于'), + _Card( + children: [ + const _AboutRow(), + Divider(height: 1, thickness: 1, color: cs.border), + _NavRow( + icon: FlutterRemix.mail_line, + label: '联系我们', + trailing: 'business@hnsiderui.cn', + onTap: () { + final now = DateTime.now(); + final lastTapAt = contactLastTapAt.value; + if (lastTapAt == null || + now.difference(lastTapAt) > + const Duration(milliseconds: 500)) { + contactDebugTapCount.value = 0; + } + contactLastTapAt.value = now; + contactDebugTapCount.value += 1; + if (contactDebugTapCount.value >= 10) { + contactDebugTapCount.value = 0; + contactLastTapAt.value = null; + context.push(AppRoutes.debug); + } + }, + ), + ], + ), + const SizedBox(height: 14), + _RiskFooter(cs: cs), + ], + ), + ); + } + + Future _confirmLogout(BuildContext context, WidgetRef ref) async { + final cs = ShadTheme.of(context).colorScheme; + final ok = await showModalBottomSheet( + context: context, + backgroundColor: cs.card, + barrierColor: Colors.black.withValues(alpha: 0.5), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(Spacing.radiusCard), + ), + ), + builder: (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(9999), + ), + ), + ), + Text( + '退出登录', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 8), + Text( + '确定要退出登录吗?', + style: TextStyle(fontSize: 13, color: cs.mutedForeground), + ), + const SizedBox(height: 22), + _LogoutSheetButton( + label: '退出', + primary: true, + onTap: () => Navigator.of(ctx).pop(true), + ), + const SizedBox(height: 12), + _LogoutSheetButton( + label: '取消', + primary: false, + onTap: () => Navigator.of(ctx).pop(false), + ), + ], + ), + ), + ), + ); + if (ok == true && context.mounted) { + await ref.read(authProvider.notifier).logout(); + } + } + + Future _showPriceColorSheet(BuildContext context, WidgetRef ref) async { + final cs = ShadTheme.of(context).colorScheme; + final current = ref.read(profileSettingsControllerProvider).priceColorMode; + final selected = await showModalBottomSheet( + context: context, + backgroundColor: cs.card, + barrierColor: Colors.black.withValues(alpha: 0.5), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(Spacing.radiusCard), + ), + ), + builder: (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(9999), + ), + ), + ), + Text( + '涨跌颜色', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 8), + Text( + '选择行情上涨和下跌的配色方案。', + style: TextStyle(fontSize: 13, color: cs.mutedForeground), + ), + const SizedBox(height: 18), + _ChoiceButton( + label: PriceColorMode.redUpGreenDown.label, + selected: current == PriceColorMode.redUpGreenDown, + onTap: () => Navigator.of(ctx).pop(PriceColorMode.redUpGreenDown), + ), + const SizedBox(height: 12), + _ChoiceButton( + label: PriceColorMode.greenUpRedDown.label, + selected: current == PriceColorMode.greenUpRedDown, + onTap: () => Navigator.of(ctx).pop(PriceColorMode.greenUpRedDown), + ), + ], + ), + ), + ), + ); + if (selected != null && context.mounted) { + ref.read(profileSettingsControllerProvider.notifier).setPriceColorMode( + selected, + ); + } + } + + Future _showPuritySheet( + BuildContext context, + ProfileSettings settings, + ) async { + final cs = ShadTheme.of(context).colorScheme; + await showModalBottomSheet( + context: context, + backgroundColor: cs.card, + barrierColor: Colors.black.withValues(alpha: 0.5), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(Spacing.radiusCard), + ), + ), + builder: (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(9999), + ), + ), + ), + Text( + '纯度系数', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 8), + Text( + '用于估值换算的本地纯度预设。', + style: TextStyle(fontSize: 13, color: cs.mutedForeground), + ), + const SizedBox(height: 16), + for (final entry in settings.purityPresets.entries) ...[ + _ValueRow( + label: entry.key, + value: '${(entry.value * 100).toStringAsFixed(2)}%', + ), + if (entry.key != settings.purityPresets.keys.last) ...[ + const SizedBox(height: 10), + ], + ], + const SizedBox(height: 18), + _SheetPrimaryButton( + label: '知道了', + onTap: () => Navigator.of(ctx).pop(), + ), + ], + ), + ), + ), + ); + } + + Future _showRecycleSheet( + BuildContext context, + ProfileSettings settings, + ) async { + final cs = ShadTheme.of(context).colorScheme; + await showModalBottomSheet( + context: context, + backgroundColor: cs.card, + barrierColor: Colors.black.withValues(alpha: 0.5), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(Spacing.radiusCard), + ), + ), + builder: (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(9999), + ), + ), + ), + Text( + '回收折扣', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 8), + Text( + '用于计算金、铂、银的回收参考值。', + style: TextStyle(fontSize: 13, color: cs.mutedForeground), + ), + const SizedBox(height: 16), + for (final entry in settings.recycleDiscounts.entries) ...[ + _ValueRow( + label: entry.key.label, + value: '${(entry.value * 100).toStringAsFixed(0)}%', + ), + if (entry.key != settings.recycleDiscounts.keys.last) ...[ + const SizedBox(height: 10), + ], + ], + const SizedBox(height: 18), + _SheetPrimaryButton( + label: '知道了', + onTap: () => Navigator.of(ctx).pop(), + ), + ], + ), + ), + ), + ); + } + + Future _showLocalBackupSheet(BuildContext context) async { + final cs = ShadTheme.of(context).colorScheme; + final ok = await showModalBottomSheet( + context: context, + backgroundColor: cs.card, + barrierColor: Colors.black.withValues(alpha: 0.5), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(Spacing.radiusCard), + ), + ), + builder: (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(9999), + ), + ), + ), + Text( + '本地导出备份', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 8), + Text( + '将当前持仓、偏好设置和 mock 账号状态导出到本地文件。', + style: TextStyle(fontSize: 13, color: cs.mutedForeground), + ), + const SizedBox(height: 18), + _SheetPrimaryButton( + label: '导出备份', + onTap: () => Navigator.of(ctx).pop(true), + ), + const SizedBox(height: 12), + _SheetSecondaryButton( + label: '取消', + onTap: () => Navigator.of(ctx).pop(false), + ), + ], + ), + ), + ), + ); + if (ok == true && context.mounted) { + toastInfo(msg: '已导出本地备份(mock)'); + } + } + + Future _showCloudBackupSheet(BuildContext context) async { + final cs = ShadTheme.of(context).colorScheme; + await showModalBottomSheet( + context: context, + backgroundColor: cs.card, + barrierColor: Colors.black.withValues(alpha: 0.5), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(Spacing.radiusCard), + ), + ), + builder: (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(9999), + ), + ), + ), + Text( + '云备份', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 8), + Text( + '后续会接入云同步与多端恢复,这里先保留入口。', + style: TextStyle(fontSize: 13, color: cs.mutedForeground), + ), + const SizedBox(height: 18), + _SheetPrimaryButton( + label: '知道了', + onTap: () => Navigator.of(ctx).pop(), + ), + ], + ), + ), + ), + ); + } + + Future _showWidgetSheet(BuildContext context) async { + final cs = ShadTheme.of(context).colorScheme; + await showModalBottomSheet( + context: context, + backgroundColor: cs.card, + barrierColor: Colors.black.withValues(alpha: 0.5), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(Spacing.radiusCard), + ), + ), + builder: (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(9999), + ), + ), + ), + Text( + '桌面小组件', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 8), + Text( + '桌面行情小组件正在整理中,先保留入口。', + style: TextStyle(fontSize: 13, color: cs.mutedForeground), + ), + const SizedBox(height: 18), + _SheetPrimaryButton( + label: '知道了', + onTap: () => Navigator.of(ctx).pop(), + ), + ], + ), + ), + ), + ); + } + + Future _showDisclaimerSheet(BuildContext context) async { + final cs = ShadTheme.of(context).colorScheme; + await showModalBottomSheet( + context: context, + backgroundColor: cs.card, + barrierColor: Colors.black.withValues(alpha: 0.5), + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical( + top: Radius.circular(Spacing.radiusCard), + ), + ), + builder: (ctx) => SafeArea( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Container( + width: 38, + height: 4, + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: cs.border, + borderRadius: BorderRadius.circular(9999), + ), + ), + ), + Text( + '免责声明', + style: TextStyle( + fontSize: 19, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + const SizedBox(height: 8), + Text( + '金值提供的价格、资讯和估值均为本地 mock 数据,仅用于产品原型验证,不构成投资建议。', + style: TextStyle( + fontSize: 13, + height: 1.55, + color: cs.mutedForeground, + ), + ), + const SizedBox(height: 18), + _SheetPrimaryButton( + label: '知道了', + onTap: () => Navigator.of(ctx).pop(), + ), + ], + ), + ), + ), + ); + } +} + +class _Section extends StatelessWidget { + const _Section(this.title); + + final String title; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: cs.foreground, + ), + ), + ); + } +} + +class _Card extends StatelessWidget { + const _Card({required this.children}); + + final List children; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Container( + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(Spacing.radiusCard), + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [ + for (var i = 0; i < children.length; i++) ...[ + if (i > 0) Divider(height: 1, thickness: 1, color: cs.border), + children[i], + ], + ], + ), + ); + } +} + +class _ThemeSegmentCard extends StatelessWidget { + const _ThemeSegmentCard({required this.themeMode, required this.onChanged}); + + final ThemeMode themeMode; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final options = <(ThemeMode, String, IconData)>[ + (ThemeMode.system, '跟随系统', Icons.brightness_auto_outlined), + (ThemeMode.light, '浅色', Icons.light_mode_outlined), + (ThemeMode.dark, '深色', Icons.dark_mode_outlined), + ]; + return Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: cs.secondary, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + for (final opt in options) + Expanded( + child: Padding( + padding: EdgeInsets.only( + left: opt == options.first ? 0 : 2, + right: opt == options.last ? 0 : 2, + ), + child: _ThemeSegmentButton( + label: opt.$2, + icon: opt.$3, + selected: themeMode == opt.$1, + onTap: () => onChanged(opt.$1), + ), + ), + ), + ], + ), + ); + } +} + +class _ThemeSegmentButton extends StatelessWidget { + const _ThemeSegmentButton({ + required this.label, + required this.icon, + required this.selected, + required this.onTap, + }); + + final String label; + final IconData icon; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final fg = selected ? cs.foreground : cs.mutedForeground; + return Material( + color: selected ? cs.background : Colors.transparent, + borderRadius: BorderRadius.circular(10), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(10), + child: Container( + height: 44, + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + boxShadow: selected + ? [ + const BoxShadow( + color: Color(0x0A000000), + blurRadius: 10, + offset: Offset(0, 2), + ), + ] + : null, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 17, color: fg), + const SizedBox(width: 8), + Text( + label, + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: fg, + ), + ), + ], + ), + ), + ), + ); + } +} + +class _SwitchRow extends StatelessWidget { + const _SwitchRow({ + required this.title, + required this.subtitle, + required this.value, + required this.onChanged, + }); + + final String title; + final String subtitle; + final bool value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: cs.foreground, + ), + ), + const SizedBox(height: 6), + Text( + subtitle, + style: AppText.meta.copyWith( + fontSize: 13, + color: cs.mutedForeground, + ), + ), + ], + ), + ), + const SizedBox(width: 16), + ShadSwitch( + value: value, + onChanged: onChanged, + checkedTrackColor: cs.primary, + thumbColor: cs.background, + width: 44, + height: 26, + margin: 2, + ), + ], + ), + ); + } +} + +class _NavRow extends StatelessWidget { + const _NavRow({ + required this.icon, + required this.label, + this.trailing, + this.onTap, + this.destructive = false, + }); + + final IconData icon; + final String label; + final String? trailing; + final VoidCallback? onTap; + final bool destructive; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + final color = destructive ? cs.destructive : cs.foreground; + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), + child: Row( + children: [ + SizedBox(width: 24, child: Icon(icon, size: 20, color: color)), + const SizedBox(width: 13), + Expanded( + child: Text(label, style: TextStyle(fontSize: 15, color: color)), + ), + if (trailing != null) + Text( + trailing!, + style: AppText.meta.copyWith( + fontSize: 13, + color: cs.mutedForeground, + ), + ), + if (onTap != null && trailing == null) + Icon( + FlutterRemix.arrow_right_s_line, + size: 20, + color: cs.mutedForeground, + ), + ], + ), + ), + ); + } +} + +class _AboutRow extends StatelessWidget { + const _AboutRow(); + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return FutureBuilder( + future: PackageInfo.fromPlatform(), + builder: (context, snap) { + final v = snap.hasData + ? 'v${snap.data!.version}+${snap.data!.buildNumber}' + : ''; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 15), + child: Row( + children: [ + SizedBox( + width: 24, + child: Icon( + FlutterRemix.information_line, + size: 20, + color: cs.foreground, + ), + ), + const SizedBox(width: 13), + Expanded( + child: Text( + '金值', + style: TextStyle(fontSize: 15, color: cs.foreground), + ), + ), + Text( + v, + style: AppText.meta.copyWith( + fontSize: 13, + color: cs.mutedForeground, + ), + ), + ], + ), + ); + }, + ); + } +} + +class _RiskFooter extends StatelessWidget { + const _RiskFooter({required this.cs}); + + final ShadColorScheme cs; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), + decoration: BoxDecoration( + color: cs.card, + border: Border.all(color: cs.border), + borderRadius: BorderRadius.circular(Spacing.radiusCard), + ), + child: Text( + '风险提示 · 数据来源 · 免责\n价格、资讯与估值均为本地 mock 数据,仅用于产品原型验证,不构成投资建议。', + style: AppText.meta.copyWith( + fontSize: 12, + color: cs.mutedForeground, + height: 1.65, + ), + ), + ); + } +} + +class _ChoiceButton extends StatelessWidget { + const _ChoiceButton({ + required this.label, + required this.selected, + required this.onTap, + }); + + final String label; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + width: double.infinity, + height: 52, + alignment: Alignment.center, + decoration: BoxDecoration( + color: selected ? cs.primary : cs.secondary, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: selected ? cs.primary : cs.border, + ), + ), + child: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: selected ? cs.primaryForeground : cs.foreground, + ), + ), + ), + ); + } +} + +class _SheetPrimaryButton extends StatelessWidget { + const _SheetPrimaryButton({required this.label, required this.onTap}); + + final String label; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Container( + width: double.infinity, + height: 52, + alignment: Alignment.center, + decoration: BoxDecoration( + color: cs.primary, + borderRadius: BorderRadius.circular(12), + ), + child: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: cs.primaryForeground, + ), + ), + ), + ); + } +} + +class _SheetSecondaryButton extends StatelessWidget { + const _SheetSecondaryButton({required this.label, required this.onTap}); + + final String label; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: Container( + width: double.infinity, + height: 52, + alignment: Alignment.center, + decoration: BoxDecoration( + color: cs.secondary, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: cs.border), + ), + child: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: cs.foreground, + ), + ), + ), + ); + } +} + +class _ValueRow extends StatelessWidget { + const _ValueRow({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return Row( + children: [ + Expanded( + child: Text( + label, + style: TextStyle(fontSize: 15, color: cs.foreground), + ), + ), + Text( + value, + style: TextStyle(fontSize: 13, color: cs.mutedForeground), + ), + ], + ); + } +} + +class _LogoutSheetButton extends StatelessWidget { + const _LogoutSheetButton({ + required this.label, + required this.primary, + required this.onTap, + }); + + final String label; + final bool primary; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final cs = ShadTheme.of(context).colorScheme; + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + width: double.infinity, + height: 52, + alignment: Alignment.center, + decoration: BoxDecoration( + color: primary ? cs.primary : cs.secondary, + borderRadius: BorderRadius.circular(12), + border: primary ? null : Border.all(color: cs.border), + ), + child: Text( + label, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: primary ? cs.primaryForeground : cs.foreground, + ), + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..e1dcdc2 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,8 @@ +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'app/bootstrap.dart'; + +Future main() async { + final app = await bootstrap(); + runApp(ProviderScope(child: app)); +} diff --git a/packages/android_id/.gitattributes b/packages/android_id/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/packages/android_id/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/packages/android_id/.github/dependabot.yml b/packages/android_id/.github/dependabot.yml new file mode 100644 index 0000000..f8218c7 --- /dev/null +++ b/packages/android_id/.github/dependabot.yml @@ -0,0 +1,37 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" + + - package-ecosystem: "pub" + directory: "/" + schedule: + interval: "daily" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "pub" + directory: "example" + schedule: + interval: "daily" + ignore: + - dependency-name: "*" + update-types: ["version-update:semver-minor", "version-update:semver-patch"] + + - package-ecosystem: "gradle" + directory: "android" + schedule: + interval: "daily" + + - package-ecosystem: "gradle" + directory: "example/android" + schedule: + interval: "daily" diff --git a/packages/android_id/.github/workflows/flutter_build_example.yml b/packages/android_id/.github/workflows/flutter_build_example.yml new file mode 100644 index 0000000..542ef32 --- /dev/null +++ b/packages/android_id/.github/workflows/flutter_build_example.yml @@ -0,0 +1,34 @@ +name: Flutter build example + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + build_android: + name: Build example for Android + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + - name: Install Flutter + uses: subosito/flutter-action@v2 + - name: Disable analytics + run: flutter config --no-analytics + - name: Flutter pub get + run: flutter pub get + - name: Flutter build appbundle + run: | + cd example + flutter build appbundle --release diff --git a/packages/android_id/.github/workflows/flutter_checks.yml b/packages/android_id/.github/workflows/flutter_checks.yml new file mode 100644 index 0000000..a124a06 --- /dev/null +++ b/packages/android_id/.github/workflows/flutter_checks.yml @@ -0,0 +1,101 @@ +name: Flutter checks + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + check_pub_dependencies: + name: Check dependencies + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + - name: Install Flutter + uses: subosito/flutter-action@v2 + - name: Disable analytics + run: flutter config --no-analytics + - name: Flutter pub get + run: flutter pub get + + check_formatting: + name: Check formatting + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + - name: Install Flutter + uses: subosito/flutter-action@v2 + - name: Disable analytics + run: flutter config --no-analytics + - name: Flutter pub get + run: flutter pub get + - name: Check Dart formatting + run: dart format --line-length 80 --set-exit-if-changed . + + analyze: + name: Dart analyze + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + - name: Install Flutter + uses: subosito/flutter-action@v2 + - name: Disable analytics + run: flutter config --no-analytics + - name: Flutter pub get + run: flutter pub get + - name: Run Dart analyze + uses: invertase/github-action-dart-analyzer@v3.0.0 + + test: + name: Flutter test + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + - name: Install Flutter + uses: subosito/flutter-action@v2 + - name: Disable analytics + run: flutter config --no-analytics + - name: Flutter pub get + run: flutter pub get + - name: Flutter test + run: flutter test + + test_example: + name: Flutter test example + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + - name: Install Flutter + uses: subosito/flutter-action@v2 + - name: Disable analytics + run: flutter config --no-analytics + - name: Flutter pub get + run: | + cd example + flutter pub get + - name: Flutter test example + run: | + cd example + flutter test diff --git a/packages/android_id/.github/workflows/pub_publish.yml b/packages/android_id/.github/workflows/pub_publish.yml new file mode 100644 index 0000000..1630b63 --- /dev/null +++ b/packages/android_id/.github/workflows/pub_publish.yml @@ -0,0 +1,18 @@ +name: Pub publish + +on: + release: + types: [published] + +jobs: + publish: + name: Pub publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + - name: Publish + uses: k-paxian/dart-package-publisher@v1.6 + with: + credentialJson: ${{ secrets.CREDENTIALS }} diff --git a/packages/android_id/.github/workflows/pub_publish_dry_run.yml b/packages/android_id/.github/workflows/pub_publish_dry_run.yml new file mode 100644 index 0000000..8cb6b5d --- /dev/null +++ b/packages/android_id/.github/workflows/pub_publish_dry_run.yml @@ -0,0 +1,21 @@ +name: Pub publish dry run + +on: + workflow_dispatch: + push: + branches: + - main + +jobs: + publish-dry-run: + name: Pub publish dry run + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + - name: Publish dry run + uses: k-paxian/dart-package-publisher@v1.6 + with: + credentialJson: 'MockCredentialJson' + dryRunOnly: true diff --git a/packages/android_id/.github/workflows/pub_score.yml b/packages/android_id/.github/workflows/pub_score.yml new file mode 100644 index 0000000..c423c77 --- /dev/null +++ b/packages/android_id/.github/workflows/pub_score.yml @@ -0,0 +1,32 @@ +name: Check pub score + +on: + workflow_dispatch: + pull_request: + push: + branches: + - main + +jobs: + check-score: + name: Check pub score + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + - uses: axel-op/dart-package-analyzer@v3 + id: workflow + with: + githubToken: ${{ secrets.GITHUB_TOKEN }} + - name: Check score + env: + TOTAL: ${{ steps.workflow.outputs.total }} + TOTAL_MAX: ${{ steps.workflow.outputs.total_max }} + run: | + MISSING_POINTS=$(( $TOTAL_MAX - $TOTAL )) + if (( MISSING_POINTS > 0 )) + then + echo Score can be improved! + exit 1 + fi diff --git a/packages/android_id/.gitignore b/packages/android_id/.gitignore new file mode 100644 index 0000000..f2e83f9 --- /dev/null +++ b/packages/android_id/.gitignore @@ -0,0 +1,26 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ +.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/android_id/.metadata b/packages/android_id/.metadata new file mode 100644 index 0000000..f3deb51 --- /dev/null +++ b/packages/android_id/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + - platform: android + create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3 + + # 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/packages/android_id/CHANGELOG.md b/packages/android_id/CHANGELOG.md new file mode 100644 index 0000000..1e24ea5 --- /dev/null +++ b/packages/android_id/CHANGELOG.md @@ -0,0 +1,103 @@ +## 0.4.0 + +* Upgrade Android dependencies (Gradle, AGP, Java, Kotlin) +* Widen `flutter_lints` constraints + +## 0.3.6 + +* Return `null` on platforms other than Android, instead of throwing an exception + +## 0.3.5 + +* Maintenance release +* Update `kotlin` and `gradle` + +## 0.3.4 + +* Refactor native code + +## 0.3.3 + +* Add topics to pubspec.yaml + +## 0.3.2 + +* Improve `namespace` fix for `Android Gradle Plugin` versions < 7 + +## 0.3.1 + +* Fix `namespace` for `Android Gradle Plugin` versions < 7 + +## 0.3.0 + +* Add `namespace` for compatibility with `Android Gradle Plugin 8` +* Update `Gradle` version + +## 0.2.0 + +* Update `Gradle` version +* Update `Android Gradle plugin` version +* Update `Kotlin` version +* Add Gradle wrapper files +* Update `pubspec.lock` of example (`url: "https://pub.dev"`) + +## 0.1.3+1 + +* Improve readme: Add explanation for connection of `Android ID` and signing key + +## 0.1.3 + +* Add building of example app to GitHub workflows +* Remove unneeded code + +## 0.1.2 + +* Add `Flutter test` for example app to workflows + +## 0.1.1+1 + +* Update readme + +## 0.1.1 + +* Update example app dependencies + +## 0.1.0 + +* Rename Android `applicationId` +* Recreate `android` folder from template + +## 0.0.8 + +* Add more GitHub Actions for checking code quality +* Update kotlin_version from 1.6.10 to 1.7.20 +* Update gradle from 7.1.2 to 7.3.1 +* Add dependabot + +## 0.0.7 + +* Add Google Play information to README. + +## 0.0.6 + +* Lower minimal sdk version to `2.12.0`. + +## 0.0.5 + +* Documentation typo fix. + +## 0.0.4 + +* Small improvements. + +## 0.0.3 + +* Improve example. + +## 0.0.2 + +* Enhance description to get full pub.dev score. + +## 0.0.1 + +* Initial release. diff --git a/packages/android_id/LICENSE b/packages/android_id/LICENSE new file mode 100644 index 0000000..a1092eb --- /dev/null +++ b/packages/android_id/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, Joachim Nohl +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/android_id/README.md b/packages/android_id/README.md new file mode 100644 index 0000000..36aed12 --- /dev/null +++ b/packages/android_id/README.md @@ -0,0 +1,47 @@ +# android_id + +[![Flutter Community: android_id](https://fluttercommunity.dev/_github/header/android_id)](https://github.com/fluttercommunity/community) + +![Pub Version](https://img.shields.io/pub/v/android_id) + +A Flutter plugin for retrieving the Android ID. + +## Getting started + +* Add plugin to pubspec.yaml +* Use it in your code (see examples of all methods below) + +## Usage + +```dart +const _androidIdPlugin = AndroidId(); + +final String? androidId = await _androidIdPlugin.getId(); +``` + +## Important + +Please note that on `Android 8` and above, the `Android ID` is not unique per device, but also per signing key the app was built with: + +
+On Android 8.0 (API level 26) and higher versions of the platform, a 64-bit number (expressed as a hexadecimal string), unique to each combination of app-signing key, user, and device. + +The value may change if a factory reset is performed on the device or if an APK signing key changes. +
+ +[Android API reference](https://developer.android.com/reference/android/provider/Settings.Secure#ANDROID_ID) + +[Stack Overflow explanation](https://stackoverflow.com/a/43393373) + + +## Google Play + +Before using this plugin in your app, make sure to follow Google Play guidelines. +For example [here](https://support.google.com/googleplay/android-developer/answer/6048248#zippy=%2Cpersistent-identifiers-including-android-id): + +
+Persistent identifiers, including Android ID + +Use for non-advertising purposes
+You can use persistent identifiers as long as you have a [privacy policy](https://support.google.com/googleplay/android-developer/answer/9859455) and handle the data in accordance with the [Developer Distribution Agreement](https://play.google.com/about/developer-distribution-agreement.html#use) and all applicable privacy laws in the areas where you make your app available. +
diff --git a/packages/android_id/analysis_options.yaml b/packages/android_id/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/packages/android_id/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/packages/android_id/android/.gitignore b/packages/android_id/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/packages/android_id/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/android_id/android/build.gradle b/packages/android_id/android/build.gradle new file mode 100644 index 0000000..2faa0c2 --- /dev/null +++ b/packages/android_id/android/build.gradle @@ -0,0 +1,74 @@ +group = "dev.fluttercommunity.android_id" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.0.0" + repositories { + google() + mavenCentral() + maven{url'https://repo1.maven.org/maven2/'} + } + + dependencies { + classpath("com.android.tools.build:gradle:8.1.4") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven{url'https://repo1.maven.org/maven2/'} + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + if (project.android.hasProperty("namespace")) { + namespace = "dev.fluttercommunity.android_id" + } + + compileSdk = 34 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 21 + } + + dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.12.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} +dependencies { +// implementation 'com.umeng.umsdk:common:9.7.7'// 必选 +// implementation 'com.umeng.umsdk:asms:1.8.3'// 必选 +} diff --git a/packages/android_id/android/settings.gradle b/packages/android_id/android/settings.gradle new file mode 100644 index 0000000..8bbde2e --- /dev/null +++ b/packages/android_id/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'android_id' diff --git a/packages/android_id/android/src/main/AndroidManifest.xml b/packages/android_id/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..84a140d --- /dev/null +++ b/packages/android_id/android/src/main/AndroidManifest.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/packages/android_id/android/src/main/kotlin/dev/fluttercommunity/android_id/AndroidIdPlugin.kt b/packages/android_id/android/src/main/kotlin/dev/fluttercommunity/android_id/AndroidIdPlugin.kt new file mode 100644 index 0000000..d79dfc1 --- /dev/null +++ b/packages/android_id/android/src/main/kotlin/dev/fluttercommunity/android_id/AndroidIdPlugin.kt @@ -0,0 +1,279 @@ +package dev.fluttercommunity.android_id + +import android.annotation.SuppressLint +import android.content.ContentResolver +import android.provider.Settings +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import android.content.Context +import android.net.wifi.WifiInfo +import android.net.wifi.WifiManager +import android.os.Build +import android.text.TextUtils +import androidx.core.app.ActivityCompat + +import java.io.IOException +import java.io.InputStreamReader +import java.io.LineNumberReader +import java.net.NetworkInterface +import java.util.Collections +import java.util.List +import java.util.ArrayList +import java.util.Locale +import android.content.pm.PackageManager +import android.telephony.TelephonyManager +//import com.umeng.commonsdk.UMConfigure + +/** AndroidIdPlugin */ +class AndroidIdPlugin : FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel: MethodChannel + + private lateinit var contentResolver: ContentResolver + + private lateinit var context: Context + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + contentResolver = flutterPluginBinding.applicationContext.contentResolver + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "android_id") + channel.setMethodCallHandler(this) + context = flutterPluginBinding.getApplicationContext() + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "getId" -> { + try { + result.success(getAndroidId()) + } catch (e: Exception) { + result.success("") +// result.error("ERROR_GETTING_ID", "Failed to get Android ID", e.localizedMessage) + } + } + + "getImei" -> { + try { + result.success(getImei()) + } catch (e: Exception) { + result.success("") +// result.error("ERROR_GETTING_ID", "Failed to get Imei ", e.localizedMessage) + } + } + + "getMacAddress" -> { + try { + result.success(getMacAddress()) + } catch (e: Exception) { + result.success("") +// result.error("ERROR_GETTING_ID", "Failed to get MacAddress", e.localizedMessage) + } + } + +// "getOaid" -> { +// try { +// UMConfigure.getOaid(context) { p0 -> +// if (p0 != null) { +// result.success(p0) +// } else { +// result.success("") +// } +// } +// } catch (e: Exception) { +// result.success("") +//// result.error("ERROR_GETTING_ID", "Failed to get MacAddress", e.localizedMessage) +// } +// } + + "checkRoot" -> { + try { + result.success(checkRoot()) + } catch (e: Exception) { + result.success(false) +// result.error("ERROR_GETTING_ID", "Failed to get MacAddress", e.localizedMessage) + } + } + "checkIsEmulator" -> { + try { + result.success(checkIsEmulator()) + } catch (e: Exception) { + result.success(false) +// result.error("ERROR_GETTING_ID", "Failed to get MacAddress", e.localizedMessage) + } + } + + else -> result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + // Fetch the Android ID while suppressing lint warning about hardware IDs + @SuppressLint("HardwareIds") + private fun getAndroidId(): String { + return Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) + } + + private fun getImei(): String? { + val tm: TelephonyManager = + context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager + var IMEIStr = "" + if (ActivityCompat.checkSelfPermission( + context, + android.Manifest.permission.READ_PHONE_STATE + ) !== PackageManager.PERMISSION_GRANTED + ) { + } else { + try { + IMEIStr = tm.getDeviceId() + if (TextUtils.isEmpty(IMEIStr)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + IMEIStr = tm.getImei() + } + } + } catch (e: java.lang.Exception) { + } + } + return IMEIStr + } + + private fun getMacAddress(): String? { + var macNo = MacUtil().getMac(context) + return macNo + } + + + private fun checkRoot(): Boolean { + return false + } + + private fun checkIsEmulator(): Boolean { + return false + } + +} + +class MacUtil { + /** + * Android 6.0 之前(不包括6.0)获取mac地址 + * 必须的权限 + * + * @param context * @return + */ + @SuppressLint("MissingPermission") + fun getMacDefault(context: Context?): String? { + var mac = "" + if (context == null) { + return mac + } + val wifi: WifiManager = + context.getApplicationContext().getSystemService(Context.WIFI_SERVICE) as WifiManager + var info: WifiInfo? = null + try { + info = wifi.getConnectionInfo() + } catch (e: java.lang.Exception) { + e.printStackTrace() + } + + if (info == null) { + return null + } + mac = info.getMacAddress() + if (!TextUtils.isEmpty(mac)) { + mac = mac.uppercase() + } + return mac + } + + val macAddress: String? + /** + * Android 6.0-Android 7.0 获取mac地址 + */ + get() { + var macSerial: String? = null + var str = "" + + try { + val pp: java.lang.Process = + java.lang.Runtime.getRuntime().exec("cat/sys/class/net/wlan0/address") + val ir: InputStreamReader = InputStreamReader(pp.getInputStream()) + val input: LineNumberReader = LineNumberReader(ir) + + while (null != str) { + str = input.readLine() + if (str != null) { + macSerial = str.trim { it <= ' ' } //去空格 + break + } + } + } catch (ex: IOException) { + // 赋予默认值 + ex.printStackTrace() + } + + return macSerial + } + + /** + * 获取mac地址(适配所有Android版本) + * + * @return + */ + fun getMac(context: Context?): String? { + var mac: String? = "" + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + mac = getMacDefault(context) + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + mac = macAddress + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mac = macFromHardware + } + return mac + } + + companion object { + private val macFromHardware: String? + /** + * Android 7.0之后获取Mac地址 + * 遍历循环所有的网络接口,找到接口是 wlan0 + * 必须的权限 + * + * @return + */ + get() { + try { + val all: ArrayList = + Collections.list(NetworkInterface.getNetworkInterfaces()) + for (nif in all) { + if (!nif.getName().equals("wlan0", ignoreCase = true)) { + continue + } + + val macBytes: ByteArray = nif.getHardwareAddress() ?: return null + + val res1: java.lang.StringBuilder = java.lang.StringBuilder() + for (b in macBytes) { + res1.append(String.format("%02X:", b)) + } + + if (res1.length > 0) { + res1.deleteCharAt(res1.length - 1) + } + return res1.toString() + } + } catch (e: java.lang.Exception) { + e.printStackTrace() + } + return null + } + } +} diff --git a/packages/android_id/lib/android_id.dart b/packages/android_id/lib/android_id.dart new file mode 100644 index 0000000..e3fccd1 --- /dev/null +++ b/packages/android_id/lib/android_id.dart @@ -0,0 +1,58 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +/// The plugin class for retrieving the Android ID. +class AndroidId { + const AndroidId(); + + /// The method channel used to interact with the native platform. + static const _methodChannel = MethodChannel('android_id'); + + /// Calls the native method to retrieve the Android ID. + Future getId() async { + final isAndroid = + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + if (!isAndroid) return null; + + return _methodChannel.invokeMethod('getId'); + } + + Future getImei() async { + final isAndroid = + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + if (!isAndroid) return null; + + return _methodChannel.invokeMethod('getImei'); + } + + Future getMaAddress() async { + final isAndroid = + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + if (!isAndroid) return null; + + return _methodChannel.invokeMethod('getMacAddress'); + } + // Future getOaid() async { + // final isAndroid = + // !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + // if (!isAndroid) return null; + // + // return _methodChannel.invokeMethod('getOaid'); + // } + + Future isRoot() async { + final isAndroid = + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + if (!isAndroid) return null; + + return _methodChannel.invokeMethod('checkRoot'); + } + + Future isEmulator() async { + final isAndroid = + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + if (!isAndroid) return null; + + return _methodChannel.invokeMethod('checkIsEmulator'); + } +} diff --git a/packages/android_id/pubspec.yaml b/packages/android_id/pubspec.yaml new file mode 100644 index 0000000..e835129 --- /dev/null +++ b/packages/android_id/pubspec.yaml @@ -0,0 +1,36 @@ +name: android_id +description: A Flutter plugin for getting the Android ID. Only for Android. + +repository: https://github.com/fluttercommunity/android_id +issue_tracker: https://github.com/fluttercommunity/android_id/issues + +maintainer: Joachim Nohl (@nohli) + +topics: + - androidid + - device + - identifier + - android + - native + +version: 0.4.0 + +environment: + sdk: '>=2.12.0 <4.0.0' + flutter: '>=2.0.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_lints: '>=2.0.0 <4.0.0' + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + android: + package: dev.fluttercommunity.android_id + pluginClass: AndroidIdPlugin diff --git a/packages/android_id/test/android_id_test.dart b/packages/android_id/test/android_id_test.dart new file mode 100644 index 0000000..8c015e6 --- /dev/null +++ b/packages/android_id/test/android_id_test.dart @@ -0,0 +1,26 @@ +import 'package:android_id/android_id.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const plugin = AndroidId(); + const channel = MethodChannel('android_id'); + + TestWidgetsFlutterBinding.ensureInitialized(); + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async => '42'); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('getAndroidId', () async { + expect(await plugin.getId(), '42'); + }); +} diff --git a/packages/android_oaid/.gitignore b/packages/android_oaid/.gitignore new file mode 100644 index 0000000..f2e83f9 --- /dev/null +++ b/packages/android_oaid/.gitignore @@ -0,0 +1,26 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ +.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/android_oaid/CHANGELOG.md b/packages/android_oaid/CHANGELOG.md new file mode 100644 index 0000000..d0bd041 --- /dev/null +++ b/packages/android_oaid/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* Initial release. diff --git a/packages/android_oaid/LICENSE b/packages/android_oaid/LICENSE new file mode 100644 index 0000000..a1092eb --- /dev/null +++ b/packages/android_oaid/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2022, Joachim Nohl +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/packages/android_oaid/README.md b/packages/android_oaid/README.md new file mode 100644 index 0000000..87a8f5d --- /dev/null +++ b/packages/android_oaid/README.md @@ -0,0 +1,15 @@ +# flutter_oaid + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/packages/android_oaid/analysis_options.yaml b/packages/android_oaid/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/packages/android_oaid/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/packages/android_oaid/android/.gitignore b/packages/android_oaid/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/packages/android_oaid/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/packages/android_oaid/android/build.gradle b/packages/android_oaid/android/build.gradle new file mode 100644 index 0000000..ac30102 --- /dev/null +++ b/packages/android_oaid/android/build.gradle @@ -0,0 +1,74 @@ +group = "dev.fluttercommunity.android_oaid" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.0.0" + repositories { + google() + mavenCentral() + maven{url'https://repo1.maven.org/maven2/'} + } + + dependencies { + classpath("com.android.tools.build:gradle:8.1.4") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven{url'https://repo1.maven.org/maven2/'} + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + if (project.android.hasProperty("namespace")) { + namespace = "dev.fluttercommunity.android_oaid" + } + + compileSdk = 34 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 21 + } + + dependencies { + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.12.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} +dependencies { + implementation 'com.umeng.umsdk:common:9.7.7'// 必选 + implementation 'com.umeng.umsdk:asms:1.8.3'// 必选 +} diff --git a/packages/android_oaid/android/settings.gradle b/packages/android_oaid/android/settings.gradle new file mode 100644 index 0000000..0af1752 --- /dev/null +++ b/packages/android_oaid/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'android_oaid' diff --git a/packages/android_oaid/android/src/main/AndroidManifest.xml b/packages/android_oaid/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..57a3bdb --- /dev/null +++ b/packages/android_oaid/android/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + diff --git a/packages/android_oaid/android/src/main/kotlin/dev/fluttercommunity/android_oaid/AndroidOaidPlugin.kt b/packages/android_oaid/android/src/main/kotlin/dev/fluttercommunity/android_oaid/AndroidOaidPlugin.kt new file mode 100644 index 0000000..31e4b49 --- /dev/null +++ b/packages/android_oaid/android/src/main/kotlin/dev/fluttercommunity/android_oaid/AndroidOaidPlugin.kt @@ -0,0 +1,59 @@ +package dev.fluttercommunity.android_oaid + +import android.content.ContentResolver + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import android.content.Context +import android.os.Handler +import android.os.Looper + +import com.umeng.commonsdk.UMConfigure + +/** AndroidOaidPlugin */ +class AndroidOaidPlugin : FlutterPlugin, MethodCallHandler { + + private lateinit var channel: MethodChannel + + private lateinit var contentResolver: ContentResolver + + private lateinit var context: Context + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + contentResolver = flutterPluginBinding.applicationContext.contentResolver + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "android_oaid") + channel.setMethodCallHandler(this) + context = flutterPluginBinding.getApplicationContext() + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "getOaid" -> { + try { + UMConfigure.getOaid(context) { p0 -> + Handler(Looper.getMainLooper()).post { + if (p0 != null) { + result.success(p0) + } else { + result.success("") + } + } + } + } catch (e: Exception) { + Handler(Looper.getMainLooper()).post { + result.success("") + } + } + } + + else -> result.notImplemented() + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/packages/android_oaid/lib/android_oaid.dart b/packages/android_oaid/lib/android_oaid.dart new file mode 100644 index 0000000..865a2e4 --- /dev/null +++ b/packages/android_oaid/lib/android_oaid.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +class AndroidOaidId { + const AndroidOaidId(); + + static const _methodChannel = MethodChannel('android_oaid'); + + + Future getOaid() async { + final isAndroid = + !kIsWeb && defaultTargetPlatform == TargetPlatform.android; + if (!isAndroid) return null; + + return _methodChannel.invokeMethod('getOaid'); + } + +} diff --git a/packages/android_oaid/pubspec.yaml b/packages/android_oaid/pubspec.yaml new file mode 100644 index 0000000..1317f24 --- /dev/null +++ b/packages/android_oaid/pubspec.yaml @@ -0,0 +1,26 @@ +name: android_oaid +description: A Flutter plugin for getting the Android OAID. Only for Android. + +maintainer: Joachim Nohl (@nohli) + +version: 0.4.0 + +environment: + sdk: '>=2.12.0 <4.0.0' + flutter: '>=2.0.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_lints: '>=2.0.0 <4.0.0' + flutter_test: + sdk: flutter + +flutter: + plugin: + platforms: + android: + package: dev.fluttercommunity.android_oaid + pluginClass: AndroidOaidPlugin diff --git a/packages/android_oaid/test/android_id_test.dart b/packages/android_oaid/test/android_id_test.dart new file mode 100644 index 0000000..8fc0dd9 --- /dev/null +++ b/packages/android_oaid/test/android_id_test.dart @@ -0,0 +1,26 @@ +import 'package:android_oaid/android_oaid.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + const plugin = AndroidOaidId(); + const channel = MethodChannel('android_oaid'); + + TestWidgetsFlutterBinding.ensureInitialized(); + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (methodCall) async => '42'); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + }); + + test('getAndroidId', () async { + expect(await plugin.getOaid(), '42'); + }); +} diff --git a/packages/flutter_paid/.gitignore b/packages/flutter_paid/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/packages/flutter_paid/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/packages/flutter_paid/.metadata b/packages/flutter_paid/.metadata new file mode 100644 index 0000000..3a8bfa6 --- /dev/null +++ b/packages/flutter_paid/.metadata @@ -0,0 +1,30 @@ +# 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: "abb292a07e20d696c4568099f918f6c5f330e6b0" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: abb292a07e20d696c4568099f918f6c5f330e6b0 + base_revision: abb292a07e20d696c4568099f918f6c5f330e6b0 + - platform: ios + create_revision: abb292a07e20d696c4568099f918f6c5f330e6b0 + base_revision: abb292a07e20d696c4568099f918f6c5f330e6b0 + + # 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/packages/flutter_paid/CHANGELOG.md b/packages/flutter_paid/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/packages/flutter_paid/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/flutter_paid/LICENSE b/packages/flutter_paid/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/packages/flutter_paid/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/flutter_paid/README.md b/packages/flutter_paid/README.md new file mode 100644 index 0000000..ebc7b79 --- /dev/null +++ b/packages/flutter_paid/README.md @@ -0,0 +1,15 @@ +# flutter_paid + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/developing-packages/), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/packages/flutter_paid/analysis_options.yaml b/packages/flutter_paid/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/packages/flutter_paid/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/flutter_paid/example/.gitignore b/packages/flutter_paid/example/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/packages/flutter_paid/example/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# 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/packages/flutter_paid/example/README.md b/packages/flutter_paid/example/README.md new file mode 100644 index 0000000..1662596 --- /dev/null +++ b/packages/flutter_paid/example/README.md @@ -0,0 +1,16 @@ +# flutter_paid_example + +Demonstrates how to use the flutter_paid plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/flutter_paid/example/analysis_options.yaml b/packages/flutter_paid/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/packages/flutter_paid/example/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/packages/flutter_paid/example/integration_test/plugin_integration_test.dart b/packages/flutter_paid/example/integration_test/plugin_integration_test.dart new file mode 100644 index 0000000..5a6e347 --- /dev/null +++ b/packages/flutter_paid/example/integration_test/plugin_integration_test.dart @@ -0,0 +1,25 @@ +// This is a basic Flutter integration test. +// +// Since integration tests run in a full Flutter application, they can interact +// with the host side of a plugin implementation, unlike Dart unit tests. +// +// For more information about Flutter integration tests, please see +// https://docs.flutter.dev/cookbook/testing/integration/introduction + + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:flutter_paid/flutter_paid.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getPlatformVersion test', (WidgetTester tester) async { + final FlutterPaid plugin = FlutterPaid(); + final String? version = await plugin.getPlatformVersion(); + // The version string depends on the host platform running the test, so + // just assert that some non-empty string is returned. + expect(version?.isNotEmpty, true); + }); +} diff --git a/packages/flutter_paid/example/ios/.gitignore b/packages/flutter_paid/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/packages/flutter_paid/example/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/packages/flutter_paid/example/ios/Flutter/AppFrameworkInfo.plist b/packages/flutter_paid/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/packages/flutter_paid/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/packages/flutter_paid/example/ios/Flutter/Debug.xcconfig b/packages/flutter_paid/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/packages/flutter_paid/example/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/packages/flutter_paid/example/ios/Flutter/Release.xcconfig b/packages/flutter_paid/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/packages/flutter_paid/example/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/packages/flutter_paid/example/ios/Podfile b/packages/flutter_paid/example/ios/Podfile new file mode 100644 index 0000000..c9339a0 --- /dev/null +++ b/packages/flutter_paid/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + 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) + end +end diff --git a/packages/flutter_paid/example/ios/Podfile.lock b/packages/flutter_paid/example/ios/Podfile.lock new file mode 100644 index 0000000..e9e715f --- /dev/null +++ b/packages/flutter_paid/example/ios/Podfile.lock @@ -0,0 +1,28 @@ +PODS: + - Flutter (1.0.0) + - flutter_paid (0.0.1): + - Flutter + - integration_test (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - flutter_paid (from `.symlinks/plugins/flutter_paid/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + flutter_paid: + :path: ".symlinks/plugins/flutter_paid/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_paid: 4461d2236314bded6afb221cd9d69b7f7ca51fc6 + integration_test: 13825b8a9334a850581300559b8839134b124670 + +PODFILE CHECKSUM: 6e700dec67e6deac9b1c69bb14c49a2217a12d15 + +COCOAPODS: 1.14.3 diff --git a/packages/flutter_paid/example/ios/Runner.xcodeproj/project.pbxproj b/packages/flutter_paid/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..343e2db --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,704 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1200A014D953763B34535EB3 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CB8157ECFBC61BAC8F6DDD98 /* libPods-Runner.a */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C80F4294D02FB00263BE5 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 331C80F3294D02FB00263BE5 /* RunnerTests.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74D02B72575A8C8A1492AE70 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF91C3F82401F1EC86B8BDBB /* libPods-RunnerTests.a */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80F5294D02FB00263BE5 /* 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 */ + 0733CEEBA4D517AC13DB7032 /* 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 = ""; }; + 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 = ""; }; + 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80F3294D02FB00263BE5 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; 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; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 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 = ""; }; + AA2F596DEE97CE584753926C /* 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 = ""; }; + BF91C3F82401F1EC86B8BDBB /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + C4F40650D87222AA05FBB409 /* 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 = ""; }; + CB8157ECFBC61BAC8F6DDD98 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + DB6D020E7BD00B83208B9A03 /* 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 = ""; }; + F5AC35C52992172B47892D71 /* 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 = ""; }; + F606F420C75B7A999C3003F1 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80EE294D02FB00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 74D02B72575A8C8A1492AE70 /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1200A014D953763B34535EB3 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80F2294D02FB00263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80F3294D02FB00263BE5 /* RunnerTests.m */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 3D1BD31C0A91C924BAF12C36 /* Frameworks */ = { + isa = PBXGroup; + children = ( + CB8157ECFBC61BAC8F6DDD98 /* libPods-Runner.a */, + BF91C3F82401F1EC86B8BDBB /* libPods-RunnerTests.a */, + ); + name = Frameworks; + 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 */, + 331C80F2294D02FB00263BE5 /* RunnerTests */, + 97C146EF1CF9000F007C117D /* Products */, + F4667D52B031A4D8AAF7780D /* Pods */, + 3D1BD31C0A91C924BAF12C36 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + F4667D52B031A4D8AAF7780D /* Pods */ = { + isa = PBXGroup; + children = ( + C4F40650D87222AA05FBB409 /* Pods-Runner.debug.xcconfig */, + AA2F596DEE97CE584753926C /* Pods-Runner.release.xcconfig */, + F5AC35C52992172B47892D71 /* Pods-Runner.profile.xcconfig */, + DB6D020E7BD00B83208B9A03 /* Pods-RunnerTests.debug.xcconfig */, + F606F420C75B7A999C3003F1 /* Pods-RunnerTests.release.xcconfig */, + 0733CEEBA4D517AC13DB7032 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80F0294D02FB00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80F7294D02FB00263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + AFF8B152CD6A1DD5D70EC498 /* [CP] Check Pods Manifest.lock */, + 331C80ED294D02FB00263BE5 /* Sources */, + 331C80EE294D02FB00263BE5 /* Frameworks */, + 331C80EF294D02FB00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80F6294D02FB00263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80F1294D02FB00263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 99BA78E65A21CE04C8370CA7 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80F0294D02FB00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C80F0294D02FB00263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80EF294D02FB00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 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"; + }; + 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"; + }; + 99BA78E65A21CE04C8370CA7 /* [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; + }; + AFF8B152CD6A1DD5D70EC498 /* [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 */ + 331C80ED294D02FB00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80F4294D02FB00263BE5 /* RunnerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80F6294D02FB00263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C80F5294D02FB00263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 979UK3QQJ3; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPaidExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C80F8294D02FB00263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = DB6D020E7BD00B83208B9A03 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPaidExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C80F9294D02FB00263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F606F420C75B7A999C3003F1 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPaidExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C80FA294D02FB00263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0733CEEBA4D517AC13DB7032 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPaidExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + 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 = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 979UK3QQJ3; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPaidExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 979UK3QQJ3; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.flutterPaidExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80F7294D02FB00263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80F8294D02FB00263BE5 /* Debug */, + 331C80F9294D02FB00263BE5 /* Release */, + 331C80FA294D02FB00263BE5 /* 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/packages/flutter_paid/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/flutter_paid/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/flutter_paid/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter_paid/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/flutter_paid/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/flutter_paid/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/flutter_paid/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/flutter_paid/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..3d0fb00 --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_paid/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/flutter_paid/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/packages/flutter_paid/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/flutter_paid/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/flutter_paid/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/flutter_paid/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/flutter_paid/example/ios/Runner/AppDelegate.h b/packages/flutter_paid/example/ios/Runner/AppDelegate.h new file mode 100644 index 0000000..36e21bb --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner/AppDelegate.h @@ -0,0 +1,6 @@ +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/flutter_paid/example/ios/Runner/AppDelegate.m b/packages/flutter_paid/example/ios/Runner/AppDelegate.m new file mode 100644 index 0000000..70e8393 --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner/AppDelegate.m @@ -0,0 +1,13 @@ +#import "AppDelegate.h" +#import "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/packages/flutter_paid/example/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/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/flutter_paid/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/flutter_paid/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_paid/example/ios/Runner/Base.lproj/Main.storyboard b/packages/flutter_paid/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/flutter_paid/example/ios/Runner/Info.plist b/packages/flutter_paid/example/ios/Runner/Info.plist new file mode 100644 index 0000000..4ea74ef --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Flutter Paid + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + flutter_paid_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/packages/flutter_paid/example/ios/Runner/main.m b/packages/flutter_paid/example/ios/Runner/main.m new file mode 100644 index 0000000..dff6597 --- /dev/null +++ b/packages/flutter_paid/example/ios/Runner/main.m @@ -0,0 +1,9 @@ +#import +#import +#import "AppDelegate.h" + +int main(int argc, char* argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/packages/flutter_paid/example/ios/RunnerTests/RunnerTests.m b/packages/flutter_paid/example/ios/RunnerTests/RunnerTests.m new file mode 100644 index 0000000..28c02d0 --- /dev/null +++ b/packages/flutter_paid/example/ios/RunnerTests/RunnerTests.m @@ -0,0 +1,33 @@ +#import +#import +#import + +@import flutter_paid; + +// This demonstrates a simple unit test of the Objective-C portion of this plugin's implementation. +// +// See https://developer.apple.com/documentation/xctest for more information about using XCTest. + +@interface RunnerTests : XCTestCase + +@end + +@implementation RunnerTests + +- (void)testExample { + FlutterPaidPlugin *plugin = [[FlutterPaidPlugin alloc] init]; + + FlutterMethodCall *call = [FlutterMethodCall methodCallWithMethodName:@"getPlatformVersion" + arguments:nil]; + XCTestExpectation *expectation = [self expectationWithDescription:@"result block must be called"]; + [plugin handleMethodCall:call + result:^(id result) { + NSString *expected = [NSString + stringWithFormat:@"iOS %@", UIDevice.currentDevice.systemVersion]; + XCTAssertEqualObjects(result, expected); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:1 handler:nil]; +} + +@end diff --git a/packages/flutter_paid/example/lib/main.dart b/packages/flutter_paid/example/lib/main.dart new file mode 100644 index 0000000..1cc2ab7 --- /dev/null +++ b/packages/flutter_paid/example/lib/main.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_paid/flutter_paid.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + String _paid = 'Unknown'; + final _flutterPaidPlugin = FlutterPaid(); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + String platformVersion; + String paid = "Unknown"; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + try { + platformVersion = await _flutterPaidPlugin.getPlatformVersion() ?? + 'Unknown platform version'; + paid = await _flutterPaidPlugin.paid() ?? 'Unknown paid'; + } on PlatformException { + platformVersion = 'Failed to get platform version.'; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + _paid = paid; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Column( + children: [ + Text('Running on: $_platformVersion\n'), + Text('Paid: $_paid\n'), + ], + ), + ), + ), + ); + } +} diff --git a/packages/flutter_paid/example/pubspec.lock b/packages/flutter_paid/example/pubspec.lock new file mode 100644 index 0000000..327242d --- /dev/null +++ b/packages/flutter_paid/example/pubspec.lock @@ -0,0 +1,283 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_paid: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + url: "https://pub.dev" + source: hosted + version: "3.0.3" +sdks: + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.3.0" diff --git a/packages/flutter_paid/example/pubspec.yaml b/packages/flutter_paid/example/pubspec.yaml new file mode 100644 index 0000000..54d0d08 --- /dev/null +++ b/packages/flutter_paid/example/pubspec.yaml @@ -0,0 +1,85 @@ +name: flutter_paid_example +description: "Demonstrates how to use the flutter_paid plugin." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: '>=3.3.0 <4.0.0' + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + flutter_paid: + # When depending on this package from a real application you should use: + # flutter_paid: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.6 + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^3.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/flutter_paid/example/test/widget_test.dart b/packages/flutter_paid/example/test/widget_test.dart new file mode 100644 index 0000000..76feab0 --- /dev/null +++ b/packages/flutter_paid/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// 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:flutter_paid_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/packages/flutter_paid/ios/.gitignore b/packages/flutter_paid/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/packages/flutter_paid/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/flutter_paid/ios/Assets/.gitkeep b/packages/flutter_paid/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/packages/flutter_paid/ios/Classes/FlutterPaidPlugin.h b/packages/flutter_paid/ios/Classes/FlutterPaidPlugin.h new file mode 100644 index 0000000..3689b96 --- /dev/null +++ b/packages/flutter_paid/ios/Classes/FlutterPaidPlugin.h @@ -0,0 +1,4 @@ +#import + +@interface FlutterPaidPlugin : NSObject +@end diff --git a/packages/flutter_paid/ios/Classes/FlutterPaidPlugin.m b/packages/flutter_paid/ios/Classes/FlutterPaidPlugin.m new file mode 100644 index 0000000..62e27a0 --- /dev/null +++ b/packages/flutter_paid/ios/Classes/FlutterPaidPlugin.m @@ -0,0 +1,105 @@ +#import "FlutterPaidPlugin.h" +#import +#import +#import + +@interface NSString (Paid) ++ (NSString *)paid; +@end + + +@implementation FlutterPaidPlugin ++ (void)registerWithRegistrar:(NSObject*)registrar { + FlutterMethodChannel* channel = [FlutterMethodChannel + methodChannelWithName:@"flutter_paid" + binaryMessenger:[registrar messenger]]; + FlutterPaidPlugin* instance = [[FlutterPaidPlugin alloc] init]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { + if ([@"getPlatformVersion" isEqualToString:call.method]) { + result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]); + } else if ([@"paid" isEqualToString:call.method]) { + result([NSString paid]); + } + else { + result(FlutterMethodNotImplemented); + } +} + +@end + + + +time_t bootSecTime(NSString *abc){ + struct timeval boottime; + size_t len = sizeof(boottime); + int mib[2] = { CTL_KERN, KERN_BOOTTIME }; + if( sysctl(mib, 2, &boottime, &len, NULL, 0) < 0 ) { + return 0; + } + return boottime.tv_sec; +} + + + +@implementation NSString (Paid) +- (NSString *)MD5 +{ + const char *cStr = [self UTF8String]; + unsigned char result[CC_MD5_DIGEST_LENGTH]; + CC_MD5( cStr, (int)strlen(cStr), result ); // This is the md5 call + return [NSString stringWithFormat: + @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + result[0], result[1], result[2], result[3], + result[4], result[5], result[6], result[7], + result[8], result[9], result[10], result[11], + result[12], result[13], result[14], result[15] + ]; +} + ++ (NSString *)paid { + NSString *paid1 = [self getFileTime]; + NSString *paid2 = [self getSysU]; + NSString *paid3 = [self bootTimeInSec]; + + NSString *result = [NSString stringWithFormat:@"%@-%@-%@", [paid1 MD5], [paid2 MD5], [paid3 MD5]]; +// NSLog(@"---PAID----\n%@", result); + return result; +} + ++ (NSString *)getFileTime { + struct stat info; + int result = stat("/var/mobile", &info); + if (result!=0) { + return @""; + } + struct timespec time = info.st_birthtimespec; + return [NSString stringWithFormat:@"%ld.%09ld", time.tv_sec, time.tv_nsec]; +} + + ++(NSString *)getSysU { + NSString *result = nil; + NSString *information = @"L3Zhci9tb2JpbGUvTGlicmFyeS9Vc2VyQ29uZmlndXJhdGlvblByb2ZpbGVzL1B1YmxpY0luZm8vTUNNZXRhLnBsaXN0"; + NSData *data=[[NSData alloc]initWithBase64EncodedString:information options:0]; + NSString *dataString = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; + NSError *error = nil; + NSDictionary *fileAttributes = [[NSFileManager defaultManager] attributesOfItemAtPath:dataString error:&error]; + if (fileAttributes) { + id singleAttibute = [fileAttributes objectForKey:NSFileCreationDate]; + if ([singleAttibute isKindOfClass:[NSDate class]]) { + NSDate *dataDate = singleAttibute; + result = [NSString stringWithFormat:@"%.6f",[dataDate timeIntervalSince1970]]; + } + } + return result; +} + + ++(NSString *)bootTimeInSec { + return [NSString stringWithFormat:@"%ld", bootSecTime(@"")]; +} + +@end diff --git a/packages/flutter_paid/ios/flutter_paid.podspec b/packages/flutter_paid/ios/flutter_paid.podspec new file mode 100644 index 0000000..d8dcd92 --- /dev/null +++ b/packages/flutter_paid/ios/flutter_paid.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint flutter_paid.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'flutter_paid' + s.version = '0.0.1' + s.summary = 'A new Flutter plugin project.' + s.description = <<-DESC +A new Flutter plugin project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } +end diff --git a/packages/flutter_paid/lib/flutter_paid.dart b/packages/flutter_paid/lib/flutter_paid.dart new file mode 100644 index 0000000..30fed9b --- /dev/null +++ b/packages/flutter_paid/lib/flutter_paid.dart @@ -0,0 +1,11 @@ +import 'flutter_paid_platform_interface.dart'; + +class FlutterPaid { + Future getPlatformVersion() { + return FlutterPaidPlatform.instance.getPlatformVersion(); + } + + Future paid() { + return FlutterPaidPlatform.instance.paid(); + } +} diff --git a/packages/flutter_paid/lib/flutter_paid_method_channel.dart b/packages/flutter_paid/lib/flutter_paid_method_channel.dart new file mode 100644 index 0000000..538aad2 --- /dev/null +++ b/packages/flutter_paid/lib/flutter_paid_method_channel.dart @@ -0,0 +1,23 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'flutter_paid_platform_interface.dart'; + +/// An implementation of [FlutterPaidPlatform] that uses method channels. +class MethodChannelFlutterPaid extends FlutterPaidPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('flutter_paid'); + + @override + Future getPlatformVersion() async { + final version = await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } + + @override + Future paid() async { + final paid = await methodChannel.invokeMethod('paid'); + return paid; + } +} diff --git a/packages/flutter_paid/lib/flutter_paid_platform_interface.dart b/packages/flutter_paid/lib/flutter_paid_platform_interface.dart new file mode 100644 index 0000000..3e1172e --- /dev/null +++ b/packages/flutter_paid/lib/flutter_paid_platform_interface.dart @@ -0,0 +1,32 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'flutter_paid_method_channel.dart'; + +abstract class FlutterPaidPlatform extends PlatformInterface { + /// Constructs a FlutterPaidPlatform. + FlutterPaidPlatform() : super(token: _token); + + static final Object _token = Object(); + + static FlutterPaidPlatform _instance = MethodChannelFlutterPaid(); + + /// The default instance of [FlutterPaidPlatform] to use. + /// + /// Defaults to [MethodChannelFlutterPaid]. + static FlutterPaidPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [FlutterPaidPlatform] when + /// they register themselves. + static set instance(FlutterPaidPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } + Future paid() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/packages/flutter_paid/pubspec.yaml b/packages/flutter_paid/pubspec.yaml new file mode 100644 index 0000000..7f4a62d --- /dev/null +++ b/packages/flutter_paid/pubspec.yaml @@ -0,0 +1,69 @@ +name: flutter_paid +description: "A new Flutter plugin project." +version: 0.0.1 +homepage: + +environment: + sdk: '>=3.3.0 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + ios: + pluginClass: FlutterPaidPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/flutter_paid/test/flutter_paid_method_channel_test.dart b/packages/flutter_paid/test/flutter_paid_method_channel_test.dart new file mode 100644 index 0000000..4aa02be --- /dev/null +++ b/packages/flutter_paid/test/flutter_paid_method_channel_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_paid/flutter_paid_method_channel.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + MethodChannelFlutterPaid platform = MethodChannelFlutterPaid(); + const MethodChannel channel = MethodChannel('flutter_paid'); + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + (MethodCall methodCall) async { + return '42'; + }, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); + }); + + test('getPlatformVersion', () async { + expect(await platform.getPlatformVersion(), '42'); + }); +} diff --git a/packages/flutter_paid/test/flutter_paid_test.dart b/packages/flutter_paid/test/flutter_paid_test.dart new file mode 100644 index 0000000..88e9a19 --- /dev/null +++ b/packages/flutter_paid/test/flutter_paid_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_paid/flutter_paid.dart'; +import 'package:flutter_paid/flutter_paid_platform_interface.dart'; +import 'package:flutter_paid/flutter_paid_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockFlutterPaidPlatform + with MockPlatformInterfaceMixin + implements FlutterPaidPlatform { + + @override + Future getPlatformVersion() => Future.value('42'); +} + +void main() { + final FlutterPaidPlatform initialPlatform = FlutterPaidPlatform.instance; + + test('$MethodChannelFlutterPaid is the default instance', () { + expect(initialPlatform, isInstanceOf()); + }); + + test('getPlatformVersion', () async { + FlutterPaid flutterPaidPlugin = FlutterPaid(); + MockFlutterPaidPlatform fakePlatform = MockFlutterPaidPlatform(); + FlutterPaidPlatform.instance = fakePlatform; + + expect(await flutterPaidPlugin.getPlatformVersion(), '42'); + }); +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..2d7edfb --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1501 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.dev" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: f4ad0fea5f102201015c9aae9d93bc02f75dd9491529a8c21f88d17a8523d44c + url: "https://pub.dev" + source: hosted + version: "7.6.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: a5ab7590c27b779f3d4de67f31c4109dbe13dd7339f86461a6f2a8ab2594d8ce + url: "https://pub.dev" + source: hosted + version: "0.13.4" + android_id: + dependency: "direct main" + description: + path: "packages/android_id" + relative: true + source: path + version: "0.4.0" + android_oaid: + dependency: "direct main" + description: + path: "packages/android_oaid" + relative: true + source: path + version: "0.4.0" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + boxy: + dependency: transitive + description: + name: boxy + sha256: "42ccafe13b2893878042acc5b7e2446025328e11a3197b0bb78db42ff76aa3f0" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + build: + dependency: transitive + description: + name: build + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + url: "https://pub.dev" + source: hosted + version: "8.12.6" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + custom_lint: + dependency: "direct dev" + description: + name: custom_lint + sha256: "9656925637516c5cf0f5da018b33df94025af2088fe09c8ae2ca54c53f2d9a84" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: "6cdc8e87e51baaaba9c43e283ed8d28e59a0c4732279df62f66f7b5984655414" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" + url: "https://pub.dev" + source: hosted + version: "0.7.5" + custom_lint_visitor: + dependency: transitive + description: + name: custom_lint_visitor + sha256: "4a86a0d8415a91fbb8298d6ef03e9034dc8e323a599ddc4120a0e36c433983a2" + url: "https://pub.dev" + source: hosted + version: "1.0.0+7.7.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" + url: "https://pub.dev" + source: hosted + version: "11.5.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.dev" + source: hosted + version: "7.0.3" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + extended_image: + dependency: transitive + description: + name: extended_image + sha256: f6cbb1d798f51262ed1a3d93b4f1f2aa0d76128df39af18ecb77fa740f88b2e0 + url: "https://pub.dev" + source: hosted + version: "10.0.1" + extended_image_library: + dependency: transitive + description: + name: extended_image_library + sha256: "1f9a24d3a00c2633891c6a7b5cab2807999eb2d5b597e5133b63f49d113811fe" + url: "https://pub.dev" + source: hosted + version: "5.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + fancy_dio_inspector: + dependency: "direct main" + description: + name: fancy_dio_inspector + sha256: "7c13cf97a595d0a5b14746a400a72a20ed174990c454995995c9fe31433fe6ab" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: "direct main" + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_animate: + dependency: transitive + description: + name: flutter_animate + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" + url: "https://pub.dev" + source: hosted + version: "4.5.2" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: d41da11fb497314fbf89811ec30af02d1d898b47980a129f0a8c0a1720460ba2 + url: "https://pub.dev" + source: hosted + version: "6.0.1" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: "8ae1f090e5f4ef5cfa6670ce1ab5dddadd33f3533a7f9ba19d9f958aa2a89f42" + url: "https://pub.dev" + source: hosted + version: "0.21.3+1" + flutter_html: + dependency: "direct main" + description: + name: flutter_html + sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_markdown_plus: + dependency: "direct main" + description: + name: flutter_markdown_plus + sha256: "039177906850278e8fb1cd364115ee0a46281135932fa8ecea8455522166d2de" + url: "https://pub.dev" + source: hosted + version: "1.0.7" + flutter_native_splash: + dependency: "direct dev" + description: + name: flutter_native_splash + sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002" + url: "https://pub.dev" + source: hosted + version: "2.4.7" + flutter_paid: + dependency: "direct main" + description: + path: "packages/flutter_paid" + relative: true + source: path + version: "0.0.1" + flutter_remix: + dependency: "direct main" + description: + name: flutter_remix + sha256: c098f0a0268b06bf4948080d66ca3410aee11441b40fc761522222a9523e3047 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + flutter_riverpod: + dependency: transitive + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "7686b1d6a29985dcbb808c59518226e603e3bfa7c0ddfd1a0d00e4cda77c868e" + url: "https://pub.dev" + source: hosted + version: "10.3.1" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: "82329fa5cdf343773b1b6897dea959105a29f092454259edff92f9f6637e8149" + url: "https://pub.dev" + source: hosted + version: "0.3.2" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: a5f35ddab43cf5c8215d2feb4ce1957851f28c5c37e6f04335066a0602087bf5 + url: "https://pub.dev" + source: hosted + version: "3.0.1" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + flutter_shaders: + dependency: transitive + description: + name: flutter_shaders + sha256: "34794acadd8275d971e02df03afee3dee0f98dbfb8c4837082ad0034f612a3e2" + url: "https://pub.dev" + source: hosted + version: "0.1.3" + flutter_svg: + dependency: transitive + description: + name: flutter_svg + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: "144ddd74d49c865eba47abe31cbc746c7b311c82d6c32e571fd73c4264b740e2" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: d8f590a69729f719177ea68eb1e598295e8dbc41bbc247fed78b2c8a25660d7c + url: "https://pub.dev" + source: hosted + version: "16.3.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: "70bba33cfc5670c84b796e6929c54b8bc5be7d0fe15bb28c2560500b9ad06966" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: "66871df468fc24eee81f1a0a7cb98acc104716f9b7376d355437b48d633c4ebf" + url: "https://pub.dev" + source: hosted + version: "4.4.0" + html: + dependency: transitive + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://pub.dev" + source: hosted + version: "0.15.6" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_client_helper: + dependency: transitive + description: + name: http_client_helper + sha256: "8a9127650734da86b5c73760de2b404494c968a3fd55602045ffec789dac3cb1" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + icons_launcher: + dependency: "direct dev" + description: + name: icons_launcher + sha256: "6317d56a73ee528f1dd570d7cd7be120ce58014e0fe635d141ada3d88782f58d" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" + intl: + dependency: transitive + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + js: + dependency: transitive + description: + name: js + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" + url: "https://pub.dev" + source: hosted + version: "4.12.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + lucide_icons_flutter: + dependency: transitive + description: + name: lucide_icons_flutter + sha256: "7c5dc01a32a9905ae34e2d84224e92d6d0c42acf8926df9e01c35a1446bf1b69" + url: "https://pub.dev" + source: hosted + version: "3.1.14+2" + markdown: + dependency: transitive + description: + name: markdown + sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9 + url: "https://pub.dev" + source: hosted + version: "7.3.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + platform_metadata: + dependency: "direct main" + description: + path: "." + ref: f381964 + resolved-ref: f3819645c70c02b878c754a61ef9ebd3421f2127 + url: "https://github.com/maoios/platform_metadata.git" + source: git + version: "0.0.2+2" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" + protobuf: + dependency: "direct main" + description: + name: protobuf + sha256: de9c9eb2c33f8e933a42932fe1dc504800ca45ebc3d673e6ed7f39754ee4053e + url: "https://pub.dev" + source: hosted + version: "4.2.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + retrofit: + dependency: "direct main" + description: + name: retrofit + sha256: "84063c18a00d55af41d6b8401edf8473e8c215bd7068ef7ec5e34c60657ffdbe" + url: "https://pub.dev" + source: hosted + version: "4.9.1" + retrofit_generator: + dependency: "direct dev" + description: + name: retrofit_generator + sha256: "9abcf21acb95bf7040546eafff87f60cf0aee20b05101d71f99876fc4df1f522" + url: "https://pub.dev" + source: hosted + version: "9.7.0" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_analyzer_utils: + dependency: transitive + description: + name: riverpod_analyzer_utils + sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" + url: "https://pub.dev" + source: hosted + version: "0.5.10" + riverpod_annotation: + dependency: "direct main" + description: + name: riverpod_annotation + sha256: e14b0bf45b71326654e2705d462f21b958f987087be850afd60578fcd502d1b8 + url: "https://pub.dev" + source: hosted + version: "2.6.1" + riverpod_generator: + dependency: "direct dev" + description: + name: riverpod_generator + sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36" + url: "https://pub.dev" + source: hosted + version: "2.6.5" + riverpod_lint: + dependency: "direct dev" + description: + name: riverpod_lint + sha256: "89a52b7334210dbff8605c3edf26cfe69b15062beed5cbfeff2c3812c33c9e35" + url: "https://pub.dev" + source: hosted + version: "2.6.5" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + serial_csv: + dependency: transitive + description: + name: serial_csv + sha256: "2d62bb70cb3ce7251383fc86ea9aae1298ab1e57af6ef4e93b6a9751c5c268dd" + url: "https://pub.dev" + source: hosted + version: "0.5.2" + shadcn_ui: + dependency: "direct main" + description: + name: shadcn_ui + sha256: "6c06f2bcebd8734b9ed0bf3f63ef5c71981573d5664923589b0302f8280e7eaf" + url: "https://pub.dev" + source: hosted + version: "0.53.6" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + shorebird_code_push: + dependency: "direct main" + description: + name: shorebird_code_push + sha256: "55d5eea098a4293199d22dfd263ce1d6c10820ceb42c2acb950f971a8bcee0ce" + url: "https://pub.dev" + source: hosted + version: "2.0.6" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + slang: + dependency: transitive + description: + name: slang + sha256: "46e929158c2f563994c4d1fce5819cfa13e18b164941473d2553bcddcf387c31" + url: "https://pub.dev" + source: hosted + version: "4.15.0" + slang_flutter: + dependency: transitive + description: + name: slang_flutter + sha256: "0eb6348416a296f1bd940fe02669bcd2df5c5cfdabf893b98e448df8b7ecf4ac" + url: "https://pub.dev" + source: hosted + version: "4.15.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + theme_extensions_builder_annotation: + dependency: transitive + description: + name: theme_extensions_builder_annotation + sha256: "75f28ac85d396d143d111a47c1395b01f3be41b7135f37bd51512921944e4206" + url: "https://pub.dev" + source: hosted + version: "7.3.0" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + two_dimensional_scrollables: + dependency: transitive + description: + name: two_dimensional_scrollables + sha256: "4f25bd42783626c5f2810333418727455397195acb61e53710e638a6a98e0e5e" + url: "https://pub.dev" + source: hosted + version: "0.5.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + universal_image: + dependency: transitive + description: + name: universal_image + sha256: "2eae13df84a47960cc4148fec88f09031ea64cbf667b4131ff2c907dd7b7c6d1" + url: "https://pub.dev" + source: hosted + version: "1.0.12" + universal_io: + dependency: transitive + description: + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + url: "https://pub.dev" + source: hosted + version: "2.3.1" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + url: "https://pub.dev" + source: hosted + version: "6.3.29" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad + url: "https://pub.dev" + source: hosted + version: "6.3.6" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.dev" + source: hosted + version: "3.2.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" + url: "https://pub.dev" + source: hosted + version: "3.2.5" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: b9b3f391857781aa96acacef96066f2f49b4cd03cf9fce3ca4d8da2ef5ea129e + url: "https://pub.dev" + source: hosted + version: "1.2.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + video_player: + dependency: "direct main" + description: + name: video_player + sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf" + url: "https://pub.dev" + source: hosted + version: "2.10.1" + video_player_android: + dependency: transitive + description: + name: video_player_android + sha256: "877a6c7ba772456077d7bfd71314629b3fe2b73733ce503fc77c3314d43a0ca0" + url: "https://pub.dev" + source: hosted + version: "2.9.5" + video_player_avfoundation: + dependency: transitive + description: + name: video_player_avfoundation + sha256: d1eb970495a76abb35e5fa93ee3c58bd76fb6839e2ddf2fbb636674f2b971dd4 + url: "https://pub.dev" + source: hosted + version: "2.8.9" + video_player_platform_interface: + dependency: transitive + description: + name: video_player_platform_interface + sha256: "16eaed5268c571c31840dc58ef8da5f0cd4db2a98490c3b8f1cf70122546c6e0" + url: "https://pub.dev" + source: hosted + version: "6.7.0" + video_player_web: + dependency: transitive + description: + name: video_player_web + sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.2 <4.0.0" + flutter: ">=3.35.6" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..823d8ca --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,83 @@ +name: jinzhi_flutter_app +description: "金值 - 贵金属资产管家" +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ^3.9.2 + +dependencies: + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + # 状态管理 + hooks_riverpod: ^2.6.1 + riverpod_annotation: ^2.6.1 + flutter_hooks: ^0.21.3+1 + # 路由 + go_router: ^16.2.4 + # 网络 + 序列化 + dio: ^5.9.0 + retrofit: ">=4.6.0 <4.9.2" + protobuf: ^4.2.0 + fixnum: ^1.1.1 + # 存储 + flutter_secure_storage: ^10.2.0 + shared_preferences: ^2.5.3 + # 设备信息 + device_info_plus: ^11.3.3 + package_info_plus: ^8.3.0 + uuid: ^4.4.0 + # UI / 视觉 + shadcn_ui: ^0.53.6 + google_fonts: ^6.2.1 + flutter_remix: ^0.0.3 + shimmer: ^3.0.0 + flutter_markdown_plus: ^1.0.7 + video_player: ^2.9.2 # 启动页视频背景 + # 工具 + url_launcher: ^6.3.1 + fancy_dio_inspector: ^2.0.1 + cupertino_icons: ^1.0.8 + android_id: + path: packages/android_id + android_oaid: + path: packages/android_oaid + flutter_paid: + path: packages/flutter_paid + platform_metadata: + git: + # 待合并git的请求 + # url: https://github.com/zhouyuzhouyu/platform_metadata.git + # ref: 3e3b6f5 + url: https://github.com/maoios/platform_metadata.git + ref: f381964 + + flutter_dotenv: ^6.0.1 + fluttertoast: ^9.0.0 + shorebird_code_push: ^2.0.3 + flutter_html: ^3.0.0 + + + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.5.4 + retrofit_generator: ^9.7.0 + riverpod_generator: ^2.6.5 + custom_lint: ^0.7.0 + icons_launcher: ^3.0.1 + flutter_native_splash: ^2.4.6 + riverpod_lint: ^2.6.1 + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true + + assets: + - shorebird.yaml + - assets/env/.env + - assets/launch/ diff --git a/scripts/build_web.sh b/scripts/build_web.sh new file mode 100755 index 0000000..b212c50 --- /dev/null +++ b/scripts/build_web.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# 金值 Flutter web release 构建。产物输出到 build/web。 +# 参数(环境变量覆盖): +# API_BASE_URL 接口地址(默认生产 API) +# BASE_HREF base href(默认 /flutter/) +set -euo pipefail + +API_BASE_URL="${API_BASE_URL:-https://jinzhi.api.local}" +BASE_HREF="${BASE_HREF:-/flutter/}" + +echo "▶ flutter pub get" +flutter pub get +make runner + +echo "▶ flutter build web(base-href=${BASE_HREF},api=${API_BASE_URL})" +flutter build web --release \ + --base-href "$BASE_HREF" \ + --dart-define=JINZHI_API_BASE_URL="$API_BASE_URL" + +echo "✅ 构建完成 → build/web" diff --git a/scripts/deploy_web.sh b/scripts/deploy_web.sh new file mode 100755 index 0000000..9a0dcc1 --- /dev/null +++ b/scripts/deploy_web.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# 金值 Flutter web 版本化部署。 +# 产物须已在 build/web(先 flutter build web)。本脚本负责:打包 → scp → 远端解到 +# 带版本号的 rev 目录 → 原子切换 cur 软链。旧 rev 全部保留,便于随时回滚。 +# +# 用法:tool/deploy_web.sh +# rev_id 版本标识(CI 传 run_number;本地传时间戳) +# +# 连接参数(环境变量覆盖;CI 由 secrets 注入): +# DEPLOY_HOST / DEPLOY_USER / DEPLOY_PORT / WORKPATH +set -euo pipefail + +REV="${1:?用法: tool/deploy_web.sh }" + +DEPLOY_HOST="${DEPLOY_HOST:-47.114.117.126}" +DEPLOY_USER="${DEPLOY_USER:-dev}" +DEPLOY_PORT="${DEPLOY_PORT:-59650}" +WORKPATH="${WORKPATH:-/opt/modules/front/jinzhi/flutter}" + +SSH_OPTS="-o StrictHostKeyChecking=accept-new -p $DEPLOY_PORT" +SCP_OPTS="-o StrictHostKeyChecking=accept-new -P $DEPLOY_PORT" # scp 端口是大写 -P +BUILD_DIR="build/web" +PKG="jinzhi-web-$REV.tar.gz" + +[ -d "$BUILD_DIR" ] || { echo "✗ 缺少 $BUILD_DIR,请先 flutter build web"; exit 1; } + +echo "▶ 打包 $BUILD_DIR → $PKG" +tar -C "$BUILD_DIR" -czf "$PKG" . + +echo "▶ scp → $DEPLOY_USER@$DEPLOY_HOST:/tmp/$PKG" +scp $SCP_OPTS "$PKG" "$DEPLOY_USER@$DEPLOY_HOST:/tmp/$PKG" + +echo "▶ 远端:解到 rev_${REV}、切 cur(旧 rev 全部保留)" +# shellcheck disable=SC2087 +ssh $SSH_OPTS "$DEPLOY_USER@$DEPLOY_HOST" \ + "REV='$REV' WORKPATH='$WORKPATH' PKG='$PKG' bash -s" <<'REMOTE' +set -euo pipefail +REVDIR="$WORKPATH/core/revs/rev_$REV" +mkdir -p "$REVDIR" +tar -C "$REVDIR" -xzf "/tmp/$PKG" +rm -f "/tmp/$PKG" +# 原子切换:-f 覆盖、-n 不跟随已存在的软链目录 +ln -sfn "$REVDIR" "$WORKPATH/cur" +echo "OK: cur -> rev_$REV,现存版本:$(ls -1d "$WORKPATH"/core/revs/rev_* | sort -r | tr '\n' ' ')" +REMOTE + +rm -f "$PKG" +echo "✅ 已部署 rev_$REV → https://jinzhi.h5.local/flutter/(no-cache,刷新即新版)" diff --git a/shorebird.yaml b/shorebird.yaml new file mode 100644 index 0000000..b97718f --- /dev/null +++ b/shorebird.yaml @@ -0,0 +1,15 @@ +# This file is used to configure the Shorebird updater used by your app. +# Learn more at https://docs.shorebird.dev +# This file does not contain any sensitive information and should be checked into version control. + +# Your app_id is the unique identifier assigned to your app. +# It is used to identify your app when requesting patches from Shorebird's servers. +# It is not a secret and can be shared publicly. +app_id: abf382bb-3bff-4d3a-9bb5-86d5bc9aab92 + +# auto_update controls if Shorebird should automatically update in the background on launch. +# If auto_update: false, you will need to use package:shorebird_code_push to trigger updates. +# https://pub.dev/packages/shorebird_code_push +# Uncomment the following line to disable automatic updates. +# auto_update: false +base_url: https://shorebird.oss.neuronlabs.art diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..64e7f6d --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('Placeholder smoke test', (WidgetTester tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold(body: Center(child: Text('jinzhi'))), + ), + ); + expect(find.text('jinzhi'), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..fae0fd2 --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + 金值 + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..e6cb083 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "金值", + "short_name": "金值", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "贵金属资产管家", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}