diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# 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/ +.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..042fa3e --- /dev/null +++ b/.metadata @@ -0,0 +1,48 @@ +# 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: "6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef" + channel: "[user-branch]" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + base_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + - platform: android + create_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + base_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + - platform: ios + create_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + base_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + - platform: linux + create_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + base_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + - platform: macos + create_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + base_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + - platform: web + create_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + base_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + - platform: windows + create_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + base_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + - platform: ohos + create_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + base_revision: 6719fd2a7c56f183697c55b8a2ea5bcc2fea3cef + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a1319e --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# mom_kitchen + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..ef23fb3 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.mom_kitchen" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.mom_kitchen" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b0f69ff --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/example/mom_kitchen/MainActivity.kt b/android/app/src/main/kotlin/com/example/mom_kitchen/MainActivity.kt new file mode 100644 index 0000000..51799ab --- /dev/null +++ b/android/app/src/main/kotlin/com/example/mom_kitchen/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.mom_kitchen + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html new file mode 100644 index 0000000..3feb18a --- /dev/null +++ b/android/build/reports/problems/problems-report.html @@ -0,0 +1,663 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..fb605bc --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/docs/superpowers/plans/2026-04-08-refactor-implementation.md b/docs/superpowers/plans/2026-04-08-refactor-implementation.md new file mode 100644 index 0000000..c33470e --- /dev/null +++ b/docs/superpowers/plans/2026-04-08-refactor-implementation.md @@ -0,0 +1,1971 @@ +# 全面重构实施计划 + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 实现 GetX 全局状态管理、组件 PageStandards 集成、路由守卫和组件复用 + +**Architecture:** 使用 GetX 进行状态管理,所有组件使用 PageStandards 统一样式,实现路由守卫保护需要认证的页面 + +**Tech Stack:** Flutter, GetX, PageStandards, Cupertino (iOS 风格) + +--- + +## 文件结构 + +### 新增文件 + +``` +lib/src/ +├── controllers/ +│ ├── base/ +│ │ ├── base_controller.dart # 基础控制器 +│ │ └── paged_controller.dart # 分页控制器 +│ ├── home_controller.dart # 首页控制器 +│ ├── cart_controller.dart # 购物车控制器 +│ └── profile_controller.dart # 个人中心控制器 +├── widgets/ +│ ├── base/ +│ │ ├── standard_button.dart # 标准按钮 +│ │ ├── standard_text_field.dart # 标准输入框 +│ │ ├── standard_card.dart # 标准卡片 +│ │ └── standard_list_tile.dart # 标准列表项 +│ ├── interactive/ +│ │ ├── standard_dialog.dart # 标准对话框 +│ │ ├── standard_bottom_sheet.dart # 标准底部弹窗 +│ │ └── standard_picker.dart # 标准选择器 +│ └── states/ +│ ├── empty_state.dart # 空状态 +│ └── error_state.dart # 错误状态 +└── standards/ + └── route_guard.dart # 路由守卫 +``` + +### 修改文件 + +``` +lib/ +├── src/ +│ ├── pages/ +│ │ ├── home_page.dart # 使用 HomeController +│ │ ├── example_page.dart # 使用 PageStandards +│ │ └── theme_demo_page.dart # 使用控制器 +│ ├── widgets/ +│ │ ├── product_card.dart # 使用 PageStandards +│ │ ├── loading_indicator.dart # 使用 PageStandards +│ │ └── skeleton_loader.dart # 使用 PageStandards +│ └── standards/ +│ └── app_pages.dart # 添加 authLevel +└── main.dart # 集成路由守卫 +``` + +--- + +## Task 1: 创建基础控制器 + +**Files:** +- Create: `lib/src/controllers/base/base_controller.dart` + +- [ ] **Step 1: 创建 BaseController** + +```dart +import 'package:get/get.dart'; +import 'package:mom_kitchen/src/services/app_service.dart'; +import 'package:mom_kitchen/src/utils/app_logger.dart'; + +abstract class BaseController extends GetxController { + final isLoading = false.obs; + final errorMessage = ''.obs; + + Future runWithLoading(Future Function() action) async { + isLoading.value = true; + errorMessage.value = ''; + try { + await action(); + } catch (e) { + errorMessage.value = e.toString(); + AppLogger.e('Controller error: $e'); + } finally { + isLoading.value = false; + } + } + + void clearError() { + errorMessage.value = ''; + } +} +``` + +- [ ] **Step 2: 创建 PagedController** + +**Files:** +- Create: `lib/src/controllers/base/paged_controller.dart` + +```dart +import 'package:get/get.dart'; +import 'package:mom_kitchen/src/controllers/base/base_controller.dart'; + +abstract class PagedController extends BaseController { + final items = [].obs; + final currentPage = 1.obs; + final hasMore = true.obs; + final pageSize = 20; + + Future> fetchPage(int page); + + Future loadMore() async { + if (!hasMore.value || isLoading.value) return; + + await runWithLoading(() async { + final newItems = await fetchPage(currentPage.value + 1); + if (newItems.isEmpty) { + hasMore.value = false; + } else { + items.addAll(newItems); + currentPage.value++; + } + }); + } + + Future refresh() async { + currentPage.value = 1; + hasMore.value = true; + items.clear(); + await loadMore(); + } + + void clear() { + items.clear(); + currentPage.value = 1; + hasMore.value = true; + } +} +``` + +- [ ] **Step 3: 提交基础控制器** + +```bash +git add lib/src/controllers/base/ +git commit -m "feat: add base controllers for GetX state management" +``` + +--- + +## Task 2: 创建首页控制器 + +**Files:** +- Create: `lib/src/controllers/home_controller.dart` + +- [ ] **Step 1: 创建 HomeController** + +```dart +import 'package:get/get.dart'; +import 'package:mom_kitchen/src/controllers/base/base_controller.dart'; + +class ProductModel { + final String name; + final double price; + final String image; + final String category; + + ProductModel({ + required this.name, + required this.price, + required this.image, + required this.category, + }); + + factory ProductModel.fromMap(Map map) { + return ProductModel( + name: map['name'] ?? '', + price: (map['price'] as num?)?.toDouble() ?? 0.0, + image: map['image'] ?? '📦', + category: map['category'] ?? '', + ); + } + + Map toMap() { + return { + 'name': name, + 'price': price, + 'image': image, + 'category': category, + }; + } +} + +class HomeController extends BaseController { + final products = [].obs; + final categories = [].obs; + final selectedCategory = ''.obs; + final searchQuery = ''.obs; + + @override + void onInit() { + super.onInit(); + loadProducts(); + } + + Future loadProducts() async { + await runWithLoading(() async { + await Future.delayed(const Duration(milliseconds: 500)); + + final mockProducts = [ + ProductModel(name: 'Organic Apples', price: 4.99, image: '🍎', category: 'Fruits'), + ProductModel(name: 'Fresh Milk', price: 3.49, image: '🥛', category: 'Dairy'), + ProductModel(name: 'Whole Wheat Bread', price: 2.99, image: '🍞', category: 'Bakery'), + ProductModel(name: 'Free Range Eggs', price: 5.99, image: '🥚', category: 'Dairy'), + ProductModel(name: 'Organic Tomatoes', price: 3.99, image: '🍅', category: 'Vegetables'), + ProductModel(name: 'Fresh Salmon', price: 12.99, image: '🐟', category: 'Seafood'), + ]; + + products.value = mockProducts; + + final categorySet = {}; + for (var product in mockProducts) { + categorySet.add(product.category); + } + categories.value = categorySet.toList(); + }); + } + + List get filteredProducts { + var result = products.toList(); + + if (selectedCategory.value.isNotEmpty) { + result = result.where((p) => p.category == selectedCategory.value).toList(); + } + + if (searchQuery.value.isNotEmpty) { + result = result.where((p) => + p.name.toLowerCase().contains(searchQuery.value.toLowerCase()) + ).toList(); + } + + return result; + } + + void selectCategory(String category) { + selectedCategory.value = category; + } + + void search(String query) { + searchQuery.value = query; + } + + void clearFilters() { + selectedCategory.value = ''; + searchQuery.value = ''; + } +} +``` + +- [ ] **Step 2: 提交首页控制器** + +```bash +git add lib/src/controllers/home_controller.dart +git commit -m "feat: add HomeController for state management" +``` + +--- + +## Task 3: 创建购物车控制器 + +**Files:** +- Create: `lib/src/controllers/cart_controller.dart` + +- [ ] **Step 1: 创建 CartController** + +```dart +import 'package:get/get.dart'; +import 'package:mom_kitchen/src/controllers/base/base_controller.dart'; +import 'package:mom_kitchen/src/controllers/home_controller.dart'; + +class CartItem { + final ProductModel product; + int quantity; + + CartItem({ + required this.product, + this.quantity = 1, + }); + + double get totalPrice => product.price * quantity; +} + +class CartController extends BaseController { + final cartItems = [].obs; + + @override + void onInit() { + super.onInit(); + } + + void addProduct(ProductModel product) { + final index = cartItems.indexWhere((item) => item.product.name == product.name); + if (index >= 0) { + cartItems[index].quantity++; + cartItems.refresh(); + } else { + cartItems.add(CartItem(product: product)); + } + } + + void removeProduct(String productName) { + cartItems.removeWhere((item) => item.product.name == productName); + } + + void updateQuantity(String productName, int quantity) { + final index = cartItems.indexWhere((item) => item.product.name == productName); + if (index >= 0) { + if (quantity <= 0) { + cartItems.removeAt(index); + } else { + cartItems[index].quantity = quantity; + cartItems.refresh(); + } + } + } + + void incrementQuantity(String productName) { + final index = cartItems.indexWhere((item) => item.product.name == productName); + if (index >= 0) { + cartItems[index].quantity++; + cartItems.refresh(); + } + } + + void decrementQuantity(String productName) { + final index = cartItems.indexWhere((item) => item.product.name == productName); + if (index >= 0) { + if (cartItems[index].quantity > 1) { + cartItems[index].quantity--; + cartItems.refresh(); + } else { + cartItems.removeAt(index); + } + } + } + + void clearCart() { + cartItems.clear(); + } + + double get totalPrice { + return cartItems.fold(0.0, (sum, item) => sum + item.totalPrice); + } + + int get totalItems { + return cartItems.fold(0, (sum, item) => sum + item.quantity); + } + + bool isInCart(String productName) { + return cartItems.any((item) => item.product.name == productName); + } + + int getQuantity(String productName) { + final index = cartItems.indexWhere((item) => item.product.name == productName); + return index >= 0 ? cartItems[index].quantity : 0; + } +} +``` + +- [ ] **Step 2: 提交购物车控制器** + +```bash +git add lib/src/controllers/cart_controller.dart +git commit -m "feat: add CartController for cart state management" +``` + +--- + +## Task 4: 创建个人中心控制器 + +**Files:** +- Create: `lib/src/controllers/profile_controller.dart` + +- [ ] **Step 1: 创建 ProfileController** + +```dart +import 'package:get/get.dart'; +import 'package:mom_kitchen/src/controllers/base/base_controller.dart'; +import 'package:mom_kitchen/src/services/app_service.dart'; + +class UserModel { + final String id; + final String name; + final String email; + final String? avatar; + + UserModel({ + required this.id, + required this.name, + required this.email, + this.avatar, + }); + + factory UserModel.empty() { + return UserModel( + id: '', + name: '', + email: '', + ); + } + + bool get isEmpty => id.isEmpty; + bool get isNotEmpty => id.isNotEmpty; +} + +class ProfileController extends BaseController { + final user = Rx(null); + final isLoggedIn = false.obs; + + @override + void onInit() { + super.onInit(); + checkLoginStatus(); + } + + Future checkLoginStatus() async { + await runWithLoading(() async { + final storage = AppService.instance.storage; + final userId = await storage.getString('user_id'); + if (userId != null && userId.isNotEmpty) { + final name = await storage.getString('user_name') ?? 'User'; + final email = await storage.getString('user_email') ?? ''; + user.value = UserModel( + id: userId, + name: name, + email: email, + ); + isLoggedIn.value = true; + } + }); + } + + Future login(String email, String password) async { + await runWithLoading(() async { + await Future.delayed(const Duration(seconds: 1)); + + final storage = AppService.instance.storage; + await storage.setString('user_id', 'user_123'); + await storage.setString('user_name', 'Test User'); + await storage.setString('user_email', email); + + user.value = UserModel( + id: 'user_123', + name: 'Test User', + email: email, + ); + isLoggedIn.value = true; + }); + } + + Future logout() async { + await runWithLoading(() async { + final storage = AppService.instance.storage; + await storage.remove('user_id'); + await storage.remove('user_name'); + await storage.remove('user_email'); + + user.value = null; + isLoggedIn.value = false; + }); + } + + Future updateProfile({String? name, String? avatar}) async { + if (user.value == null) return; + + await runWithLoading(() async { + await Future.delayed(const Duration(milliseconds: 500)); + + final storage = AppService.instance.storage; + + if (name != null) { + await storage.setString('user_name', name); + } + + user.value = UserModel( + id: user.value!.id, + name: name ?? user.value!.name, + email: user.value!.email, + avatar: avatar ?? user.value!.avatar, + ); + }); + } +} +``` + +- [ ] **Step 2: 提交个人中心控制器** + +```bash +git add lib/src/controllers/profile_controller.dart +git commit -m "feat: add ProfileController for user state management" +``` + +--- + +## Task 5: 创建标准按钮组件 + +**Files:** +- Create: `lib/src/widgets/base/standard_button.dart` + +- [ ] **Step 1: 创建 StandardButton** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +enum StandardButtonType { primary, secondary, outline, text } + +class StandardButton extends StatelessWidget { + final String text; + final VoidCallback? onPressed; + final StandardButtonType type; + final bool isLoading; + final IconData? icon; + final bool isFullWidth; + final double? width; + final double? height; + + const StandardButton({ + super.key, + required this.text, + this.onPressed, + this.type = StandardButtonType.primary, + this.isLoading = false, + this.icon, + this.isFullWidth = false, + this.width, + this.height, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return GestureDetector( + onTap: isLoading ? null : onPressed, + child: Container( + width: isFullWidth ? double.infinity : width, + height: height ?? standards.scaledHeight(48), + padding: standards.scaledPadding( + EdgeInsets.symmetric(horizontal: 24, vertical: 12), + ), + decoration: _buildDecoration(standards), + child: _buildChild(standards), + ), + ); + } + + BoxDecoration _buildDecoration(PageStandards standards) { + switch (type) { + case StandardButtonType.primary: + return BoxDecoration( + color: standards.primaryColor, + borderRadius: BorderRadius.circular(standards.scaledRadius(12)), + ); + case StandardButtonType.secondary: + return BoxDecoration( + color: standards.secondaryColor, + borderRadius: BorderRadius.circular(standards.scaledRadius(12)), + ); + case StandardButtonType.outline: + return BoxDecoration( + border: Border.all(color: standards.primaryColor, width: 1.5), + borderRadius: BorderRadius.circular(standards.scaledRadius(12)), + ); + case StandardButtonType.text: + return const BoxDecoration(); + } + } + + Widget _buildChild(PageStandards standards) { + if (isLoading) { + return Center( + child: CupertinoActivityIndicator(color: _getTextColor(standards)), + ); + } + + return Row( + mainAxisSize: isFullWidth ? MainAxisSize.max : MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[ + Icon(icon, color: _getTextColor(standards), size: standards.fontSize + 4), + SizedBox(width: standards.scaledWidth(8)), + ], + Text( + text, + style: standards.textStyle.copyWith( + color: _getTextColor(standards), + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } + + Color _getTextColor(PageStandards standards) { + switch (type) { + case StandardButtonType.primary: + case StandardButtonType.secondary: + return CupertinoColors.white; + case StandardButtonType.outline: + case StandardButtonType.text: + return standards.primaryColor; + } + } +} +``` + +- [ ] **Step 2: 提交标准按钮组件** + +```bash +git add lib/src/widgets/base/standard_button.dart +git commit -m "feat: add StandardButton component with PageStandards" +``` + +--- + +## Task 6: 创建标准输入框组件 + +**Files:** +- Create: `lib/src/widgets/base/standard_text_field.dart` + +- [ ] **Step 1: 创建 StandardTextField** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardTextField extends StatelessWidget { + final String? placeholder; + final String? initialValue; + final ValueChanged? onChanged; + final VoidCallback? onEditingComplete; + final ValueChanged? onSubmitted; + final bool obscureText; + final TextInputType? keyboardType; + final TextEditingController? controller; + final FocusNode? focusNode; + final IconData? prefixIcon; + final IconData? suffixIcon; + final VoidCallback? onSuffixIconPressed; + final String? errorText; + final int? maxLines; + final bool enabled; + final bool autofocus; + + const StandardTextField({ + super.key, + this.placeholder, + this.initialValue, + this.onChanged, + this.onEditingComplete, + this.onSubmitted, + this.obscureText = false, + this.keyboardType, + this.controller, + this.focusNode, + this.prefixIcon, + this.suffixIcon, + this.onSuffixIconPressed, + this.errorText, + this.maxLines = 1, + this.enabled = true, + this.autofocus = false, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: standards.backgroundColor, + borderRadius: BorderRadius.circular(standards.scaledRadius(12)), + border: Border.all( + color: errorText != null + ? standards.secondaryColor + : standards.textColor.withOpacity(0.2), + ), + ), + child: Row( + children: [ + if (prefixIcon != null) + Padding( + padding: EdgeInsets.only(left: standards.scaledWidth(12)), + child: Icon( + prefixIcon, + color: standards.textColor.withOpacity(0.5), + size: standards.fontSize + 4, + ), + ), + Expanded( + child: CupertinoTextField( + controller: controller, + focusNode: focusNode, + placeholder: placeholder, + placeholderStyle: TextStyle( + color: standards.textColor.withOpacity(0.5), + fontSize: standards.fontSize, + ), + style: TextStyle( + color: standards.textColor, + fontSize: standards.fontSize, + ), + onChanged: onChanged, + onEditingComplete: onEditingComplete, + onSubmitted: onSubmitted, + obscureText: obscureText, + keyboardType: keyboardType, + maxLines: maxLines, + enabled: enabled, + autofocus: autofocus, + decoration: const BoxDecoration(), + padding: standards.scaledPadding( + EdgeInsets.symmetric(horizontal: 12, vertical: 12), + ), + ), + ), + if (suffixIcon != null) + GestureDetector( + onTap: onSuffixIconPressed, + child: Padding( + padding: EdgeInsets.only(right: standards.scaledWidth(12)), + child: Icon( + suffixIcon, + color: standards.textColor.withOpacity(0.5), + size: standards.fontSize + 4, + ), + ), + ), + ], + ), + ), + if (errorText != null) ...[ + SizedBox(height: standards.scaledHeight(4)), + Padding( + padding: EdgeInsets.only(left: standards.scaledWidth(12)), + child: Text( + errorText!, + style: TextStyle( + color: standards.secondaryColor, + fontSize: standards.fontSize - 2, + ), + ), + ), + ], + ], + ); + } +} +``` + +- [ ] **Step 2: 提交标准输入框组件** + +```bash +git add lib/src/widgets/base/standard_text_field.dart +git commit -m "feat: add StandardTextField component with PageStandards" +``` + +--- + +## Task 7: 创建标准卡片组件 + +**Files:** +- Create: `lib/src/widgets/base/standard_card.dart` + +- [ ] **Step 1: 创建 StandardCard** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardCard extends StatelessWidget { + final Widget child; + final VoidCallback? onTap; + final EdgeInsetsGeometry? padding; + final EdgeInsetsGeometry? margin; + final Color? backgroundColor; + final double? borderRadius; + final bool showShadow; + final bool showBorder; + final Color? borderColor; + + const StandardCard({ + super.key, + required this.child, + this.onTap, + this.padding, + this.margin, + this.backgroundColor, + this.borderRadius, + this.showShadow = true, + this.showBorder = false, + this.borderColor, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return GestureDetector( + onTap: onTap, + child: Container( + margin: margin ?? standards.scaledPadding(EdgeInsets.all(8)), + padding: padding ?? standards.scaledPadding(EdgeInsets.all(16)), + decoration: BoxDecoration( + color: backgroundColor ?? standards.backgroundColor, + borderRadius: BorderRadius.circular( + borderRadius ?? standards.scaledRadius(16), + ), + boxShadow: showShadow + ? [ + BoxShadow( + color: standards.textColor.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ] + : null, + border: showBorder + ? Border.all( + color: borderColor ?? standards.textColor.withOpacity(0.1), + ) + : null, + ), + child: child, + ), + ); + } +} +``` + +- [ ] **Step 2: 提交标准卡片组件** + +```bash +git add lib/src/widgets/base/standard_card.dart +git commit -m "feat: add StandardCard component with PageStandards" +``` + +--- + +## Task 8: 创建标准列表项组件 + +**Files:** +- Create: `lib/src/widgets/base/standard_list_tile.dart` + +- [ ] **Step 1: 创建 StandardListTile** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardListTile extends StatelessWidget { + final IconData? leadingIcon; + final Widget? leading; + final String title; + final String? subtitle; + final Widget? trailing; + final VoidCallback? onTap; + final bool showChevron; + final EdgeInsetsGeometry? padding; + + const StandardListTile({ + super.key, + this.leadingIcon, + this.leading, + required this.title, + this.subtitle, + this.trailing, + this.onTap, + this.showChevron = false, + this.padding, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: Container( + padding: padding ?? + standards.scaledPadding( + EdgeInsets.symmetric(horizontal: 16, vertical: 12), + ), + child: Row( + children: [ + if (leading != null) leading!, + if (leadingIcon != null) + Container( + width: standards.scaledWidth(40), + height: standards.scaledHeight(40), + decoration: BoxDecoration( + color: standards.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(standards.scaledRadius(10)), + ), + child: Icon( + leadingIcon, + color: standards.primaryColor, + size: standards.fontSize + 8, + ), + ), + if (leadingIcon != null || leading != null) + SizedBox(width: standards.scaledWidth(12)), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: standards.textStyle.copyWith( + fontWeight: FontWeight.w500, + ), + ), + if (subtitle != null) ...[ + SizedBox(height: standards.scaledHeight(2)), + Text( + subtitle!, + style: standards.textStyle.copyWith( + color: standards.textColor.withOpacity(0.6), + fontSize: standards.fontSize - 2, + ), + ), + ], + ], + ), + ), + if (trailing != null) trailing!, + if (showChevron) + Icon( + CupertinoIcons.chevron_right, + color: standards.textColor.withOpacity(0.4), + size: standards.fontSize + 4, + ), + ], + ), + ), + ); + } +} +``` + +- [ ] **Step 2: 提交标准列表项组件** + +```bash +git add lib/src/widgets/base/standard_list_tile.dart +git commit -m "feat: add StandardListTile component with PageStandards" +``` + +--- + +## Task 9: 创建空状态组件 + +**Files:** +- Create: `lib/src/widgets/states/empty_state.dart` + +- [ ] **Step 1: 创建 EmptyState** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; +import 'package:mom_kitchen/src/widgets/base/standard_button.dart'; + +class EmptyState extends StatelessWidget { + final String? title; + final String? message; + final IconData? icon; + final String? emoji; + final String? buttonText; + final VoidCallback? onButtonPressed; + + const EmptyState({ + super.key, + this.title, + this.message, + this.icon, + this.emoji, + this.buttonText, + this.onButtonPressed, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + final l10n = standards.l10n; + + return Center( + child: Padding( + padding: standards.scaledPadding(EdgeInsets.all(32)), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (emoji != null) + Text( + emoji!, + style: TextStyle(fontSize: standards.scaledFontSize(64)), + ) + else + Icon( + icon ?? CupertinoIcons.cube_box, + size: standards.scaledFontSize(64), + color: standards.textColor.withOpacity(0.4), + ), + SizedBox(height: standards.scaledHeight(16)), + Text( + title ?? l10n.noData, + style: standards.textStyle.copyWith( + fontSize: standards.fontSize + 4, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + if (message != null) ...[ + SizedBox(height: standards.scaledHeight(8)), + Text( + message!, + style: standards.textStyle.copyWith( + color: standards.textColor.withOpacity(0.6), + ), + textAlign: TextAlign.center, + ), + ], + if (buttonText != null && onButtonPressed != null) ...[ + SizedBox(height: standards.scaledHeight(24)), + StandardButton( + text: buttonText!, + onPressed: onButtonPressed, + type: StandardButtonType.primary, + ), + ], + ], + ), + ), + ); + } +} +``` + +- [ ] **Step 2: 提交空状态组件** + +```bash +git add lib/src/widgets/states/empty_state.dart +git commit -m "feat: add EmptyState component with PageStandards" +``` + +--- + +## Task 10: 创建错误状态组件 + +**Files:** +- Create: `lib/src/widgets/states/error_state.dart` + +- [ ] **Step 1: 创建 ErrorState** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; +import 'package:mom_kitchen/src/widgets/base/standard_button.dart'; + +class ErrorState extends StatelessWidget { + final String message; + final VoidCallback? onRetry; + final String? retryText; + + const ErrorState({ + super.key, + required this.message, + this.onRetry, + this.retryText, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + final l10n = standards.l10n; + + return Center( + child: Padding( + padding: standards.scaledPadding(EdgeInsets.all(32)), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + '⚠️', + style: TextStyle(fontSize: standards.scaledFontSize(64)), + ), + SizedBox(height: standards.scaledHeight(16)), + Text( + message, + style: standards.textStyle, + textAlign: TextAlign.center, + ), + if (onRetry != null) ...[ + SizedBox(height: standards.scaledHeight(24)), + StandardButton( + text: retryText ?? l10n.retry, + onPressed: onRetry, + type: StandardButtonType.outline, + ), + ], + ], + ), + ), + ); + } +} +``` + +- [ ] **Step 2: 提交错误状态组件** + +```bash +git add lib/src/widgets/states/error_state.dart +git commit -m "feat: add ErrorState component with PageStandards" +``` + +--- + +## Task 11: 创建标准对话框组件 + +**Files:** +- Create: `lib/src/widgets/interactive/standard_dialog.dart` + +- [ ] **Step 1: 创建 StandardDialog** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardDialog extends StatelessWidget { + final String title; + final String? message; + final String? confirmText; + final String? cancelText; + final VoidCallback? onConfirm; + final VoidCallback? onCancel; + final bool isDestructive; + + const StandardDialog({ + super.key, + required this.title, + this.message, + this.confirmText, + this.cancelText, + this.onConfirm, + this.onCancel, + this.isDestructive = false, + }); + + static Future show( + BuildContext context, { + required String title, + String? message, + String? confirmText, + String? cancelText, + bool isDestructive = false, + }) { + return showCupertinoDialog( + context: context, + builder: (context) => StandardDialog( + title: title, + message: message, + confirmText: confirmText, + cancelText: cancelText, + isDestructive: isDestructive, + ), + ); + } + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + final l10n = standards.l10n; + + return CupertinoAlertDialog( + title: Text(title, style: standards.textStyle), + content: message != null + ? Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text(message!, style: standards.textStyle), + ) + : null, + actions: [ + if (cancelText != null || onCancel != null) + CupertinoDialogAction( + onPressed: () { + Navigator.pop(context, false); + onCancel?.call(); + }, + child: Text(cancelText ?? l10n.cancel), + ), + CupertinoDialogAction( + isDestructiveAction: isDestructive, + onPressed: () { + Navigator.pop(context, true); + onConfirm?.call(); + }, + child: Text(confirmText ?? l10n.confirm), + ), + ], + ); + } +} +``` + +- [ ] **Step 2: 提交标准对话框组件** + +```bash +git add lib/src/widgets/interactive/standard_dialog.dart +git commit -m "feat: add StandardDialog component with PageStandards" +``` + +--- + +## Task 12: 创建标准底部弹窗组件 + +**Files:** +- Create: `lib/src/widgets/interactive/standard_bottom_sheet.dart` + +- [ ] **Step 1: 创建 StandardBottomSheet** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardBottomSheet extends StatelessWidget { + final String? title; + final Widget child; + final double? height; + + const StandardBottomSheet({ + super.key, + this.title, + required this.child, + this.height, + }); + + static Future show({ + required BuildContext context, + String? title, + required Widget child, + double? height, + bool isScrollControlled = false, + }) { + return showModalBottomSheet( + context: context, + isScrollControlled: isScrollControlled, + backgroundColor: Colors.transparent, + builder: (context) => StandardBottomSheet( + title: title, + child: child, + height: height, + ), + ); + } + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return Container( + height: height, + decoration: BoxDecoration( + color: standards.backgroundColor, + borderRadius: BorderRadius.vertical( + top: Radius.circular(standards.scaledRadius(20)), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + margin: EdgeInsets.only(top: standards.scaledHeight(8)), + width: standards.scaledWidth(36), + height: standards.scaledHeight(4), + decoration: BoxDecoration( + color: standards.textColor.withOpacity(0.2), + borderRadius: BorderRadius.circular(standards.scaledRadius(2)), + ), + ), + if (title != null) + Padding( + padding: standards.scaledPadding(EdgeInsets.all(16)), + child: Text( + title!, + style: standards.textStyle.copyWith( + fontSize: standards.fontSize + 4, + fontWeight: FontWeight.w600, + ), + ), + ), + Flexible(child: child), + ], + ), + ); + } +} +``` + +- [ ] **Step 2: 提交标准底部弹窗组件** + +```bash +git add lib/src/widgets/interactive/standard_bottom_sheet.dart +git commit -m "feat: add StandardBottomSheet component with PageStandards" +``` + +--- + +## Task 13: 创建标准选择器组件 + +**Files:** +- Create: `lib/src/widgets/interactive/standard_picker.dart` + +- [ ] **Step 1: 创建 StandardPicker** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class StandardPickerItem { + final T value; + final String label; + + StandardPickerItem({ + required this.value, + required this.label, + }); +} + +class StandardPicker extends StatelessWidget { + final List> items; + final T? selectedValue; + final ValueChanged? onChanged; + final String? title; + + const StandardPicker({ + super.key, + required this.items, + this.selectedValue, + this.onChanged, + this.title, + }); + + static Future show({ + required BuildContext context, + required List> items, + T? selectedValue, + String? title, + }) async { + final standards = PageStandards.of(context); + + return await showCupertinoModalPopup( + context: context, + builder: (context) => Container( + height: standards.scaledHeight(300), + decoration: BoxDecoration( + color: standards.backgroundColor, + borderRadius: BorderRadius.vertical( + top: Radius.circular(standards.scaledRadius(20)), + ), + ), + child: Column( + children: [ + if (title != null) + Container( + padding: standards.scaledPadding(EdgeInsets.all(16)), + child: Text( + title!, + style: standards.textStyle.copyWith( + fontSize: standards.fontSize + 4, + fontWeight: FontWeight.w600, + ), + ), + ), + Expanded( + child: CupertinoPicker( + itemExtent: standards.scaledHeight(40), + scrollController: FixedExtentScrollController( + initialItem: selectedValue != null + ? items.indexWhere((e) => e.value == selectedValue) + : 0, + ), + onSelectedItemChanged: (index) { + Navigator.pop(context, items[index].value); + }, + children: items + .map((item) => Center( + child: Text( + item.label, + style: standards.textStyle, + ), + )) + .toList(), + ), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return Container( + height: standards.scaledHeight(200), + color: standards.backgroundColor, + child: CupertinoPicker( + itemExtent: standards.scaledHeight(40), + scrollController: FixedExtentScrollController( + initialItem: selectedValue != null + ? items.indexWhere((e) => e.value == selectedValue) + : 0, + ), + onSelectedItemChanged: (index) { + onChanged?.call(items[index].value); + }, + children: items + .map((item) => Center( + child: Text( + item.label, + style: standards.textStyle, + ), + )) + .toList(), + ), + ); + } +} +``` + +- [ ] **Step 2: 提交标准选择器组件** + +```bash +git add lib/src/widgets/interactive/standard_picker.dart +git commit -m "feat: add StandardPicker component with PageStandards" +``` + +--- + +## Task 14: 创建路由守卫 + +**Files:** +- Create: `lib/src/standards/route_guard.dart` + +- [ ] **Step 1: 创建 RouteGuard** + +```dart +import 'package:get/get.dart'; +import 'package:mom_kitchen/src/standards/app_pages.dart'; +import 'package:mom_kitchen/src/controllers/profile_controller.dart'; + +enum AuthLevel { none, optional, required } + +class RouteGuard { + static bool canAccess(String route, {String? userId}) { + final pageInfo = PageRegistry.getPage(route); + if (pageInfo == null) return false; + + final authLevel = pageInfo.metadata?['authLevel'] as AuthLevel? ?? AuthLevel.none; + + switch (authLevel) { + case AuthLevel.none: + return true; + case AuthLevel.optional: + return true; + case AuthLevel.required: + return userId != null && userId.isNotEmpty; + } + } + + static String? getRedirectRoute(String route, {String? userId}) { + if (canAccess(route, userId: userId)) return null; + + return '/login'; + } + + static Future checkAuth(String route) async { + final profileController = Get.find(); + await profileController.checkLoginStatus(); + + final userId = profileController.user.value?.id; + return canAccess(route, userId: userId); + } + + static Future getAuthRedirect(String route) async { + final profileController = Get.find(); + await profileController.checkLoginStatus(); + + final userId = profileController.user.value?.id; + return getRedirectRoute(route, userId: userId); + } +} +``` + +- [ ] **Step 2: 提交路由守卫** + +```bash +git add lib/src/standards/route_guard.dart +git commit -m "feat: add RouteGuard for authentication" +``` + +--- + +## Task 15: 重构 LoadingIndicator + +**Files:** +- Modify: `lib/src/widgets/loading_indicator.dart` + +- [ ] **Step 1: 读取现有 LoadingIndicator** + +Run: Read `lib/src/widgets/loading_indicator.dart` + +- [ ] **Step 2: 重构 LoadingIndicator 使用 PageStandards** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; + +class LoadingIndicator extends StatelessWidget { + final double size; + final Color? color; + + const LoadingIndicator({ + super.key, + this.size = 32, + this.color, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return Center( + child: CupertinoActivityIndicator( + radius: standards.scaledRadius(size / 2), + color: color ?? standards.primaryColor, + ), + ); + } +} +``` + +- [ ] **Step 3: 提交重构** + +```bash +git add lib/src/widgets/loading_indicator.dart +git commit -m "refactor: update LoadingIndicator to use PageStandards" +``` + +--- + +## Task 16: 重构 ProductCard + +**Files:** +- Modify: `lib/src/widgets/product_card.dart` + +- [ ] **Step 1: 读取现有 ProductCard** + +Run: Read `lib/src/widgets/product_card.dart` + +- [ ] **Step 2: 重构 ProductCard 使用 PageStandards** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; +import 'package:mom_kitchen/src/controllers/home_controller.dart'; + +class ProductCard extends StatelessWidget { + final ProductModel product; + final VoidCallback? onTap; + final VoidCallback? onAddToCart; + + const ProductCard({ + super.key, + required this.product, + this.onTap, + this.onAddToCart, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: standards.backgroundColor, + borderRadius: BorderRadius.circular(standards.scaledRadius(16)), + boxShadow: [ + BoxShadow( + color: standards.textColor.withOpacity(0.05), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildImage(standards), + _buildInfo(standards), + ], + ), + ), + ); + } + + Widget _buildImage(PageStandards standards) { + return Expanded( + child: Container( + decoration: BoxDecoration( + color: standards.primaryColor.withOpacity(0.1), + borderRadius: BorderRadius.vertical( + top: Radius.circular(standards.scaledRadius(16)), + ), + ), + child: Center( + child: Text( + product.image, + style: TextStyle(fontSize: standards.scaledFontSize(48)), + ), + ), + ), + ); + } + + Widget _buildInfo(PageStandards standards) { + return Padding( + padding: standards.scaledPadding(EdgeInsets.all(12)), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product.name, + style: standards.textStyle.copyWith( + fontWeight: FontWeight.w600, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + SizedBox(height: standards.scaledHeight(4)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '\$${product.price.toStringAsFixed(2)}', + style: standards.primaryTextStyle.copyWith( + fontWeight: FontWeight.bold, + ), + ), + if (onAddToCart != null) + GestureDetector( + onTap: onAddToCart, + child: Container( + padding: standards.scaledPadding(EdgeInsets.all(6)), + decoration: BoxDecoration( + color: standards.primaryColor, + borderRadius: BorderRadius.circular( + standards.scaledRadius(8), + ), + ), + child: Icon( + CupertinoIcons.plus, + color: CupertinoColors.white, + size: standards.fontSize, + ), + ), + ), + ], + ), + ], + ), + ); + } +} +``` + +- [ ] **Step 3: 提交重构** + +```bash +git add lib/src/widgets/product_card.dart +git commit -m "refactor: update ProductCard to use PageStandards" +``` + +--- + +## Task 17: 重构 HomePage 使用 GetX + +**Files:** +- Modify: `lib/src/pages/home_page.dart` + +- [ ] **Step 1: 读取现有 HomePage** + +Run: Read `lib/src/pages/home_page.dart` + +- [ ] **Step 2: 重构 HomePage 使用 GetX 控制器** + +```dart +import 'package:flutter/cupertino.dart'; +import 'package:get/get.dart'; +import 'package:mom_kitchen/src/controllers/home_controller.dart'; +import 'package:mom_kitchen/src/controllers/cart_controller.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; +import 'package:mom_kitchen/src/widgets/product_card.dart'; +import 'package:mom_kitchen/src/widgets/states/empty_state.dart'; +import 'package:mom_kitchen/src/widgets/states/error_state.dart'; +import 'package:mom_kitchen/src/widgets/loading_indicator.dart'; +import 'package:mom_kitchen/src/widgets/responsive_grid.dart'; +import 'package:mom_kitchen/src/widgets/adaptive_scaffold.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key}); + + @override + Widget build(BuildContext context) { + final homeController = Get.put(HomeController()); + final cartController = Get.put(CartController()); + final standards = PageStandards.of(context); + + return CupertinoPageScaffold( + backgroundColor: standards.backgroundColor, + child: SafeArea( + child: Obx(() { + if (homeController.isLoading.value) { + return const LoadingIndicator(); + } + + if (homeController.errorMessage.value.isNotEmpty) { + return ErrorState( + message: homeController.errorMessage.value, + onRetry: () => homeController.loadProducts(), + ); + } + + return Column( + children: [ + _buildSearchBar(homeController, standards), + _buildCategoryFilter(homeController, standards), + Expanded( + child: homeController.filteredProducts.isEmpty + ? const EmptyState( + emoji: '🔍', + title: 'No products found', + message: 'Try a different search or category', + ) + : _buildProductGrid( + homeController, + cartController, + standards, + ), + ), + ], + ); + }), + ), + ); + } + + Widget _buildSearchBar(HomeController controller, PageStandards standards) { + return Padding( + padding: standards.scaledPadding(EdgeInsets.all(16)), + child: Container( + decoration: BoxDecoration( + color: standards.backgroundColor, + borderRadius: BorderRadius.circular(standards.scaledRadius(12)), + border: Border.all( + color: standards.textColor.withOpacity(0.1), + ), + ), + child: CupertinoSearchTextField( + placeholder: 'Search products...', + style: TextStyle(color: standards.textColor), + placeholderStyle: TextStyle( + color: standards.textColor.withOpacity(0.5), + ), + decoration: null, + onChanged: controller.search, + ), + ), + ); + } + + Widget _buildCategoryFilter(HomeController controller, PageStandards standards) { + return Obx(() => Container( + height: standards.scaledHeight(44), + padding: EdgeInsets.symmetric(horizontal: standards.scaledWidth(16)), + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: controller.categories.length + 1, + itemBuilder: (context, index) { + final isSelected = index == 0 + ? controller.selectedCategory.value.isEmpty + : controller.selectedCategory.value == + controller.categories[index - 1]; + + return Padding( + padding: EdgeInsets.only(right: standards.scaledWidth(8)), + child: GestureDetector( + onTap: () { + if (index == 0) { + controller.selectCategory(''); + } else { + controller.selectCategory(controller.categories[index - 1]); + } + }, + child: Container( + padding: standards.scaledPadding( + EdgeInsets.symmetric(horizontal: 16, vertical: 8), + ), + decoration: BoxDecoration( + color: isSelected + ? standards.primaryColor + : standards.backgroundColor, + borderRadius: BorderRadius.circular( + standards.scaledRadius(20), + ), + border: Border.all( + color: isSelected + ? standards.primaryColor + : standards.textColor.withOpacity(0.2), + ), + ), + child: Text( + index == 0 ? 'All' : controller.categories[index - 1], + style: standards.textStyle.copyWith( + color: isSelected + ? CupertinoColors.white + : standards.textColor, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + }, + ), + )); + } + + Widget _buildProductGrid( + HomeController homeController, + CartController cartController, + PageStandards standards, + ) { + return ResponsiveGrid( + crossAxisCount: 2, + crossAxisCountMedium: 3, + crossAxisCountLarge: 4, + childAspectRatio: 0.75, + spacing: 12, + runSpacing: 12, + children: homeController.filteredProducts + .map((product) => ProductCard( + product: product, + onAddToCart: () => cartController.addProduct(product), + )) + .toList(), + ); + } +} +``` + +- [ ] **Step 3: 提交重构** + +```bash +git add lib/src/pages/home_page.dart +git commit -m "refactor: update HomePage to use GetX state management" +``` + +--- + +## Task 18: 更新 CHANGELOG.md + +**Files:** +- Modify: `CHANGELOG.md` + +- [ ] **Step 1: 读取 CHANGELOG.md** + +Run: Read `CHANGELOG.md` + +- [ ] **Step 2: 添加版本更新日志** + +在文件顶部添加: + +```markdown +## [1.3.0] - 2026-04-08 + +### Added +- GetX 全局状态管理系统 + - BaseController 基础控制器 + - PagedController 分页控制器 + - HomeController 首页控制器 + - CartController 购物车控制器 + - ProfileController 个人中心控制器 +- 标准组件库 + - StandardButton 标准按钮 + - StandardTextField 标准输入框 + - StandardCard 标准卡片 + - StandardListTile 标准列表项 +- 状态组件 + - EmptyState 空状态 + - ErrorState 错误状态 +- 交互组件 + - StandardDialog 标准对话框 + - StandardBottomSheet 标准底部弹窗 + - StandardPicker 标准选择器 +- RouteGuard 路由守卫系统 + +### Changed +- 重构 HomePage 使用 GetX 状态管理 +- 重构 ProductCard 使用 PageStandards +- 重构 LoadingIndicator 使用 PageStandards + +### Technical +- 统一状态管理为 GetX +- 所有组件集成 PageStandards +- 实现路由守卫认证机制 +``` + +- [ ] **Step 3: 提交 CHANGELOG** + +```bash +git add CHANGELOG.md +git commit -m "docs: update CHANGELOG for v1.3.0" +``` + +--- + +## 验收清单 + +- [ ] 所有控制器正常工作 +- [ ] 所有组件使用 PageStandards +- [ ] 路由守卫正常拦截 +- [ ] HomePage 使用 GetX +- [ ] 代码分析无错误 +- [ ] 深色模式正常 +- [ ] 多语言正常 + +--- + +**创建时间:** 2026-04-08 +**作者:** AI Assistant diff --git a/docs/superpowers/specs/2026-04-08-refactor-design.md b/docs/superpowers/specs/2026-04-08-refactor-design.md new file mode 100644 index 0000000..f3795b9 --- /dev/null +++ b/docs/superpowers/specs/2026-04-08-refactor-design.md @@ -0,0 +1,301 @@ +# 全面重构设计方案 + +## 概述 + +本设计文档描述了对 Mom's Kitchen 项目进行全面重构的方案,包括: +1. GetX 全局状态管理 +2. 组件使用 PageStandards +3. 路由守卫系统 +4. 组件复用方案 + +--- + +## 一、GetX 全局状态管理 + +### 1.1 架构设计 + +``` +lib/src/controllers/ +├── base/ +│ ├── base_controller.dart # 基础控制器 +│ └── paged_controller.dart # 分页控制器 +├── home_controller.dart # 首页控制器 +├── cart_controller.dart # 购物车控制器 +├── profile_controller.dart # 个人中心控制器 +└── app_controller.dart # 应用全局控制器 +``` + +### 1.2 基础控制器 + +**BaseController:** +- 提供 `isLoading` 状态 +- 提供 `errorMessage` 状态 +- 提供 `runWithLoading()` 方法封装异步操作 + +**PagedController:** +- 继承 BaseController +- 提供 `items` 列表 +- 提供 `currentPage` 当前页码 +- 提供 `hasMore` 是否有更多数据 +- 提供 `loadMore()` 加载更多 +- 提供 `refresh()` 刷新数据 + +### 1.3 页面控制器 + +**HomeController:** +- 管理产品列表 +- 管理分类列表 +- 管理搜索和筛选状态 + +**CartController:** +- 管理购物车商品 +- 计算总价 +- 添加/删除商品 + +**ProfileController:** +- 管理用户信息 +- 处理登录/登出 + +### 1.4 页面使用方式 + +- 使用 `Get.put()` 注册控制器 +- 使用 `Obx()` 或 `GetBuilder` 监听状态变化 +- 使用 `Get.find()` 获取控制器实例 + +--- + +## 二、组件使用 PageStandards + +### 2.1 组件库结构 + +``` +lib/src/widgets/ +├── base/ +│ ├── standard_button.dart # 标准按钮 +│ ├── standard_text_field.dart # 标准输入框 +│ ├── standard_card.dart # 标准卡片 +│ └── standard_list_tile.dart # 标准列表项 +├── interactive/ +│ ├── standard_dialog.dart # 标准对话框 +│ ├── standard_bottom_sheet.dart # 标准底部弹窗 +│ └── standard_picker.dart # 标准选择器 +├── states/ +│ ├── loading_indicator.dart # 加载指示器 +│ ├── empty_state.dart # 空状态 +│ ├── error_state.dart # 错误状态 +│ └── skeleton_loader.dart # 骨架屏 +└── product_card.dart # 产品卡片(重构) +``` + +### 2.2 设计原则 + +1. **所有组件必须使用 PageStandards** + - 颜色使用 `standards.primaryColor` 等 + - 字体使用 `standards.textStyle` 等 + - 尺寸使用 `standards.scaledWidth()` 等 + +2. **支持多语言** + - 通过 `standards.l10n` 获取翻译 + +3. **支持深色模式** + - 自动跟随主题变化 + +### 2.3 标准按钮组件 + +**StandardButton:** +- 支持 4 种类型:primary、secondary、outline、text +- 支持加载状态 +- 支持图标 +- 支持全宽 + +--- + +## 三、路由守卫系统 + +### 3.1 认证级别 + +```dart +enum AuthLevel { + none, // 无需认证 + optional, // 可选认证 + required // 必须认证 +} +``` + +### 3.2 RouteGuard 类 + +**功能:** +- `canAccess()` - 检查是否可以访问 +- `getRedirectRoute()` - 获取重定向路由 + +**使用方式:** +- 在 PageInfo 的 metadata 中设置 `authLevel` +- 在 GetPage 中调用 RouteGuard 进行验证 + +### 3.3 集成方式 + +```dart +GetPage( + name: '/profile', + page: () { + final redirect = RouteGuard.getRedirectRoute('/profile', user: currentUser); + if (redirect != null) { + return LoginPage(); + } + return const ProfilePage(); + }, +), +``` + +--- + +## 四、组件复用方案 + +### 4.1 状态组件 + +**LoadingIndicator:** +- 显示加载动画 +- 支持自定义大小和颜色 + +**EmptyState:** +- 显示空状态提示 +- 支持自定义图标、标题、消息 +- 支持操作按钮 + +**ErrorState:** +- 显示错误信息 +- 支持重试按钮 + +### 4.2 交互组件 + +**StandardDialog:** +- 标准对话框 +- 支持确认/取消按钮 +- 提供静态 `show()` 方法 + +**StandardBottomSheet:** +- 标准底部弹窗 +- 支持自定义内容 + +**StandardPicker:** +- 标准选择器 +- 支持单选/多选 + +### 4.3 基础组件 + +**StandardButton:** +- 标准按钮 +- 支持 4 种样式 + +**StandardTextField:** +- 标准输入框 +- 支持验证 + +**StandardCard:** +- 标准卡片 +- 支持点击事件 + +**StandardListTile:** +- 标准列表项 +- 支持图标、标题、副标题 + +--- + +## 五、实施计划 + +### 5.1 第一阶段:基础架构 + +1. 创建 controllers/base/ 目录 +2. 实现 BaseController +3. 实现 PagedController + +### 5.2 第二阶段:控制器迁移 + +1. 创建 HomeController +2. 创建 CartController +3. 创建 ProfileController +4. 重构现有页面使用控制器 + +### 5.3 第三阶段:组件重构 + +1. 创建 widgets/base/ 目录 +2. 实现 StandardButton +3. 实现 StandardTextField +4. 实现 StandardCard +5. 实现 StandardListTile +6. 重构 ProductCard + +### 5.4 第四阶段:状态组件 + +1. 重构 LoadingIndicator +2. 实现 EmptyState +3. 实现 ErrorState +4. 重构 SkeletonLoader + +### 5.5 第五阶段:交互组件 + +1. 实现 StandardDialog +2. 实现 StandardBottomSheet +3. 实现 StandardPicker + +### 5.6 第六阶段:路由守卫 + +1. 实现 RouteGuard +2. 更新 PageInfo +3. 更新 main.dart + +--- + +## 六、文件清单 + +### 新增文件 + +| 文件路径 | 说明 | +|----------|------| +| `lib/src/controllers/base/base_controller.dart` | 基础控制器 | +| `lib/src/controllers/base/paged_controller.dart` | 分页控制器 | +| `lib/src/controllers/home_controller.dart` | 首页控制器 | +| `lib/src/controllers/cart_controller.dart` | 购物车控制器 | +| `lib/src/controllers/profile_controller.dart` | 个人中心控制器 | +| `lib/src/controllers/app_controller.dart` | 应用全局控制器 | +| `lib/src/widgets/base/standard_button.dart` | 标准按钮 | +| `lib/src/widgets/base/standard_text_field.dart` | 标准输入框 | +| `lib/src/widgets/base/standard_card.dart` | 标准卡片 | +| `lib/src/widgets/base/standard_list_tile.dart` | 标准列表项 | +| `lib/src/widgets/interactive/standard_dialog.dart` | 标准对话框 | +| `lib/src/widgets/interactive/standard_bottom_sheet.dart` | 标准底部弹窗 | +| `lib/src/widgets/interactive/standard_picker.dart` | 标准选择器 | +| `lib/src/widgets/states/empty_state.dart` | 空状态 | +| `lib/src/widgets/states/error_state.dart` | 错误状态 | +| `lib/src/standards/route_guard.dart` | 路由守卫 | + +### 修改文件 + +| 文件路径 | 修改内容 | +|----------|----------| +| `lib/src/pages/home_page.dart` | 使用 HomeController | +| `lib/src/pages/example_page.dart` | 使用 PageStandards | +| `lib/src/pages/theme_demo_page.dart` | 使用控制器 | +| `lib/src/widgets/product_card.dart` | 使用 PageStandards | +| `lib/src/widgets/loading_indicator.dart` | 使用 PageStandards | +| `lib/src/widgets/skeleton_loader.dart` | 使用 PageStandards | +| `lib/src/standards/app_pages.dart` | 添加 authLevel | +| `lib/main.dart` | 集成路由守卫 | +| `CHANGELOG.md` | 更新日志 | + +--- + +## 七、验收标准 + +1. ✅ 所有页面使用 GetX 状态管理 +2. ✅ 所有组件使用 PageStandards +3. ✅ 路由守卫正常工作 +4. ✅ 组件可复用 +5. ✅ 代码分析无错误 +6. ✅ 深色模式正常切换 +7. ✅ 多语言正常工作 + +--- + +**创建时间:** 2026-04-08 +**作者:** AI Assistant 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..592ceee --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..fa55352 --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 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 = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + 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 = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + 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 = ( + 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 = { + 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 */ + 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"; + }; +/* 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; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.momKitchen; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.momKitchen.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; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.momKitchen.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; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.momKitchen.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; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.momKitchen; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.momKitchen; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 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..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + 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..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 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..797d452 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..6ed2d93 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..4cd7b00 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..fe73094 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..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 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..502f463 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..0ec3034 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..0ec3034 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..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a 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..8953cba 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..0467bf1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..d6bfcab --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Mom Kitchen + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + mom_kitchen + 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/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/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..f40adb9 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,7 @@ +arb-dir: lib/src/l10n +template-arb-file: app_en.arb +ooutput-localization-file: app_localizations.dart +output-class: AppLocalizations +supported-locales: + - en + - zh \ No newline at end of file diff --git a/lib/PAGE_STANDARDS.md b/lib/PAGE_STANDARDS.md new file mode 100644 index 0000000..6b16e1a --- /dev/null +++ b/lib/PAGE_STANDARDS.md @@ -0,0 +1,548 @@ +# 页面规范与验证系统 + +本文档说明如何使用页面规范系统和页面验证系统,确保所有页面遵循统一的开发标准。 + +--- + +## 目录 + +- [页面规范系统](#页面规范系统) + - [PageStandards 类](#pagestandards-类) + - [PageStandardsMixin 混入](#pagestandardsmixin-混入) + - [StandardPage 基类](#standardpage-基类) + - [扩展方法](#扩展方法) +- [页面验证系统](#页面验证系统) + - [页面注册](#页面注册) + - [规范检查项](#规范检查项) + - [验证报告](#验证报告) +- [最佳实践](#最佳实践) + +--- + +## 页面规范系统 + +### PageStandards 类 + +`PageStandards` 是页面规范配置类,提供统一的页面构建标准。 + +**包含的规范属性:** + +| 类别 | 属性 | 类型 | 说明 | +|------|------|------|------| +| **主题颜色** | `primaryColor` | Color | 主题色 | +| | `secondaryColor` | Color | 次要色 | +| | `textColor` | Color | 文字颜色 | +| | `backgroundColor` | Color | 背景颜色 | +| **字体大小** | `fontSize` | double | 基础字体大小 | +| | `textStyle` | TextStyle | 主题文字样式 | +| | `primaryTextStyle` | TextStyle | 主题色文字样式 | +| | `secondaryTextStyle` | TextStyle | 次要色文字样式 | +| **动画配置** | `animationEnabled` | bool | 动画开关 | +| | `animationSpeed` | double | 动画速度 | +| | `animationIntensity` | double | 动画强度 | +| | `animationPreset` | AnimationPreset | 动画预设 | +| | `animationCurve` | AnimationCurveType | 动画曲线 | +| **响应式布局** | `scaleEnabled` | bool | 缩放开关 | +| | `screenWidth` | double | 屏幕宽度 | +| | `screenHeight` | double | 屏幕高度 | +| | `statusBarHeight` | double | 状态栏高度 | +| | `bottomBarHeight` | double | 底部安全区高度 | +| **多语言** | `l10n` | AppLocalizations | 多语言实例 | +| | `currentLocale` | Locale | 当前语言 | +| | `languageCode` | String | 语言代码 | +| **屏幕方向** | `orientation` | Orientation | 当前方向 | +| | `isPortrait` | bool | 是否竖屏 | +| | `isLandscape` | bool | 是否横屏 | +| **消息样式** | `toastStyle` | ToastStyle | 消息提示样式 | +| **状态栏** | `isStatusBarImmersive` | bool | 状态栏沉浸开关 | +| | `systemUiOverlayStyle` | SystemUiOverlayStyle | 系统UI样式 | +| **深色模式** | `isDarkMode` | bool | 是否深色模式 | +| | `brightness` | Brightness | 当前亮度 | +| **设备类型** | `deviceType` | DeviceType | 设备类型 | +| | `isMobile` | bool | 是否手机 | +| | `isTablet` | bool | 是否平板 | +| | `isWeb` | bool | 是否 Web | +| | `isDesktop` | bool | 是否桌面端 | +| | `isHarmonyOS` | bool | 是否鸿蒙 | + +**缩放方法:** + +| 方法 | 参数 | 返回值 | 说明 | +|------|------|--------|------| +| `scaledWidth()` | double | double | 缩放宽度 | +| `scaledHeight()` | double | double | 缩放高度 | +| `scaledFontSize()` | double | double | 缩放字体 | +| `scaledRadius()` | double | double | 缩放圆角 | +| `scaledPadding()` | EdgeInsets | EdgeInsets | 缩放内边距 | +| `scaledMargin()` | EdgeInsets | EdgeInsets | 缩放外边距 | + +**使用示例:** + +```dart +final standards = PageStandards.of(context); + +// 获取主题色 +Container( + color: standards.backgroundColor, + child: Text( + 'Hello', + style: standards.textStyle, + ), +); + +// 使用缩放方法 +Container( + width: standards.scaledWidth(100), + height: standards.scaledHeight(50), + padding: standards.scaledPadding(EdgeInsets.all(16)), +); +``` + +--- + +### PageStandardsMixin 混入 + +使用 `PageStandardsMixin` 可以自动注入页面规范,无需手动创建 `PageStandards` 实例。 + +**使用示例:** + +```dart +class MyPage extends StatefulWidget { + const MyPage({super.key}); + + @override + State createState() => _MyPageState(); +} + +class _MyPageState extends State with PageStandardsMixin { + @override + Widget build(BuildContext context) { + return CupertinoPageScaffold( + backgroundColor: backgroundColor, // 直接访问主题背景色 + child: Column( + children: [ + Text( + l10n.appTitle, // 直接访问多语言 + style: textStyle, // 直接访问主题文字样式 + ), + Container( + width: scaledWidth(100), // 使用缩放方法 + height: scaledHeight(50), + ), + ], + ), + ); + } +} +``` + +--- + +### StandardPage 基类 + +`StandardPage` 和 `StandardPageState` 是标准页面基类,自动集成了 `PageStandardsMixin`。 + +**使用示例:** + +```dart +class MyPage extends StandardPage { + const MyPage({super.key}); + + @override + State createState() => _MyPageState(); +} + +class _MyPageState extends StandardPageState { + @override + Widget buildPage(BuildContext context) { + return CupertinoPageScaffold( + backgroundColor: backgroundColor, + child: Text( + l10n.appTitle, + style: textStyle, + ), + ); + } +} +``` + +--- + +### 扩展方法 + +`PageStandardsExtension` 为 `BuildContext` 提供了便捷的扩展方法。 + +**使用示例:** + +```dart +Widget build(BuildContext context) { + return Container( + color: context.backgroundColor, // 主题背景色 + child: Text( + context.l10n.appTitle, // 多语言 + style: TextStyle( + color: context.textColor, // 主题字体色 + fontSize: context.fontSize, // 主题字体大小 + ), + ), + ); +} +``` + +**可用的扩展方法:** + +| 扩展属性 | 类型 | 说明 | +|----------|------|------| +| `context.standards` | PageStandards | 完整规范实例 | +| `context.l10n` | AppLocalizations | 多语言实例 | +| `context.primaryColor` | Color | 主题色 | +| `context.textColor` | Color | 文字颜色 | +| `context.backgroundColor` | Color | 背景颜色 | +| `context.fontSize` | double | 字体大小 | + +--- + +## 页面验证系统 + +### 页面注册 + +所有页面必须在 `AppPages` 中注册,以便进行规范验证。 + +**注册位置:** `lib/src/standards/app_pages.dart` + +**注册示例:** + +```dart +import 'package:mom_kitchen/src/standards/page_validator.dart'; +import 'package:mom_kitchen/src/pages/my_page.dart'; + +class AppPages { + static final List pages = [ + PageInfo( + route: '/my_page', + name: '我的页面', + description: '示例页面说明', + requiredStandards: [ + StandardCheck.themeColors, // 主题颜色 + StandardCheck.textColors, // 字体颜色 + StandardCheck.fontSize, // 字体大小 + StandardCheck.animation, // 动画配置 + StandardCheck.responsive, // 响应式布局 + StandardCheck.localization, // 多语言 + ], + builder: () => const MyPage(), + metadata: { // 可选的元数据 + 'icon': 'person', + 'category': 'user', + }, + ), + ]; +} +``` + +**PageInfo 属性说明:** + +| 属性 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `route` | String | ✅ | 路由路径 | +| `name` | String | ✅ | 页面名称 | +| `description` | String | ✅ | 页面描述 | +| `requiredStandards` | List\ | ✅ | 必须遵循的规范 | +| `builder` | Widget Function() | ✅ | 页面构建函数 | +| `metadata` | Map\? | ❌ | 自定义元数据 | + +--- + +### 规范检查项 + +`StandardCheck` 枚举定义了 12 种规范检查项: + +| 检查项 | 标签 | 说明 | +|--------|------|------| +| `themeColors` | 主题颜色 | 检查是否使用主题色、次要色 | +| `textColors` | 字体颜色 | 检查是否使用主题字体色 | +| `fontSize` | 字体大小 | 检查是否使用主题字体大小 | +| `spacing` | 间距规范 | 检查是否使用标准间距 | +| `animation` | 动画配置 | 检查是否遵循动画配置 | +| `responsive` | 响应式布局 | 检查是否适配不同屏幕 | +| `localization` | 多语言 | 检查是否使用多语言 | +| `orientation` | 屏幕方向 | 检查是否处理屏幕方向 | +| `toastStyle` | 消息样式 | 检查是否使用标准消息样式 | +| `statusBarImmersive` | 状态栏沉浸 | 检查是否处理状态栏沉浸 | +| `darkMode` | 深色模式 | 检查是否支持深色模式 | +| `deviceType` | 设备类型 | 检查是否适配不同设备类型 | + +--- + +### 验证报告 + +**开发模式自动验证:** + +在开发模式下(`kDebugMode == true`),页面验证系统会自动: +1. 检测页面是否已注册 +2. 验证页面是否遵循规范 +3. 在控制台输出验证结果 + +**控制台输出示例:** + +``` +✅ 已注册 3 个页面 + - 首页 (/) + - 主题设置 (/theme) + - 示例页面 (/example) + +🔍 开始验证页面: 首页 (/) + ✓ 主题颜色 + ✓ 字体颜色 + ✓ 字体大小 + ✓ 多语言 + ✓ 深色模式 +✅ 页面验证完成: 首页 +``` + +**未注册页面错误:** + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +❌ 页面未注册错误 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +路由: /unknown_page + +该页面未在 AppPages 中注册,请检查: +1. 是否已在 AppPages.pages 中添加页面信息 +2. 路由名称是否正确 +3. 是否调用了 AppPages.registerAll() + +已注册的页面: + - /: 首页 + - /theme: 主题设置 + - /example: 示例页面 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +**手动获取验证报告:** + +```dart +// 获取验证历史 +final history = PageValidator.history; + +// 获取失败的验证结果 +final failed = PageValidator.getFailedResults(); + +// 生成验证报告 +final report = PageValidator.generateReport(); +print('总检查数: ${report['totalChecks']}'); +print('通过: ${report['passed']}'); +print('失败: ${report['failed']}'); +print('通过率: ${report['passRate']}%'); + +// 打印报告 +PageValidator.printReport(); +``` + +**调试面板:** + +```dart +// 在页面中显示调试面板 +if (kDebugMode) { + showCupertinoDialog( + context: context, + builder: (context) => const PageValidationDebugPanel(), + ); +} +``` + +--- + +## 最佳实践 + +### 1. 创建新页面 + +**推荐方式:** 继承 `StandardPage` 基类 + +```dart +class NewPage extends StandardPage { + const NewPage({super.key}); + + @override + State createState() => _NewPageState(); +} + +class _NewPageState extends StandardPageState { + @override + Widget buildPage(BuildContext context) { + return CupertinoPageScaffold( + backgroundColor: backgroundColor, + navigationBar: CupertinoNavigationBar( + middle: Text(l10n.appTitle), + ), + child: SafeArea( + child: ListView( + padding: scaledPadding(EdgeInsets.all(16)), + children: [ + Text( + l10n.welcomeMessage, + style: textStyle, + ), + SizedBox(height: scaledHeight(20)), + CupertinoButton.filled( + onPressed: _handlePress, + child: Text(l10n.getStarted), + ), + ], + ), + ), + ); + } + + void _handlePress() { + // 处理点击 + } +} +``` + +### 2. 注册新页面 + +在 `lib/src/standards/app_pages.dart` 中添加: + +```dart +PageInfo( + route: '/new_page', + name: '新页面', + description: '新页面说明', + requiredStandards: [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.fontSize, + StandardCheck.localization, + StandardCheck.darkMode, + ], + builder: () => const NewPage(), +), +``` + +### 3. 添加路由 + +在 `lib/main.dart` 的 `getPages` 中添加: + +```dart +GetPage( + name: '/new_page', + page: () { + AppPages.validateRoute('/new_page'); + return const NewPage(); + }, +), +``` + +### 4. 响应式布局 + +使用 `LayoutBuilder` 和 `PageStandards` 实现响应式: + +```dart +@override +Widget buildPage(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + if (isTablet && isLandscape) { + return _buildTabletLandscape(constraints); + } else if (isTablet) { + return _buildTabletPortrait(constraints); + } else if (isMobile && isLandscape) { + return _buildMobileLandscape(constraints); + } else { + return _buildMobilePortrait(constraints); + } + }, + ); +} + +Widget _buildMobilePortrait(BoxConstraints constraints) { + return CupertinoPageScaffold( + backgroundColor: backgroundColor, + child: Column( + children: [ + Expanded( + child: ListView( + padding: scaledPadding(EdgeInsets.all(16)), + children: [ + // 竖屏布局 + ], + ), + ), + ], + ), + ); +} +``` + +### 5. 深色模式支持 + +确保所有颜色使用主题值: + +```dart +// ❌ 错误:硬编码颜色 +Container( + color: Colors.white, + child: Text( + 'Hello', + style: TextStyle(color: Colors.black), + ), +) + +// ✅ 正确:使用主题值 +Container( + color: backgroundColor, + child: Text( + 'Hello', + style: textStyle, + ), +) +``` + +### 6. 多语言支持 + +确保所有文本使用多语言: + +```dart +// ❌ 错误:硬编码文本 +Text('欢迎使用') + +// ✅ 正确:使用多语言 +Text(l10n.welcomeMessage) +``` + +### 7. 动画配置 + +使用 `AnimationService` 的配置: + +```dart +AnimatedContainer( + duration: AppService.instance.animation.config.baseDuration, + curve: AppService.instance.animation.config.curve, + // ... +) +``` + +--- + +## 文件结构 + +``` +lib/src/standards/ +├── page_standards.dart # 页面规范系统 +├── page_validator.dart # 页面验证系统 +└── app_pages.dart # 页面注册表 +``` + +--- + +## 相关文档 + +- [README.md](./README.md) - 项目基础支撑库说明 +- [CHANGELOG.md](../../CHANGELOG.md) - 更新日志 + +--- + +**最后更新时间:** 2026-04-08 +**维护者:** Mom's Kitchen 开发团队 diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..106a47a --- /dev/null +++ b/lib/README.md @@ -0,0 +1,739 @@ +# Mom's Kitchen 项目基础支撑库说明 + +## 项目架构 + +``` +lib/ +├── main.dart # 应用入口 +├── src/ +│ ├── config/ # 配置文件 +│ │ ├── api_config.dart # API 配置 +│ │ └── app_config.dart # 应用配置 +│ ├── l10n/ # 国际化 +│ │ ├── app_en.arb # 英文翻译 +│ │ ├── app_zh.arb # 中文翻译 +│ │ ├── app_zh_Hant.arb # 繁体中文翻译 +│ │ ├── app_localizations.dart # 生成的本地化代码 +│ │ ├── app_localizations_en.dart # 英文实现 +│ │ ├── app_localizations_zh.dart # 中文实现 +│ │ └── app_localizations_zh_hant.dart # 繁体中文实现 +│ ├── models/ # 数据模型 +│ │ └── theme_model.dart # 主题模型 +│ ├── pages/ # 页面 +│ │ ├── cart_page.dart # 购物车页面 +│ │ ├── example_page.dart # 示例页面 +│ │ ├── home_page.dart # 首页 +│ │ └── theme_demo_page.dart # 主题设置页面 +│ ├── services/ # 服务层 +│ │ ├── api_service.dart # 网络请求服务 +│ │ ├── app_info_service.dart # 应用信息服务 +│ │ ├── app_service.dart # 应用服务(统一管理) +│ │ ├── animation_service.dart # 动画管理服务 +│ │ ├── logger_service.dart # 日志管理服务 +│ │ ├── notification_service.dart # 通知服务 +│ │ ├── orientation_service.dart # 屏幕方向服务 +│ │ ├── permission_service.dart # 权限管理服务 +│ │ ├── screen_util_config.dart # 屏幕适配配置 +│ │ ├── storage_service.dart # 本地存储服务 +│ │ ├── theme_service.dart # 主题管理服务 +│ │ └── toast_service.dart # 消息提示服务 +│ ├── standards/ # 页面规范 +│ │ ├── app_pages.dart # 页面注册表 +│ │ ├── page_standards.dart # 页面规范系统 +│ │ └── page_validator.dart # 页面验证系统 +│ ├── utils/ # 工具类 +│ │ ├── app_logger.dart # 全局日志工具 +│ │ ├── common_utils.dart # 通用工具 +│ │ ├── date_utils.dart # 日期工具 +│ │ ├── network_utils.dart # 网络工具 +│ │ ├── platform_utils.dart # 平台识别工具 +│ │ └── string_utils.dart # 字符串工具 +│ └── widgets/ # 通用组件 +│ ├── adaptive_page_interface.dart # 自适应页面接口 +│ ├── adaptive_scaffold.dart # 自适应布局组件 +│ ├── error_widget.dart # 错误组件 +│ ├── loading_indicator.dart # 加载指示器 +│ ├── product_card.dart # 产品卡片 +│ └── skeleton_loader.dart # 骨架屏 +``` + +## 已使用的依赖库 + +### 核心库 + +| 库名 | 版本 | 用途 | 使用位置 | +|------|------|------|----------| +| `cupertino_icons` | ^1.0.8 | iOS 风格图标 | 全局使用 | +| `flutter_localizations` | SDK | 国际化支持 | main.dart, app_localizations.dart | +| `intl` | ^0.20.2 | 国际化工具 | l10n 文件, date_utils.dart | +| `get` | git | 状态管理、路由管理、依赖注入 | ThemeService, AnimationService, main.dart 等 | + +### 网络与存储 + +| 库名 | 版本 | 用途 | 使用位置 | +|------|------|------|----------| +| `dio` | ^5.9.2 | 网络请求 | ApiService | +| `dio_cache_interceptor` | ^3.5.0 | Dio 网络请求缓存 | ApiService | +| `shared_preferences` | git | 本地键值存储 | StorageService, ThemeService, AnimationService | +| `connectivity_plus` | git | 网络状态检测 | ApiService, NetworkUtils | +| `path_provider` | git | 文件路径获取 | LoggerService | + +### 平台与工具 + +| 库名 | 版本 | 用途 | 使用位置 | +|------|------|------|----------| +| `flutter_local_notifications` | git | 本地通知 | NotificationService | +| `package_info_plus` | git | 应用信息 | AppInfoService | +| `fluttertoast` | git | Toast 消息提示 | ToastService | +| `share_plus` | git | 分享功能 | CommonUtils | +| `permission_handler` | git | 权限管理 | PermissionService | +| `device_info_plus` | git | 设备信息 | 待开发 | +| `logger` | ^2.7.0 | 日志管理 | LoggerService | +| `pretty_dio_logger` | ^1.4.0 | Dio 日志美化 | ApiService | + +### UI 与动画 + +| 库名 | 版本 | 用途 | 使用位置 | +|------|------|------|----------| +| `flutter_adaptive_scaffold` | git | 自适应布局 | AdaptivePageInterface, AdaptiveScaffold | +| `animations` | git | 页面过渡动画 | AnimationService | +| `flutter_screenutil` | 本地 v5.9.3 | 屏幕适配 | 全局使用(支持鸿蒙) | + +## 尚未使用的依赖库 + +| 库名 | 版本 | 用途 | 建议使用场景 | +|------|------|------|--------------| +| `pigeon` | git | Flutter 与原生平台通信 | 需要调用原生 API 时使用 | +| `orientation` | git | 屏幕方向控制 | 已改用 Flutter 原生 SystemChrome | +| `platform_info` | ^5.0.0 | 平台识别 | 已改用 dart:io 的 Platform | + +## 建议添加的基础库 + +| 库名 | 用途 | 优先级 | +|------|------|--------| +| `image_picker` | 图片选择、拍照 | 高 | +| `url_launcher` | 打开 URL、拨打电话 | 高 | +| `path_provider` | 文件路径获取 | 中 | +| `camera` | 相机访问 | 中 | +| `geolocator` | 地理定位 | 中 | +| `fl_chart` | 图表库 | 低 | +| `sqflite` | SQLite 数据库 | 低 | + +## 核心服务说明 + +### 1. ApiService(网络请求服务) + +**功能:** +- 基于 Dio 封装的网络请求 +- 支持缓存拦截器 +- 网络状态检测 +- 统一错误处理 + +**使用示例:** +```dart +final api = AppService.instance.api; +final response = await api.get('/products'); +``` + +### 2. StorageService(本地存储服务) + +**功能:** +- 基于 shared_preferences 封装 +- 支持键值对存储 +- 支持对象序列化 + +**使用示例:** +```dart +final storage = AppService.instance.storage; +await storage.setString('key', 'value'); +final value = storage.getString('key'); +``` + +### 3. ThemeService(主题管理服务) + +**功能:** +- 动态切换白天/夜间模式 +- 自定义主题色、次要色 +- 字体大小调节 +- 动画强度调节 +- 状态栏沉浸设置 +- 主题持久化存储 + +**使用示例:** +```dart +final theme = AppService.instance.theme; +await theme.toggleThemeMode(); +await theme.setPrimaryColor(Colors.blue); +``` + +### 4. OrientationService(屏幕方向服务) + +**功能:** +- 锁定屏幕方向 +- 解锁屏幕方向 +- 获取当前屏幕方向 + +**使用示例:** +```dart +final orientation = AppService.instance.orientation; +await orientation.lockPortrait(); +await orientation.unlockOrientation(); +``` + +### 5. NotificationService(通知服务) + +**功能:** +- 本地通知管理 +- 定时通知 +- 进度通知 +- 大文本通知 +- 图片通知 +- 通知权限请求 + +**使用示例:** +```dart +final notification = AppService.instance.notification; + +// 请求权限 +await notification.requestPermission(); + +// 显示普通通知 +await notification.showNotification( + id: 1, + title: '标题', + body: '内容', +); + +// 显示定时通知 +await notification.scheduleNotification( + id: 2, + title: '提醒', + body: '该吃饭了!', + scheduledDate: DateTime.now().add(Duration(hours: 1)), +); + +// 取消通知 +await notification.cancelNotification(1); +``` + +### 6. AppInfoService(应用信息服务) + +**功能:** +- 获取应用名称 +- 获取包名 +- 获取版本号 +- 获取构建号 +- 版本比较 +- 更新检测 + +**使用示例:** +```dart +final appInfo = AppService.instance.appInfo; + +// 获取版本信息 +print('版本: ${appInfo.version}'); +print('构建号: ${appInfo.buildNumber}'); +print('完整版本: ${appInfo.fullVersion}'); + +// 检查是否需要更新 +if (appInfo.needsUpdate('2.0.0')) { + print('需要更新应用'); +} + +// 获取应用信息摘要 +print(appInfo.appInfoString); +``` + +### 7. ToastService(消息提示服务) + +**功能:** +- 4 种消息类型:信息、成功(积极)、警告、错误(消极) +- 4 种显示样式:GetX、FlutterToast、默认、混合 +- 样式动态切换 +- 支持自定义显示时长 + +**消息类型:** +- `MessageType.info` - 普通信息(蓝色) +- `MessageType.success` - 成功/积极消息(绿色) +- `MessageType.warning` - 警告消息(橙色) +- `MessageType.error` - 错误/消极消息(红色) + +**显示样式:** +- `ToastStyle.getx` - GetX 默认样式 +- `ToastStyle.fluttertoast` - FlutterToast 样式 +- `ToastStyle.default_` - 默认样式(Cupertino 风格) +- `ToastStyle.hybrid` - 混合样式(推荐) + +**使用示例:** +```dart +final toast = AppService.instance.toast; + +// 设置显示样式 +toast.setStyle(ToastStyle.hybrid); + +// 显示信息消息 +await toast.info('这是一条信息'); + +// 显示成功消息(积极) +await toast.success('操作成功!'); + +// 显示警告消息 +await toast.warning('请注意!'); + +// 显示错误消息(消极) +await toast.error('操作失败!'); + +// 自定义显示 +await toast.show( + '自定义消息', + type: MessageType.info, + style: ToastStyle.getx, + duration: Duration(seconds: 3), +); + +// 取消所有消息 +toast.cancelAll(); +``` + +### 8. PermissionService(权限管理服务) + +**功能:** +- 支持 15 种常见权限类型(相机、麦克风、相册、位置等) +- 跨平台权限适配(iOS、Android、HarmonyOS、Web、Windows、macOS、Linux) +- 权限状态记录和查询功能 +- 批量权限检查和请求功能 + +**使用示例:** +```dart +final permission = AppService.instance.permission; + +// 检查单个权限 +final status = await permission.checkPermission(PermissionType.camera); +if (status.isGranted) { + // 权限已授予 +} + +// 请求单个权限 +final result = await permission.requestPermission(PermissionType.camera); +if (result.isGranted) { + // 权限已授予 +} + +// 批量检查权限 +final permissions = [ + PermissionType.camera, + PermissionType.microphone, + PermissionType.photos, +]; +final statuses = await permission.checkPermissions(permissions); + +// 批量请求权限 +final results = await permission.requestPermissions(permissions); + +// 获取所有权限状态 +final allStatuses = permission.getAllPermissionStatuses(); +``` + +### 9. AnimationService(动画管理服务) + +**功能:** +- 使用 `animations` 库实现专业动画效果 +- 支持动画开关、速度、强度、曲线四种参数调节 +- 提供 7 种预设模式(Standard、Fast、Slow、Smooth、Bouncy、Minimal、None) +- 支持 9 种动画曲线类型 +- 配置持久化存储,应用重启后保持设置 + +**预设模式:** +- `AnimationPreset.standard` - 标准动画(默认) +- `AnimationPreset.fast` - 快速动画 +- `AnimationPreset.slow` - 慢速动画 +- `AnimationPreset.smooth` - 平滑动画 +- `AnimationPreset.bouncy` - 弹性动画 +- `AnimationPreset.minimal` - 最小动画 +- `AnimationPreset.none` - 无动画 + +**使用示例:** +```dart +final animation = AppService.instance.animation; + +// 获取动画配置 +print('动画已启用: ${animation.enabled}'); +print('动画速度: ${animation.speed}'); +print('动画强度: ${animation.intensity}'); +print('动画曲线: ${animation.curveType}'); + +// 设置动画参数 +await animation.setEnabled(true); +await animation.setSpeed(1.5); +await animation.setIntensity(1.2); +await animation.setCurveType(AnimationCurveType.easeInOut); +await animation.setPreset(AnimationPreset.smooth); + +// 使用动画组件 +AnimatedButton( + onPressed: () => print('点击'), + child: Text('动画按钮'), +); + +AnimatedCard( + onTap: () => print('点击卡片'), + child: Text('动画卡片'), +); + +// 页面转场动画 +PageTransitions.fadeThrough( + child: NewPage(), +); +``` + +### 10. LoggerService(日志管理服务) + +**功能:** +- 支持日志开关、级别、文件写入配置 +- 支持 5 种日志级别(Debug、Info、Warning、Error、Off) +- 支持控制台输出和文件输出 +- 支持日志文件自动清理(按大小和数量) +- 配置持久化存储 + +**使用示例:** +```dart +// 使用 AppLogger 全局工具类 +AppLogger.d('调试信息'); +AppLogger.i('普通信息'); +AppLogger.w('警告信息'); +AppLogger.e('错误信息'); + +// 或使用 LoggerService +final logger = AppService.instance.logger; +logger.debug('调试信息'); +logger.info('普通信息'); +logger.warning('警告信息'); +logger.error('错误信息'); + +// 配置日志 +await AppLogger.setEnabled(true); +await AppLogger.setWriteToFile(true); + +// 获取日志文件 +final files = await AppLogger.getLogFiles(); +final content = await AppLogger.getLogContent(); + +// 清空日志 +await AppLogger.clearLogs(); +``` + +### 11. ScreenUtilConfig(屏幕适配配置) + +**功能:** +- 设计稿尺寸配置(默认 375x812) +- 最小字体适配开关 +- 分屏模式开关 +- 缩放开关控制 +- 配置持久化存储 + +**使用示例:** +```dart +// 初始化 +final screenUtil = AppService.instance.screenUtil; +screenUtil.initScreenUtil(context); + +// 或异步初始化 +await screenUtil.ensureScreenSizeAndInit(context); + +// 配置设计稿尺寸 +await screenUtil.setDesignSize(Size(375, 812)); +await screenUtil.setDesignWidth(375); +await screenUtil.setDesignHeight(812); + +// 配置适配选项 +await screenUtil.setMinTextAdapt(true); +await screenUtil.setSplitScreenMode(true); +await screenUtil.setScaleEnabled(true); + +// 使用扩展方法 +Container( + width: 100.w, // 按设计稿比例缩放 + height: 50.h, + padding: EdgeInsets.all(16.w), + child: Text( + 'Hello', + style: TextStyle(fontSize: 14.sp), // 字体缩放 + ), +) + +// 禁用缩放时,扩展方法返回原始值 +await screenUtil.setScaleEnabled(false); +// 此时 100.w 返回 100.0 +``` + +## 页面规范系统 + +> 📖 详细文档请查看 [PAGE_STANDARDS.md](./PAGE_STANDARDS.md) + +页面规范系统提供统一的页面构建标准,确保所有页面遵循统一的开发规范。 + +### 核心组件 + +| 组件 | 说明 | +|------|------| +| `PageStandards` | 页面规范配置类 | +| `PageStandardsMixin` | 页面混入,自动注入规范 | +| `StandardPage` | 标准页面基类 | +| `PageValidator` | 页面验证器 | +| `PageRegistry` | 页面注册表 | + +### 快速使用 + +```dart +class MyPage extends StandardPage { + const MyPage({super.key}); + + @override + State createState() => _MyPageState(); +} + +class _MyPageState extends StandardPageState { + @override + Widget buildPage(BuildContext context) { + return CupertinoPageScaffold( + backgroundColor: backgroundColor, // 自动获取主题背景色 + child: Text( + l10n.appTitle, // 自动获取多语言 + style: textStyle, // 自动获取主题文字样式 + ), + ); + } +} +``` + +--- + +## 主题系统说明 + +> 📖 页面主题使用详见 [PAGE_STANDARDS.md](./PAGE_STANDARDS.md#页面规范系统) + +### 主题属性 + +创建新页面时,**必须读取所有主题值**: + +```dart +@override +Widget build(BuildContext context) { + final theme = AppService.instance.theme; + + final isDarkMode = theme.isDarkMode; + final primaryColor = theme.primaryColor; + final secondaryColor = theme.secondaryColor; + final fontSize = theme.fontSize; + final textColor = theme.textColor; + final backgroundColor = theme.backgroundColor; + + return CupertinoPageScaffold( + backgroundColor: backgroundColor, + child: Text('Hello', style: TextStyle(color: textColor)), + ); +} +``` + +### 主题属性说明 + +| 属性 | 类型 | 说明 | 默认值 | +|------|------|------|--------| +| `isDarkMode` | bool | 是否为深色模式 | false | +| `primaryColor` | Color | 主题色 | Colors.blue | +| `secondaryColor` | Color | 次要色 | Colors.orange | +| `fontSize` | double | 基础字体大小 | 16.0 | +| `textColor` | Color | 文字颜色 | Colors.black | +| `backgroundColor` | Color | 背景颜色 | Colors.white | + +--- + +## 国际化说明 + +### 支持的语言 + +- 英文(en) +- 中文(zh) +- 繁体中文(zh_Hant) + +### 使用方法 + +```dart +final l10n = AppLocalizations.of(context)!; +Text(l10n.appTitle) +``` + +### 添加新的翻译 + +1. 编辑 `lib/src/l10n/app_en.arb` 和 `lib/src/l10n/app_zh.arb` +2. 运行 `flutter gen-l10n` 生成代码 + +## 注意事项 + +### 1. 组件风格 + +- **优先使用 iOS 风格组件**(Cupertino) +- 若 iOS 无对应组件,再使用 Material 组件 +- 示例:使用 `CupertinoButton` 而非 `ElevatedButton` + +### 2. 响应式布局 + +- 使用 `LayoutBuilder` 和 `MediaQuery` 实现响应式 +- 支持横竖屏切换 +- 适配不同屏幕尺寸 + +### 3. 骨架屏与动画 + +- 页面加载时显示骨架屏 +- 使用 `AnimationService` 统一管理动画 +- 动画参数可在设置中调节(开关、速度、强度、曲线) +- 使用 `AnimatedButton`、`AnimatedCard` 等动画组件 + +### 4. 网络图片加载 + +- 使用 `Image.network` 加载网络图片 +- 添加 `loadingBuilder` 和 `errorBuilder` +- 示例: +```dart +Image.network( + url, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return const CupertinoActivityIndicator(); + }, + errorBuilder: (context, error, stackTrace) { + return const Icon(CupertinoIcons.exclamationmark_circle); + }, +) +``` + +### 5. 平台识别 + +使用 `PlatformUtils` 识别平台: + +```dart +final platform = PlatformUtils(); +if (platform.isIOS) { + // iOS 特定逻辑 +} else if (platform.isAndroid) { + // Android 特定逻辑 +} else if (platform.isHarmonyOS) { + // 鸿蒙特定逻辑 +} +``` + +### 6. 状态管理 + +使用 GetX 进行状态管理: + +```dart +// 定义响应式变量 +final count = 0.obs; + +// 更新值 +count.value++; + +// 监听变化 +Obx(() => Text('${count.value}')) +``` + +### 7. 路由管理 + +使用 GetX 进行路由管理: + +```dart +// 跳转到新页面 +Get.to(const NewPage()); + +// 返回上一页 +Get.back(); + +// 替换当前页面 +Get.off(const NewPage()); + +// 清空栈并跳转 +Get.offAll(const HomePage()); +``` + +### 8. 依赖注入 + +使用 GetX 进行依赖注入: + +```dart +// 注册服务 +Get.put(ThemeService()); + +// 获取服务 +final theme = Get.find(); +``` + +## 开发规范 + +### 1. 文件命名 + +- 文件名使用小写下划线:`theme_service.dart` +- 类名使用大驼峰:`ThemeService` + +### 2. 导入顺序 + +```dart +// 1. Flutter SDK +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; + +// 2. 第三方库 +import 'package:get/get.dart'; + +// 3. 项目内部文件 +import 'package:mom_kitchen/src/services/app_service.dart'; +``` + +### 3. 注释规范 + +- 使用中文注释 +- 公共方法必须添加注释说明 +- 复杂逻辑添加行内注释 + +### 4. 代码风格 + +- 使用 `const` 构造函数 +- 使用 `final` 定义不可变变量 +- 避免使用 `dynamic`,明确类型 + +## 常见问题 + +### Q1: 如何添加新的主题属性? + +1. 在 `ThemeService` 中添加新的响应式变量 +2. 在 `_loadTheme()` 和 `_saveTheme()` 中添加加载和保存逻辑 +3. 添加 getter 方法 +4. 在 `getThemeData()` 中应用新属性 +5. 更新本文档的主题属性表格 + +### Q2: 如何添加新的语言支持? + +1. 在 `l10n.yaml` 中添加新的语言代码 +2. 创建对应的 ARB 文件:`app_xx.arb` +3. 运行 `flutter gen-l10n` 生成代码 + +### Q3: 如何使用未使用的库? + +参考本文档"尚未使用的依赖库"表格中的建议使用场景,在相应的服务或页面中集成。 + +### Q4: 如何创建新页面? + +请参考 [PAGE_STANDARDS.md](./PAGE_STANDARDS.md#最佳实践) 中的最佳实践部分。 + +--- + +## 相关文档 + +| 文档 | 说明 | +|------|------| +| [PAGE_STANDARDS.md](./PAGE_STANDARDS.md) | 页面规范与验证系统详细说明 | +| [CHANGELOG.md](../../CHANGELOG.md) | 项目更新日志 | + +--- + +**最后更新时间:** 2026-04-08 +**维护者:** Mom's Kitchen 开发团队 diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..b2c6688 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:get/get.dart'; +import 'package:mom_kitchen/src/services/app_service.dart'; +import 'package:mom_kitchen/src/services/orientation_service.dart'; +import 'package:mom_kitchen/src/pages/home_page.dart'; +import 'package:mom_kitchen/src/pages/theme_demo_page.dart'; +import 'package:mom_kitchen/src/l10n/app_localizations.dart'; +import 'package:mom_kitchen/src/standards/app_pages.dart'; +import 'package:mom_kitchen/src/standards/page_validator.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + await AppService.instance.init(); + + await OrientationService().lockPortrait(); + + AppPages.registerAll(); + + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + final themeService = AppService.instance.theme; + + return GetMaterialApp( + title: 'Mom\'s Kitchen', + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + theme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: themeService.primaryColor, + brightness: themeService.isDarkMode + ? Brightness.dark + : Brightness.light, + ), + ), + darkTheme: ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: themeService.primaryColor, + brightness: Brightness.dark, + ), + ), + themeMode: themeService.isDarkMode ? ThemeMode.dark : ThemeMode.light, + home: const HomePage(), + getPages: [ + GetPage( + name: '/', + page: () { + AppPages.validateRoute('/'); + return const HomePage(); + }, + ), + GetPage( + name: '/theme', + page: () { + AppPages.validateRoute('/theme'); + return const ThemeDemoPage(); + }, + ), + ], + onGenerateRoute: (settings) { + AppPages.validateRoute(settings.name ?? ''); + return null; + }, + navigatorObservers: [PageRoutingObserver()], + ); + } +} + +class HomeScreen extends StatelessWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final themeService = AppService.instance.theme; + + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(l10n.appTitle), + backgroundColor: themeService.backgroundColor.withValues(alpha: 0.95), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.welcomeMessage, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: themeService.textColor, + ), + ), + const SizedBox(height: 20), + Text( + l10n.appDescription, + style: TextStyle( + fontSize: 16, + color: themeService.textColor.withValues(alpha: 0.6), + ), + ), + const SizedBox(height: 40), + CupertinoButton.filled( + onPressed: () { + Get.to(const HomePage()); + }, + child: Text(l10n.getStarted), + ), + const SizedBox(height: 20), + CupertinoButton.filled( + onPressed: () { + Get.to(const ThemeDemoPage()); + }, + child: Text(l10n.themeSettings), + ), + ], + ), + ), + ); + } +} + +class PageRoutingObserver extends NavigatorObserver { + @override + void didPush(Route route, Route? previousRoute) { + super.didPush(route, previousRoute); + _validateRoute(route.settings.name, previousRoute?.settings.name); + } + + @override + void didPop(Route route, Route? previousRoute) { + super.didPop(route, previousRoute); + _validateRoute(previousRoute?.settings.name, route.settings.name); + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + super.didReplace(newRoute: newRoute, oldRoute: oldRoute); + _validateRoute(newRoute?.settings.name, oldRoute?.settings.name); + } + + void _validateRoute(String? routeName, String? previousRouteName) { + final currentRoute = routeName ?? previousRouteName; + if (currentRoute == null || currentRoute.isEmpty) return; + + AppPages.validateRoute(currentRoute); + + if (kDebugMode && navigator?.context != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + PageValidator.validate(navigator!.context, currentRoute); + }); + } + } +} diff --git a/lib/src/config/api_config.dart b/lib/src/config/api_config.dart new file mode 100644 index 0000000..396d479 --- /dev/null +++ b/lib/src/config/api_config.dart @@ -0,0 +1,68 @@ +class ApiConfig { + // 认证相关 + static const String login = '/auth/login'; + static const String register = '/auth/register'; + static const String logout = '/auth/logout'; + static const String refreshToken = '/auth/refresh'; + + // 用户相关 + static const String userProfile = '/user/profile'; + static const String updateProfile = '/user/update'; + static const String changePassword = '/user/change-password'; + + // 产品相关 + static const String products = '/products'; + static const String productDetail = '/products/{id}'; + static const String productCategories = '/products/categories'; + static const String productSearch = '/products/search'; + + // 购物车相关 + static const String cart = '/cart'; + static const String addToCart = '/cart/add'; + static const String updateCart = '/cart/update'; + static const String removeFromCart = '/cart/remove'; + + // 订单相关 + static const String orders = '/orders'; + static const String orderDetail = '/orders/{id}'; + static const String createOrder = '/orders/create'; + static const String cancelOrder = '/orders/{id}/cancel'; + + // 支付相关 + static const String paymentMethods = '/payment/methods'; + static const String createPayment = '/payment/create'; + static const String verifyPayment = '/payment/verify'; + + // 地址相关 + static const String addresses = '/addresses'; + static const String addAddress = '/addresses/add'; + static const String updateAddress = '/addresses/{id}/update'; + static const String deleteAddress = '/addresses/{id}/delete'; + + // 通知相关 + static const String notifications = '/notifications'; + static const String markAsRead = '/notifications/{id}/read'; + + // 评论相关 + static const String productReviews = '/products/{id}/reviews'; + static const String addReview = '/products/{id}/reviews/add'; + + // 优惠券相关 + static const String coupons = '/coupons'; + static const String applyCoupon = '/coupons/apply'; + + // 公共相关 + static const String banners = '/banners'; + static const String settings = '/settings'; + static const String about = '/about'; + static const String contact = '/contact'; + + // 替换路径参数 + static String replaceParams(String path, Map params) { + var result = path; + params.forEach((key, value) { + result = result.replaceAll('{$key}', value.toString()); + }); + return result; + } +} \ No newline at end of file diff --git a/lib/src/config/app_config.dart b/lib/src/config/app_config.dart new file mode 100644 index 0000000..1c2278d --- /dev/null +++ b/lib/src/config/app_config.dart @@ -0,0 +1,63 @@ +import 'package:mom_kitchen/src/services/app_info_service.dart'; + +class AppConfig { + // 应用名称 + static String get appName => 'Mom\'s Kitchen'; + + // 应用版本(从 AppInfoService 获取) + static String get appVersion => AppInfoService().version; + + // 应用构建号(从 AppInfoService 获取) + static String get buildNumber => AppInfoService().buildNumber; + + // 完整版本信息(从 AppInfoService 获取) + static String get fullVersion => AppInfoService().fullVersion; + + // API 基础 URL + static const String baseUrl = 'https://api.example.com'; + + // 超时时间(秒) + static const int timeoutSeconds = 10; + + // 缓存时间(天) + static const int cacheDays = 7; + + // 分页大小 + static const int pageSize = 20; + + // 存储键值 + static const Map storageKeys = { + 'token': 'auth_token', + 'user': 'user_info', + 'language': 'app_language', + 'theme': 'app_theme', + }; + + // 支持的语言 + static const List supportedLanguages = [ + 'en', // English + 'zh', // Chinese + ]; + + // 默认语言 + static const String defaultLanguage = 'en'; + + // 主题模式 + static const Map themeModes = { + 'light': 'light', + 'dark': 'dark', + 'system': 'system', + }; + + // 默认主题 + static const String defaultTheme = 'system'; + + // 获取应用信息 + static Map get appInfo => { + 'appName': appName, + 'appVersion': appVersion, + 'buildNumber': buildNumber, + 'fullVersion': fullVersion, + 'baseUrl': baseUrl, + }; +} diff --git a/lib/src/l10n/app_en.arb b/lib/src/l10n/app_en.arb new file mode 100644 index 0000000..49ab658 --- /dev/null +++ b/lib/src/l10n/app_en.arb @@ -0,0 +1,17 @@ +{ + "@@locale": "en", + "appTitle": "Mom's Kitchen", + "welcomeMessage": "Welcome to Mom's Kitchen!", + "appDescription": "Your favorite food delivery app", + "getStarted": "Get Started", + "themeSettings": "Theme Settings", + "darkMode": "Dark Mode", + "statusBarImmersive": "Status Bar Immersive", + "fontSize": "Font Size", + "primaryColor": "Primary Color", + "secondaryColor": "Secondary Color", + "resetToDefault": "Reset to Default", + "preview": "Preview", + "sampleText": "This is a sample text to preview the theme settings.", + "sampleButton": "Sample Button" +} \ No newline at end of file diff --git a/lib/src/l10n/app_localizations.dart b/lib/src/l10n/app_localizations.dart new file mode 100644 index 0000000..819e631 --- /dev/null +++ b/lib/src/l10n/app_localizations.dart @@ -0,0 +1,231 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_zh.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('zh'), + Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), + ]; + + /// No description provided for @appTitle. + /// + /// In en, this message translates to: + /// **'Mom\'s Kitchen'** + String get appTitle; + + /// No description provided for @welcomeMessage. + /// + /// In en, this message translates to: + /// **'Welcome to Mom\'s Kitchen!'** + String get welcomeMessage; + + /// No description provided for @appDescription. + /// + /// In en, this message translates to: + /// **'Your favorite food delivery app'** + String get appDescription; + + /// No description provided for @getStarted. + /// + /// In en, this message translates to: + /// **'Get Started'** + String get getStarted; + + /// No description provided for @themeSettings. + /// + /// In en, this message translates to: + /// **'Theme Settings'** + String get themeSettings; + + /// No description provided for @darkMode. + /// + /// In en, this message translates to: + /// **'Dark Mode'** + String get darkMode; + + /// No description provided for @statusBarImmersive. + /// + /// In en, this message translates to: + /// **'Status Bar Immersive'** + String get statusBarImmersive; + + /// No description provided for @fontSize. + /// + /// In en, this message translates to: + /// **'Font Size'** + String get fontSize; + + /// No description provided for @primaryColor. + /// + /// In en, this message translates to: + /// **'Primary Color'** + String get primaryColor; + + /// No description provided for @secondaryColor. + /// + /// In en, this message translates to: + /// **'Secondary Color'** + String get secondaryColor; + + /// No description provided for @resetToDefault. + /// + /// In en, this message translates to: + /// **'Reset to Default'** + String get resetToDefault; + + /// No description provided for @preview. + /// + /// In en, this message translates to: + /// **'Preview'** + String get preview; + + /// No description provided for @sampleText. + /// + /// In en, this message translates to: + /// **'This is a sample text to preview the theme settings.'** + String get sampleText; + + /// No description provided for @sampleButton. + /// + /// In en, this message translates to: + /// **'Sample Button'** + String get sampleButton; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'zh'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when language+script codes are specified. + switch (locale.languageCode) { + case 'zh': + { + switch (locale.scriptCode) { + case 'Hant': + return AppLocalizationsZhHant(); + } + break; + } + } + + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'zh': + return AppLocalizationsZh(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/src/l10n/app_localizations_en.dart b/lib/src/l10n/app_localizations_en.dart new file mode 100644 index 0000000..f8ff4a3 --- /dev/null +++ b/lib/src/l10n/app_localizations_en.dart @@ -0,0 +1,53 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'Mom\'s Kitchen'; + + @override + String get welcomeMessage => 'Welcome to Mom\'s Kitchen!'; + + @override + String get appDescription => 'Your favorite food delivery app'; + + @override + String get getStarted => 'Get Started'; + + @override + String get themeSettings => 'Theme Settings'; + + @override + String get darkMode => 'Dark Mode'; + + @override + String get statusBarImmersive => 'Status Bar Immersive'; + + @override + String get fontSize => 'Font Size'; + + @override + String get primaryColor => 'Primary Color'; + + @override + String get secondaryColor => 'Secondary Color'; + + @override + String get resetToDefault => 'Reset to Default'; + + @override + String get preview => 'Preview'; + + @override + String get sampleText => + 'This is a sample text to preview the theme settings.'; + + @override + String get sampleButton => 'Sample Button'; +} diff --git a/lib/src/l10n/app_localizations_zh.dart b/lib/src/l10n/app_localizations_zh.dart new file mode 100644 index 0000000..a358922 --- /dev/null +++ b/lib/src/l10n/app_localizations_zh.dart @@ -0,0 +1,99 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Chinese (`zh`). +class AppLocalizationsZh extends AppLocalizations { + AppLocalizationsZh([String locale = 'zh']) : super(locale); + + @override + String get appTitle => '妈妈的厨房'; + + @override + String get welcomeMessage => '欢迎来到妈妈的厨房!'; + + @override + String get appDescription => '您最喜爱的美食配送应用'; + + @override + String get getStarted => '开始使用'; + + @override + String get themeSettings => '主题设置'; + + @override + String get darkMode => '深色模式'; + + @override + String get statusBarImmersive => '状态栏沉浸'; + + @override + String get fontSize => '字体大小'; + + @override + String get primaryColor => '主题色'; + + @override + String get secondaryColor => '次要色'; + + @override + String get resetToDefault => '重置为默认'; + + @override + String get preview => '预览'; + + @override + String get sampleText => '这是一段示例文本,用于预览主题设置。'; + + @override + String get sampleButton => '示例按钮'; +} + +/// The translations for Chinese, using the Han script (`zh_Hant`). +class AppLocalizationsZhHant extends AppLocalizationsZh { + AppLocalizationsZhHant() : super('zh_Hant'); + + @override + String get appTitle => '媽媽的廚房'; + + @override + String get welcomeMessage => '歡迎來到媽媽的廚房!'; + + @override + String get appDescription => '您最喜愛的美食配送應用'; + + @override + String get getStarted => '開始使用'; + + @override + String get themeSettings => '主題設置'; + + @override + String get darkMode => '深色模式'; + + @override + String get statusBarImmersive => '狀態欄沉浸'; + + @override + String get fontSize => '字體大小'; + + @override + String get primaryColor => '主題色'; + + @override + String get secondaryColor => '次要色'; + + @override + String get resetToDefault => '重置為預設'; + + @override + String get preview => '預覽'; + + @override + String get sampleText => '這是一段示例文本,用於預覽主題設置。'; + + @override + String get sampleButton => '示例按鈕'; +} diff --git a/lib/src/l10n/app_localizations_zh_hant.dart b/lib/src/l10n/app_localizations_zh_hant.dart new file mode 100644 index 0000000..81fb2c6 --- /dev/null +++ b/lib/src/l10n/app_localizations_zh_hant.dart @@ -0,0 +1,52 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Traditional Chinese (`zh_Hant`). +class AppLocalizationsZhHant extends AppLocalizations { + AppLocalizationsZhHant([String locale = 'zh_Hant']) : super(locale); + + @override + String get appTitle => '媽媽的廚房'; + + @override + String get welcomeMessage => '歡迎來到媽媽的廚房!'; + + @override + String get appDescription => '您最喜愛的美食配送應用'; + + @override + String get getStarted => '開始使用'; + + @override + String get themeSettings => '主題設置'; + + @override + String get darkMode => '深色模式'; + + @override + String get statusBarImmersive => '狀態欄沉浸'; + + @override + String get fontSize => '字體大小'; + + @override + String get primaryColor => '主題色'; + + @override + String get secondaryColor => '次要色'; + + @override + String get resetToDefault => '重置為預設'; + + @override + String get preview => '預覽'; + + @override + String get sampleText => '這是一段示例文本,用於預覽主題設置。'; + + @override + String get sampleButton => '示例按鈕'; +} diff --git a/lib/src/l10n/app_zh.arb b/lib/src/l10n/app_zh.arb new file mode 100644 index 0000000..f24fc96 --- /dev/null +++ b/lib/src/l10n/app_zh.arb @@ -0,0 +1,17 @@ +{ + "@@locale": "zh", + "appTitle": "妈妈的厨房", + "welcomeMessage": "欢迎来到妈妈的厨房!", + "appDescription": "您最喜爱的美食配送应用", + "getStarted": "开始使用", + "themeSettings": "主题设置", + "darkMode": "深色模式", + "statusBarImmersive": "状态栏沉浸", + "fontSize": "字体大小", + "primaryColor": "主题色", + "secondaryColor": "次要色", + "resetToDefault": "重置为默认", + "preview": "预览", + "sampleText": "这是一段示例文本,用于预览主题设置。", + "sampleButton": "示例按钮" +} \ No newline at end of file diff --git a/lib/src/l10n/app_zh_Hant.arb b/lib/src/l10n/app_zh_Hant.arb new file mode 100644 index 0000000..880f3bb --- /dev/null +++ b/lib/src/l10n/app_zh_Hant.arb @@ -0,0 +1,17 @@ +{ + "@@locale": "zh_Hant", + "appTitle": "媽媽的廚房", + "welcomeMessage": "歡迎來到媽媽的廚房!", + "appDescription": "您最喜愛的美食配送應用", + "getStarted": "開始使用", + "themeSettings": "主題設置", + "darkMode": "深色模式", + "statusBarImmersive": "狀態欄沉浸", + "fontSize": "字體大小", + "primaryColor": "主題色", + "secondaryColor": "次要色", + "resetToDefault": "重置為預設", + "preview": "預覽", + "sampleText": "這是一段示例文本,用於預覽主題設置。", + "sampleButton": "示例按鈕" +} diff --git a/lib/src/models/theme_model.dart b/lib/src/models/theme_model.dart new file mode 100644 index 0000000..430fa55 --- /dev/null +++ b/lib/src/models/theme_model.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; + +class ThemeModel { + final bool isDarkMode; + final Color primaryColor; + final Color secondaryColor; + final Color textColor; + final Color backgroundColor; + final bool isStatusBarImmersive; + final double fontSize; + + ThemeModel({ + required this.isDarkMode, + required this.primaryColor, + required this.secondaryColor, + required this.textColor, + required this.backgroundColor, + required this.isStatusBarImmersive, + required this.fontSize, + }); + + // 默认主题 + factory ThemeModel.defaultTheme() { + return ThemeModel( + isDarkMode: false, + primaryColor: Colors.blue, + secondaryColor: Colors.orange, + textColor: Colors.black, + backgroundColor: Colors.white, + isStatusBarImmersive: false, + fontSize: 16.0, + ); + } + + // 夜间主题 + factory ThemeModel.darkTheme() { + return ThemeModel( + isDarkMode: true, + primaryColor: Colors.blue, + secondaryColor: Colors.orange, + textColor: Colors.white, + backgroundColor: Colors.black, + isStatusBarImmersive: false, + fontSize: 16.0, + ); + } + + // 复制方法,用于修改主题 + ThemeModel copyWith({ + bool? isDarkMode, + Color? primaryColor, + Color? secondaryColor, + Color? textColor, + Color? backgroundColor, + bool? isStatusBarImmersive, + double? fontSize, + }) { + return ThemeModel( + isDarkMode: isDarkMode ?? this.isDarkMode, + primaryColor: primaryColor ?? this.primaryColor, + secondaryColor: secondaryColor ?? this.secondaryColor, + textColor: textColor ?? this.textColor, + backgroundColor: backgroundColor ?? this.backgroundColor, + isStatusBarImmersive: isStatusBarImmersive ?? this.isStatusBarImmersive, + fontSize: fontSize ?? this.fontSize, + ); + } + + // 转换为 ThemeData + ThemeData toThemeData() { + return ThemeData( + brightness: isDarkMode ? Brightness.dark : Brightness.light, + primaryColor: primaryColor, + secondaryHeaderColor: secondaryColor, + textTheme: TextTheme( + bodyLarge: TextStyle(color: textColor, fontSize: fontSize), + bodyMedium: TextStyle(color: textColor, fontSize: fontSize - 2), + bodySmall: TextStyle(color: textColor, fontSize: fontSize - 4), + titleLarge: TextStyle(color: textColor, fontSize: fontSize + 4), + titleMedium: TextStyle(color: textColor, fontSize: fontSize + 2), + titleSmall: TextStyle(color: textColor, fontSize: fontSize), + ), + scaffoldBackgroundColor: backgroundColor, + ); + } +} \ No newline at end of file diff --git a/lib/src/pages/cart_page.dart b/lib/src/pages/cart_page.dart new file mode 100644 index 0000000..558ce07 --- /dev/null +++ b/lib/src/pages/cart_page.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; + +class CartPage extends StatefulWidget { + const CartPage({Key? key}) : super(key: key); + + @override + State createState() => _CartPageState(); +} + +class _CartPageState extends State { + final List> cartItems = [ + { + 'id': 1, + 'name': 'Classic Pizza', + 'price': 29.99, + 'quantity': 1, + 'image': 'https://example.com/pizza.jpg', + }, + { + 'id': 2, + 'name': 'Burger', + 'price': 19.99, + 'quantity': 2, + 'image': 'https://example.com/burger.jpg', + }, + ]; + + double get totalPrice { + return cartItems.fold(0, (sum, item) => sum + (item['price'] * item['quantity'])); + } + + void updateQuantity(int id, int newQuantity) { + setState(() { + final item = cartItems.firstWhere((item) => item['id'] == id); + if (newQuantity > 0) { + item['quantity'] = newQuantity; + } else { + cartItems.remove(item); + } + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Your Cart'), + centerTitle: true, + ), + body: SafeArea( + child: Column( + children: [ + Expanded( + child: cartItems.isEmpty + ? const Center( + child: Text('Your cart is empty'), + ) + : ListView.builder( + itemCount: cartItems.length, + itemBuilder: (context, index) { + final item = cartItems[index]; + return ListTile( + leading: Image.network( + item['image'], + width: 80, + height: 80, + fit: BoxFit.cover, + ), + title: Text(item['name']), + subtitle: Text('\$${item['price']}'), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.remove), + onPressed: () { + updateQuantity(item['id'], item['quantity'] - 1); + }, + ), + Text(item['quantity'].toString()), + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + updateQuantity(item['id'], item['quantity'] + 1); + }, + ), + ], + ), + ); + }, + ), + ), + if (cartItems.isNotEmpty) + Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + border: Border(top: BorderSide(color: Colors.grey.shade200)), + ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Total:'), + Text('\$${totalPrice.toStringAsFixed(2)}'), + ], + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + // Handle checkout + }, + style: ElevatedButton.styleFrom( + minimumSize: const Size(double.infinity, 48), + ), + child: const Text('Checkout'), + ), + ], + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/pages/example_page.dart b/lib/src/pages/example_page.dart new file mode 100644 index 0000000..b212d0b --- /dev/null +++ b/lib/src/pages/example_page.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:mom_kitchen/src/services/app_service.dart'; +import 'package:mom_kitchen/src/l10n/app_localizations.dart'; +import 'package:mom_kitchen/src/widgets/skeleton_loader.dart'; + +class ExamplePage extends StatefulWidget { + const ExamplePage({Key? key}) : super(key: key); + + @override + State createState() => _ExamplePageState(); +} + +class _ExamplePageState extends State { + final _themeService = AppService.instance.theme; + bool _isLoading = true; + + @override + void initState() { + super.initState(); + // 模拟加载数据 + Future.delayed(const Duration(seconds: 2), () { + setState(() { + _isLoading = false; + }); + }); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + // 读取主题的所有值 + final isDarkMode = _themeService.isDarkMode; + final primaryColor = _themeService.primaryColor; + final secondaryColor = _themeService.secondaryColor; + final fontSize = _themeService.fontSize; + final isStatusBarImmersive = _themeService.isStatusBarImmersive; + final textColor = _themeService.textColor; + final backgroundColor = _themeService.backgroundColor; + final animationIntensity = _themeService.animationIntensity; + + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: Text(l10n.appTitle), + ), + child: Container( + color: backgroundColor, + child: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + // 页面标题 + Text( + 'Example Page', + style: TextStyle( + color: textColor, + fontSize: fontSize + 8, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + + // 主题值展示 + Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: secondaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: secondaryColor, width: 1), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Theme Values', + style: TextStyle( + color: textColor, + fontSize: fontSize + 4, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + _buildThemeValueRow('Dark Mode', isDarkMode.toString(), textColor), + _buildThemeValueRow('Primary Color', primaryColor.toString(), textColor), + _buildThemeValueRow('Secondary Color', secondaryColor.toString(), textColor), + _buildThemeValueRow('Font Size', fontSize.toString(), textColor), + _buildThemeValueRow('Status Bar Immersive', isStatusBarImmersive.toString(), textColor), + _buildThemeValueRow('Text Color', textColor.toString(), textColor), + _buildThemeValueRow('Background Color', backgroundColor.toString(), textColor), + _buildThemeValueRow('Animation Intensity', animationIntensity.toString(), textColor), + ], + ), + ), + const SizedBox(height: 20), + + // 骨架屏示例 + Text( + 'Skeleton Loader Example', + style: TextStyle( + color: textColor, + fontSize: fontSize + 4, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + SkeletonContainer( + isLoading: _isLoading, + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: secondaryColor.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: secondaryColor, width: 1), + ), + child: Text( + 'This content is loaded after the skeleton disappears', + style: TextStyle( + color: textColor, + fontSize: fontSize, + ), + ), + ), + ), + const SizedBox(height: 20), + + // 动画示例 + Text( + 'Animation Example', + style: TextStyle( + color: textColor, + fontSize: fontSize + 4, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + GestureDetector( + onTap: () { + // 这里可以添加动画效果,使用 animationIntensity 来控制动画强度 + }, + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: primaryColor, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'Tap for Animation', + style: TextStyle( + color: textColor, + fontSize: fontSize, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildThemeValueRow(String label, String value, Color textColor) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + color: textColor, + fontSize: _themeService.fontSize, + ), + ), + Text( + value, + style: TextStyle( + color: textColor, + fontSize: _themeService.fontSize, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/pages/theme_demo_page.dart b/lib/src/pages/theme_demo_page.dart new file mode 100644 index 0000000..f0dbc97 --- /dev/null +++ b/lib/src/pages/theme_demo_page.dart @@ -0,0 +1,561 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:mom_kitchen/src/services/app_service.dart'; +import 'package:mom_kitchen/src/services/theme_service.dart'; +import 'package:mom_kitchen/src/services/animation_service.dart'; +import 'package:mom_kitchen/src/l10n/app_localizations.dart'; + +class ThemeDemoPage extends StatefulWidget { + const ThemeDemoPage({super.key}); + + @override + State createState() => _ThemeDemoPageState(); +} + +class _ThemeDemoPageState extends State { + late ThemeService _themeService; + late AnimationService _animationService; + + @override + void initState() { + super.initState(); + _themeService = AppService.instance.theme; + _animationService = AppService.instance.animation; + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar(middle: Text(l10n.themeSettings)), + child: Container( + color: _themeService.backgroundColor, + child: ListView( + padding: const EdgeInsets.all(16.0), + children: [ + _buildSectionHeader('🎨 Theme Settings', CupertinoIcons.paintbrush), + + CupertinoListTile( + title: Text( + l10n.darkMode, + style: TextStyle(color: _themeService.textColor), + ), + trailing: CupertinoSwitch( + value: _themeService.isDarkMode, + onChanged: (value) async { + await _themeService.toggleThemeMode(); + setState(() {}); + }, + ), + ), + + CupertinoListTile( + title: Text( + l10n.statusBarImmersive, + style: TextStyle(color: _themeService.textColor), + ), + trailing: CupertinoSwitch( + value: _themeService.isStatusBarImmersive, + onChanged: (value) async { + await _themeService.setStatusBarImmersive(value); + setState(() {}); + }, + ), + ), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${l10n.fontSize}: ${_themeService.fontSize.toStringAsFixed(1)}', + style: TextStyle(color: _themeService.textColor), + ), + const SizedBox(height: 8), + CupertinoSlider( + value: _themeService.fontSize, + min: 12.0, + max: 24.0, + divisions: 12, + onChanged: (value) async { + await _themeService.setFontSize(value); + setState(() {}); + }, + ), + ], + ), + ), + + CupertinoListTile( + title: Text( + l10n.primaryColor, + style: TextStyle(color: _themeService.textColor), + ), + trailing: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: _themeService.primaryColor, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: _themeService.textColor, width: 2), + ), + ), + onTap: () => _showColorPicker('primary'), + ), + + CupertinoListTile( + title: Text( + l10n.secondaryColor, + style: TextStyle(color: _themeService.textColor), + ), + trailing: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: _themeService.secondaryColor, + borderRadius: BorderRadius.circular(20), + border: Border.all(color: _themeService.textColor, width: 2), + ), + ), + onTap: () => _showColorPicker('secondary'), + ), + + const SizedBox(height: 24), + _buildSectionHeader('✨ Animation Settings', CupertinoIcons.sparkles), + + CupertinoListTile( + title: Text( + 'Enable Animations', + style: TextStyle(color: _themeService.textColor), + ), + trailing: CupertinoSwitch( + value: _animationService.enabled, + onChanged: (value) async { + await _animationService.setEnabled(value); + setState(() {}); + }, + ), + ), + + _buildPresetSelector(), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Animation Speed: ${_animationService.speed.toStringAsFixed(1)}x', + style: TextStyle(color: _themeService.textColor), + ), + const SizedBox(height: 8), + CupertinoSlider( + value: _animationService.speed, + min: 0.1, + max: 3.0, + divisions: 29, + onChanged: _animationService.enabled ? (value) async { + await _animationService.setSpeed(value); + setState(() {}); + } : null, + ), + ], + ), + ), + + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Animation Intensity: ${_animationService.intensity.toStringAsFixed(1)}', + style: TextStyle(color: _themeService.textColor), + ), + const SizedBox(height: 8), + CupertinoSlider( + value: _animationService.intensity, + min: 0.0, + max: 2.0, + divisions: 20, + onChanged: _animationService.enabled ? (value) async { + await _animationService.setIntensity(value); + setState(() {}); + } : null, + ), + ], + ), + ), + + _buildCurveSelector(), + + const SizedBox(height: 24), + _buildSectionHeader('👁️ Preview', CupertinoIcons.eye), + + _buildPreviewArea(), + + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Expanded( + child: CupertinoButton.filled( + onPressed: () async { + await _themeService.resetToDefault(); + await _animationService.resetToDefault(); + setState(() {}); + }, + child: const Text('Reset All'), + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildSectionHeader(String title, IconData icon) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: Row( + children: [ + Icon(icon, color: _themeService.primaryColor, size: 20), + const SizedBox(width: 8), + Text( + title, + style: TextStyle( + fontSize: _themeService.fontSize + 2, + fontWeight: FontWeight.w600, + color: _themeService.textColor, + ), + ), + ], + ), + ); + } + + Widget _buildPresetSelector() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Animation Preset', + style: TextStyle(color: _themeService.textColor), + ), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: AnimationPreset.values.map((preset) { + final isSelected = _animationService.preset == preset; + return GestureDetector( + onTap: _animationService.enabled ? () async { + await _animationService.setPreset(preset); + setState(() {}); + } : null, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isSelected + ? _themeService.primaryColor + : _themeService.backgroundColor, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected + ? _themeService.primaryColor + : _themeService.textColor.withValues(alpha: 0.3), + ), + ), + child: Text( + _getPresetName(preset), + style: TextStyle( + color: isSelected + ? Colors.white + : _themeService.textColor, + fontSize: _themeService.fontSize - 2, + ), + ), + ), + ); + }).toList(), + ), + ], + ), + ); + } + + String _getPresetName(AnimationPreset preset) { + switch (preset) { + case AnimationPreset.standard: + return 'Standard'; + case AnimationPreset.fast: + return '⚡ Fast'; + case AnimationPreset.slow: + return '🐢 Slow'; + case AnimationPreset.smooth: + return '🌊 Smooth'; + case AnimationPreset.bouncy: + return '🏀 Bouncy'; + case AnimationPreset.minimal: + return '✨ Minimal'; + case AnimationPreset.none: + return '❌ None'; + } + } + + Widget _buildCurveSelector() { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Animation Curve', + style: TextStyle(color: _themeService.textColor), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: _themeService.backgroundColor, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: _themeService.textColor.withValues(alpha: 0.3), + ), + ), + child: CupertinoButton( + padding: EdgeInsets.zero, + onPressed: _animationService.enabled ? () => _showCurvePicker() : null, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + AnimationCurves.getCurveName(_animationService.curveType), + style: TextStyle(color: _themeService.textColor), + ), + Icon( + CupertinoIcons.chevron_down, + color: _themeService.textColor, + ), + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildPreviewArea() { + return Container( + margin: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: _themeService.secondaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: _themeService.secondaryColor, + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Preview', + style: TextStyle( + color: _themeService.textColor, + fontSize: _themeService.fontSize + 4, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + Text( + 'This is a sample text to preview the theme settings.', + style: TextStyle( + color: _themeService.textColor, + fontSize: _themeService.fontSize, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + AnimatedButton( + onPressed: () {}, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: _themeService.primaryColor, + borderRadius: BorderRadius.circular(8), + ), + child: const Text( + 'Animated Button', + style: TextStyle(color: Colors.white), + ), + ), + ), + ], + ), + const SizedBox(height: 16), + AnimatedListView( + children: List.generate(3, (index) => _buildPreviewItem(index)), + ), + ], + ), + ); + } + + Widget _buildPreviewItem(int index) { + return Container( + margin: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: _themeService.backgroundColor, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: _themeService.textColor.withValues(alpha: 0.1), + ), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: _themeService.primaryColor.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + [CupertinoIcons.star, CupertinoIcons.heart, CupertinoIcons.bell][index], + color: _themeService.primaryColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Preview Item ${index + 1}', + style: TextStyle(color: _themeService.textColor), + ), + ), + ], + ), + ); + } + + void _showColorPicker(String type) { + showCupertinoModalPopup( + context: context, + builder: (context) => Container( + height: 300, + padding: const EdgeInsets.only(top: 6.0), + decoration: BoxDecoration( + color: _themeService.backgroundColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + children: [ + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: _themeService.textColor.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + Expanded( + child: GridView.count( + crossAxisCount: 6, + padding: const EdgeInsets.all(16), + children: [ + Colors.red, Colors.pink, Colors.purple, Colors.deepPurple, + Colors.indigo, Colors.blue, Colors.lightBlue, Colors.cyan, + Colors.teal, Colors.green, Colors.lightGreen, Colors.lime, + Colors.yellow, Colors.amber, Colors.orange, Colors.deepOrange, + Colors.brown, Colors.grey, + ].map((color) { + return GestureDetector( + onTap: () async { + if (type == 'primary') { + await _themeService.setPrimaryColor(color); + } else { + await _themeService.setSecondaryColor(color); + } + Navigator.pop(context); + setState(() {}); + }, + child: Container( + margin: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + ), + ); + }).toList(), + ), + ), + ], + ), + ), + ); + } + + void _showCurvePicker() { + showCupertinoModalPopup( + context: context, + builder: (context) => Container( + height: 300, + padding: const EdgeInsets.only(top: 6.0), + decoration: BoxDecoration( + color: _themeService.backgroundColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + children: [ + Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: _themeService.textColor.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(2), + ), + ), + Expanded( + child: ListView.builder( + itemCount: AnimationCurveType.values.length, + itemBuilder: (context, index) { + final curveType = AnimationCurveType.values[index]; + final isSelected = _animationService.curveType == curveType; + + return CupertinoListTile( + title: Text( + AnimationCurves.getCurveName(curveType), + style: TextStyle(color: _themeService.textColor), + ), + trailing: isSelected + ? Icon(CupertinoIcons.checkmark_circle_fill, + color: _themeService.primaryColor) + : null, + onTap: () async { + await _animationService.setCurveType(curveType); + Navigator.pop(context); + setState(() {}); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/services/animation_service.dart b/lib/src/services/animation_service.dart new file mode 100644 index 0000000..c826ebd --- /dev/null +++ b/lib/src/services/animation_service.dart @@ -0,0 +1,867 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:animations/animations.dart'; + +enum AnimationPreset { standard, fast, slow, smooth, bouncy, minimal, none } + +enum AnimationCurveType { + linear, + easeIn, + easeOut, + easeInOut, + curveEase, + bounceOut, + elasticOut, + fastOutSlowIn, + slowMiddle, +} + +class AnimationConfig { + final bool enabled; + final double speed; + final double intensity; + final AnimationCurveType curveType; + final AnimationPreset preset; + + const AnimationConfig({ + this.enabled = true, + this.speed = 1.0, + this.intensity = 1.0, + this.curveType = AnimationCurveType.easeInOut, + this.preset = AnimationPreset.standard, + }); + + AnimationConfig copyWith({ + bool? enabled, + double? speed, + double? intensity, + AnimationCurveType? curveType, + AnimationPreset? preset, + }) { + return AnimationConfig( + enabled: enabled ?? this.enabled, + speed: speed ?? this.speed, + intensity: intensity ?? this.intensity, + curveType: curveType ?? this.curveType, + preset: preset ?? this.preset, + ); + } + + Duration get baseDuration => Duration(milliseconds: (300 / speed).round()); + + Duration get shortDuration => Duration(milliseconds: (150 / speed).round()); + + Duration get longDuration => Duration(milliseconds: (500 / speed).round()); + + Curve get curve => AnimationCurves.getCurve(curveType, intensity); + + static AnimationConfig fromPreset(AnimationPreset preset) { + switch (preset) { + case AnimationPreset.standard: + return const AnimationConfig( + enabled: true, + speed: 1.0, + intensity: 1.0, + curveType: AnimationCurveType.easeInOut, + preset: AnimationPreset.standard, + ); + case AnimationPreset.fast: + return const AnimationConfig( + enabled: true, + speed: 2.0, + intensity: 0.8, + curveType: AnimationCurveType.fastOutSlowIn, + preset: AnimationPreset.fast, + ); + case AnimationPreset.slow: + return const AnimationConfig( + enabled: true, + speed: 0.5, + intensity: 1.2, + curveType: AnimationCurveType.slowMiddle, + preset: AnimationPreset.slow, + ); + case AnimationPreset.smooth: + return const AnimationConfig( + enabled: true, + speed: 0.8, + intensity: 1.0, + curveType: AnimationCurveType.curveEase, + preset: AnimationPreset.smooth, + ); + case AnimationPreset.bouncy: + return const AnimationConfig( + enabled: true, + speed: 1.0, + intensity: 1.5, + curveType: AnimationCurveType.bounceOut, + preset: AnimationPreset.bouncy, + ); + case AnimationPreset.minimal: + return const AnimationConfig( + enabled: true, + speed: 1.5, + intensity: 0.3, + curveType: AnimationCurveType.linear, + preset: AnimationPreset.minimal, + ); + case AnimationPreset.none: + return const AnimationConfig( + enabled: false, + speed: 1.0, + intensity: 0.0, + curveType: AnimationCurveType.linear, + preset: AnimationPreset.none, + ); + } + } + + Map toJson() => { + 'enabled': enabled, + 'speed': speed, + 'intensity': intensity, + 'curveType': curveType.index, + 'preset': preset.index, + }; + + factory AnimationConfig.fromJson(Map json) { + return AnimationConfig( + enabled: json['enabled'] as bool? ?? true, + speed: (json['speed'] as num?)?.toDouble() ?? 1.0, + intensity: (json['intensity'] as num?)?.toDouble() ?? 1.0, + curveType: AnimationCurveType.values[json['curveType'] as int? ?? 0], + preset: AnimationPreset.values[json['preset'] as int? ?? 0], + ); + } +} + +class AnimationCurves { + static Curve getCurve(AnimationCurveType type, double intensity) { + switch (type) { + case AnimationCurveType.linear: + return Curves.linear; + case AnimationCurveType.easeIn: + return Curves.easeIn; + case AnimationCurveType.easeOut: + return Curves.easeOut; + case AnimationCurveType.easeInOut: + return Curves.easeInOut; + case AnimationCurveType.curveEase: + return Curves.ease; + case AnimationCurveType.bounceOut: + return Curves.bounceOut; + case AnimationCurveType.elasticOut: + return Curves.elasticOut; + case AnimationCurveType.fastOutSlowIn: + return Curves.fastOutSlowIn; + case AnimationCurveType.slowMiddle: + return Curves.slowMiddle; + } + } + + static String getCurveName(AnimationCurveType type) { + switch (type) { + case AnimationCurveType.linear: + return 'Linear'; + case AnimationCurveType.easeIn: + return 'Ease In'; + case AnimationCurveType.easeOut: + return 'Ease Out'; + case AnimationCurveType.easeInOut: + return 'Ease In Out'; + case AnimationCurveType.curveEase: + return 'Ease'; + case AnimationCurveType.bounceOut: + return 'Bounce'; + case AnimationCurveType.elasticOut: + return 'Elastic'; + case AnimationCurveType.fastOutSlowIn: + return 'Fast Out Slow In'; + case AnimationCurveType.slowMiddle: + return 'Slow Middle'; + } + } +} + +class AnimationService extends GetxService { + static AnimationService get to => Get.find(); + + final _config = Rx(const AnimationConfig()); + late SharedPreferences _prefs; + + AnimationConfig get config => _config.value; + bool get enabled => _config.value.enabled; + double get speed => _config.value.speed; + double get intensity => _config.value.intensity; + AnimationCurveType get curveType => _config.value.curveType; + AnimationPreset get preset => _config.value.preset; + Duration get baseDuration => _config.value.baseDuration; + Duration get shortDuration => _config.value.shortDuration; + Duration get longDuration => _config.value.longDuration; + Curve get curve => _config.value.curve; + + Future init() async { + _prefs = await SharedPreferences.getInstance(); + await _loadConfig(); + return this; + } + + Future _loadConfig() async { + final enabled = _prefs.getBool('animation_enabled') ?? true; + final speed = _prefs.getDouble('animation_speed') ?? 1.0; + final intensity = _prefs.getDouble('animation_intensity') ?? 1.0; + final curveIndex = _prefs.getInt('animation_curve') ?? 3; + final presetIndex = _prefs.getInt('animation_preset') ?? 0; + + _config.value = AnimationConfig( + enabled: enabled, + speed: speed, + intensity: intensity, + curveType: AnimationCurveType.values[curveIndex], + preset: AnimationPreset.values[presetIndex], + ); + } + + Future _saveConfig() async { + await _prefs.setBool('animation_enabled', _config.value.enabled); + await _prefs.setDouble('animation_speed', _config.value.speed); + await _prefs.setDouble('animation_intensity', _config.value.intensity); + await _prefs.setInt('animation_curve', _config.value.curveType.index); + await _prefs.setInt('animation_preset', _config.value.preset.index); + } + + Future setEnabled(bool enabled) async { + _config.value = _config.value.copyWith(enabled: enabled); + await _saveConfig(); + } + + Future setSpeed(double speed) async { + _config.value = _config.value.copyWith(speed: speed.clamp(0.1, 3.0)); + await _saveConfig(); + } + + Future setIntensity(double intensity) async { + _config.value = _config.value.copyWith( + intensity: intensity.clamp(0.0, 2.0), + ); + await _saveConfig(); + } + + Future setCurveType(AnimationCurveType curveType) async { + _config.value = _config.value.copyWith(curveType: curveType); + await _saveConfig(); + } + + Future setPreset(AnimationPreset preset) async { + _config.value = AnimationConfig.fromPreset(preset); + await _saveConfig(); + } + + Future setCustomConfig({ + bool? enabled, + double? speed, + double? intensity, + AnimationCurveType? curveType, + }) async { + _config.value = _config.value.copyWith( + enabled: enabled, + speed: speed, + intensity: intensity, + curveType: curveType, + preset: AnimationPreset.standard, + ); + await _saveConfig(); + } + + Future resetToDefault() async { + _config.value = const AnimationConfig(); + await _saveConfig(); + } + + Duration getDuration(AnimationDurationType type) { + if (!enabled) return Duration.zero; + + switch (type) { + case AnimationDurationType.short: + return shortDuration; + case AnimationDurationType.base: + return baseDuration; + case AnimationDurationType.long: + return longDuration; + } + } +} + +enum AnimationDurationType { short, base, long } + +class PageTransitions { + static Widget fadeThrough(Widget child, AnimationConfig config) { + if (!config.enabled) return child; + + return FadeThroughTransition( + animation: CurvedAnimation( + parent: AlwaysStoppedAnimation(1.0), + curve: config.curve, + ), + secondaryAnimation: CurvedAnimation( + parent: AlwaysStoppedAnimation(1.0), + curve: config.curve, + ), + fillColor: Colors.transparent, + child: child, + ); + } + + static Widget fadeScale(Widget child, AnimationConfig config) { + if (!config.enabled) return child; + + return FadeScaleTransition( + animation: CurvedAnimation( + parent: AlwaysStoppedAnimation(1.0), + curve: config.curve, + ), + child: child, + ); + } + + static Widget sharedAxis({ + required Widget child, + required AnimationConfig config, + SharedAxisTransitionType type = SharedAxisTransitionType.horizontal, + }) { + if (!config.enabled) return child; + + return SharedAxisTransition( + animation: CurvedAnimation( + parent: AlwaysStoppedAnimation(1.0), + curve: config.curve, + ), + secondaryAnimation: CurvedAnimation( + parent: AlwaysStoppedAnimation(1.0), + curve: config.curve, + ), + transitionType: type, + fillColor: Colors.transparent, + child: child, + ); + } + + static PageTransitionsBuilder getTransitionBuilder(AnimationConfig config) { + return _AdaptivePageTransitionsBuilder(config: config); + } +} + +class _AdaptivePageTransitionsBuilder extends PageTransitionsBuilder { + final AnimationConfig config; + + const _AdaptivePageTransitionsBuilder({required this.config}); + + @override + Widget buildTransitions( + PageRoute route, + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + if (!config.enabled) return child; + + if (route.settings.name == '/') { + return child; + } + + return SharedAxisTransition( + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.horizontal, + fillColor: Colors.transparent, + child: child, + ); + } +} + +class AnimatedListItem extends StatelessWidget { + final Widget child; + final int index; + final Duration? duration; + final AnimationConfig? config; + + const AnimatedListItem({ + super.key, + required this.child, + required this.index, + this.duration, + this.config, + }); + + @override + Widget build(BuildContext context) { + final animConfig = config ?? AnimationService.to.config; + if (!animConfig.enabled) return child; + + final animDuration = duration ?? animConfig.baseDuration; + final delay = Duration( + milliseconds: (index * 50 * (1 / animConfig.speed)).round(), + ); + + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: animDuration + delay, + curve: animConfig.curve, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset(0, 30 * (1 - value) * animConfig.intensity), + child: child, + ), + ); + }, + child: child, + ); + } +} + +class AnimatedListView extends StatelessWidget { + final List children; + final Duration? itemDuration; + final Duration? staggerDelay; + final AnimationConfig? config; + + const AnimatedListView({ + super.key, + required this.children, + this.itemDuration, + this.staggerDelay, + this.config, + }); + + @override + Widget build(BuildContext context) { + final animConfig = config ?? AnimationService.to.config; + if (!animConfig.enabled) { + return Column(children: children); + } + + return Column( + children: children.asMap().entries.map((entry) { + return AnimatedListItem( + index: entry.key, + duration: itemDuration, + config: animConfig, + child: entry.value, + ); + }).toList(), + ); + } +} + +class AnimatedGridView extends StatelessWidget { + final List children; + final int crossAxisCount; + final double spacing; + final double runSpacing; + final Duration? itemDuration; + final AnimationConfig? config; + + const AnimatedGridView({ + super.key, + required this.children, + this.crossAxisCount = 2, + this.spacing = 16.0, + this.runSpacing = 16.0, + this.itemDuration, + this.config, + }); + + @override + Widget build(BuildContext context) { + final animConfig = config ?? AnimationService.to.config; + + return GridView.count( + crossAxisCount: crossAxisCount, + mainAxisSpacing: runSpacing, + crossAxisSpacing: spacing, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: children.asMap().entries.map((entry) { + if (!animConfig.enabled) return entry.value; + + return AnimatedListItem( + index: entry.key, + duration: itemDuration, + config: animConfig, + child: entry.value, + ); + }).toList(), + ); + } +} + +class AnimatedButton extends StatefulWidget { + final Widget child; + final VoidCallback? onPressed; + final Duration? duration; + final double scale; + final AnimationConfig? config; + + const AnimatedButton({ + super.key, + required this.child, + this.onPressed, + this.duration, + this.scale = 0.95, + this.config, + }); + + @override + State createState() => _AnimatedButtonState(); +} + +class _AnimatedButtonState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.duration ?? const Duration(milliseconds: 100), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 1.0, + end: widget.scale, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _onTapDown(TapDownDetails details) { + _controller.forward(); + } + + void _onTapUp(TapUpDetails details) { + _controller.reverse(); + widget.onPressed?.call(); + } + + void _onTapCancel() { + _controller.reverse(); + } + + @override + Widget build(BuildContext context) { + final config = widget.config ?? AnimationService.to.config; + if (!config.enabled) { + return GestureDetector(onTap: widget.onPressed, child: widget.child); + } + + return GestureDetector( + onTapDown: _onTapDown, + onTapUp: _onTapUp, + onTapCancel: _onTapCancel, + child: ScaleTransition(scale: _scaleAnimation, child: widget.child), + ); + } +} + +class AnimatedCard extends StatefulWidget { + final Widget child; + final VoidCallback? onTap; + final Duration? duration; + final double elevation; + final Color? backgroundColor; + final BorderRadius? borderRadius; + final AnimationConfig? config; + + const AnimatedCard({ + super.key, + required this.child, + this.onTap, + this.duration, + this.elevation = 4.0, + this.backgroundColor, + this.borderRadius, + this.config, + }); + + @override + State createState() => _AnimatedCardState(); +} + +class _AnimatedCardState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _elevationAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.duration ?? const Duration(milliseconds: 150), + vsync: this, + ); + _scaleAnimation = Tween( + begin: 1.0, + end: 0.98, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + _elevationAnimation = Tween( + begin: widget.elevation, + end: 2.0, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final config = widget.config ?? AnimationService.to.config; + + return GestureDetector( + onTapDown: config.enabled ? (_) => _controller.forward() : null, + onTapUp: config.enabled ? (_) => _controller.reverse() : null, + onTapCancel: config.enabled ? () => _controller.reverse() : null, + onTap: widget.onTap, + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Transform.scale( + scale: config.enabled ? _scaleAnimation.value : 1.0, + child: Material( + elevation: config.enabled + ? _elevationAnimation.value + : widget.elevation, + borderRadius: widget.borderRadius ?? BorderRadius.circular(12), + color: widget.backgroundColor, + child: child, + ), + ); + }, + child: widget.child, + ), + ); + } +} + +class FadeInWidget extends StatelessWidget { + final Widget child; + final Duration? duration; + final Offset? offset; + final AnimationConfig? config; + + const FadeInWidget({ + super.key, + required this.child, + this.duration, + this.offset, + this.config, + }); + + @override + Widget build(BuildContext context) { + final animConfig = config ?? AnimationService.to.config; + if (!animConfig.enabled) return child; + + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: duration ?? animConfig.baseDuration, + curve: animConfig.curve, + builder: (context, value, child) { + final slideOffset = offset ?? const Offset(0, 20); + return Opacity( + opacity: value, + child: Transform.translate( + offset: Offset( + slideOffset.dx * (1 - value), + slideOffset.dy * (1 - value) * animConfig.intensity, + ), + child: child, + ), + ); + }, + child: child, + ); + } +} + +class SlideInWidget extends StatelessWidget { + final Widget child; + final Duration? duration; + final Offset beginOffset; + final AnimationConfig? config; + + const SlideInWidget({ + super.key, + required this.child, + this.duration, + this.beginOffset = const Offset(1.0, 0.0), + this.config, + }); + + @override + Widget build(BuildContext context) { + final animConfig = config ?? AnimationService.to.config; + if (!animConfig.enabled) return child; + + return TweenAnimationBuilder( + tween: Tween(begin: beginOffset, end: Offset.zero), + duration: duration ?? animConfig.baseDuration, + curve: animConfig.curve, + builder: (context, value, child) { + return FractionalTranslation(translation: value, child: child); + }, + child: child, + ); + } +} + +class ScaleInWidget extends StatelessWidget { + final Widget child; + final Duration? duration; + final double beginScale; + final AnimationConfig? config; + + const ScaleInWidget({ + super.key, + required this.child, + this.duration, + this.beginScale = 0.0, + this.config, + }); + + @override + Widget build(BuildContext context) { + final animConfig = config ?? AnimationService.to.config; + if (!animConfig.enabled) return child; + + return TweenAnimationBuilder( + tween: Tween(begin: beginScale, end: 1.0), + duration: duration ?? animConfig.baseDuration, + curve: animConfig.curve, + builder: (context, value, child) { + return Transform.scale( + scale: value, + child: Opacity(opacity: value, child: child), + ); + }, + child: child, + ); + } +} + +class StaggeredAnimation extends StatelessWidget { + final List children; + final Duration? itemDelay; + final Duration? itemDuration; + final Axis direction; + final AnimationConfig? config; + + const StaggeredAnimation({ + super.key, + required this.children, + this.itemDelay, + this.itemDuration, + this.direction = Axis.vertical, + this.config, + }); + + @override + Widget build(BuildContext context) { + final animConfig = config ?? AnimationService.to.config; + if (!animConfig.enabled) { + return direction == Axis.vertical + ? Column(children: children) + : Row(children: children); + } + + final delay = + itemDelay ?? Duration(milliseconds: (100 / animConfig.speed).round()); + final duration = itemDuration ?? animConfig.baseDuration; + + final animatedChildren = children.asMap().entries.map((entry) { + final index = entry.key; + final totalDelay = Duration(milliseconds: delay.inMilliseconds * index); + + return TweenAnimationBuilder( + tween: Tween(begin: 0.0, end: 1.0), + duration: duration + totalDelay, + curve: animConfig.curve, + builder: (context, value, child) { + return Opacity( + opacity: value, + child: Transform.translate( + offset: direction == Axis.vertical + ? Offset(0, 30 * (1 - value) * animConfig.intensity) + : Offset(30 * (1 - value) * animConfig.intensity, 0), + child: child, + ), + ); + }, + child: entry.value, + ); + }).toList(); + + return direction == Axis.vertical + ? Column(children: animatedChildren) + : Row(children: animatedChildren); + } +} + +class OpenContainerWrapper extends StatelessWidget { + final Widget closedChild; + final Widget Function(BuildContext context, VoidCallback action) openBuilder; + final Color? closedColor; + final Color? openColor; + final double closedElevation; + final double openElevation; + final BorderRadius? closedBorderRadius; + final ShapeBorder? closedShape; + final AnimationConfig? config; + + const OpenContainerWrapper({ + super.key, + required this.closedChild, + required this.openBuilder, + this.closedColor, + this.openColor, + this.closedElevation = 0.0, + this.openElevation = 0.0, + this.closedBorderRadius, + this.closedShape, + this.config, + }); + + @override + Widget build(BuildContext context) { + final animConfig = config ?? AnimationService.to.config; + + return OpenContainer( + transitionDuration: animConfig.enabled + ? animConfig.baseDuration + : Duration.zero, + openBuilder: openBuilder, + closedElevation: closedElevation, + closedColor: closedColor ?? Colors.transparent, + openColor: openColor ?? Colors.transparent, + openElevation: openElevation, + closedShape: + closedShape ?? + RoundedRectangleBorder( + borderRadius: closedBorderRadius ?? BorderRadius.circular(12), + ), + closedBuilder: (context, action) => closedChild, + ); + } +} diff --git a/lib/src/services/api_service.dart b/lib/src/services/api_service.dart new file mode 100644 index 0000000..31673e7 --- /dev/null +++ b/lib/src/services/api_service.dart @@ -0,0 +1,104 @@ +import 'package:dio/dio.dart'; +import 'package:dio_cache_interceptor/dio_cache_interceptor.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; + +class ApiService { + static final ApiService _instance = ApiService._internal(); + factory ApiService() => _instance; + + late Dio _dio; + + ApiService._internal() { + final options = BaseOptions( + baseUrl: 'https://api.example.com', + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 10), + ); + + _dio = Dio(options); + + // 添加缓存拦截器 + final cacheOptions = CacheOptions( + store: MemCacheStore(), + policy: CachePolicy.request, + hitCacheOnErrorExcept: [401, 403], + maxStale: const Duration(days: 7), + priority: CachePriority.normal, + cipher: null, + keyBuilder: CacheOptions.defaultCacheKeyBuilder, + allowPostMethod: false, + ); + + _dio.interceptors.add(DioCacheInterceptor(options: cacheOptions)); + + // 添加请求拦截器 + _dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) { + // 可以在这里添加认证token等 + return handler.next(options); + }, + onResponse: (response, handler) { + return handler.next(response); + }, + onError: (DioException e, handler) { + return handler.next(e); + }, + )); + } + + Future get(String path, {Map? queryParameters}) async { + try { + final connectivityResult = await Connectivity().checkConnectivity(); + if (connectivityResult == ConnectivityResult.none) { + throw Exception('No internet connection'); + } + + final response = await _dio.get(path, queryParameters: queryParameters); + return response; + } catch (e) { + rethrow; + } + } + + Future post(String path, {dynamic data, Map? queryParameters}) async { + try { + final connectivityResult = await Connectivity().checkConnectivity(); + if (connectivityResult == ConnectivityResult.none) { + throw Exception('No internet connection'); + } + + final response = await _dio.post(path, data: data, queryParameters: queryParameters); + return response; + } catch (e) { + rethrow; + } + } + + Future put(String path, {dynamic data, Map? queryParameters}) async { + try { + final connectivityResult = await Connectivity().checkConnectivity(); + if (connectivityResult == ConnectivityResult.none) { + throw Exception('No internet connection'); + } + + final response = await _dio.put(path, data: data, queryParameters: queryParameters); + return response; + } catch (e) { + rethrow; + } + } + + Future delete(String path, {Map? queryParameters}) async { + try { + final connectivityResult = await Connectivity().checkConnectivity(); + if (connectivityResult == ConnectivityResult.none) { + throw Exception('No internet connection'); + } + + final response = await _dio.delete(path, queryParameters: queryParameters); + return response; + } catch (e) { + rethrow; + } + } +} \ No newline at end of file diff --git a/lib/src/services/app_info_service.dart b/lib/src/services/app_info_service.dart new file mode 100644 index 0000000..3a5affb --- /dev/null +++ b/lib/src/services/app_info_service.dart @@ -0,0 +1,80 @@ +import 'package:package_info_plus/package_info_plus.dart'; + +class AppInfoService { + static final AppInfoService _instance = AppInfoService._internal(); + factory AppInfoService() => _instance; + + AppInfoService._internal(); + + PackageInfo? _packageInfo; + + // 初始化服务 + Future init() async { + _packageInfo = await PackageInfo.fromPlatform(); + } + + // 获取应用名称 + String get appName => _packageInfo?.appName ?? 'Mom\'s Kitchen'; + + // 获取包名 + String get packageName => + _packageInfo?.packageName ?? 'com.example.mom_kitchen'; + + // 获取版本号 (如 1.0.0) + String get version => _packageInfo?.version ?? '1.0.0'; + + // 获取构建号 (如 1) + String get buildNumber => _packageInfo?.buildNumber ?? '1'; + + // 获取完整版本信息 (如 1.0.0+1) + String get fullVersion => '$version+$buildNumber'; + + // 获取构建签名 + String get buildSignature => _packageInfo?.buildSignature ?? ''; + + // 获取安装来源 + String get installerStore => _packageInfo?.installerStore ?? ''; + + // 获取应用信息摘要 + Map get appInfo => { + 'appName': appName, + 'packageName': packageName, + 'version': version, + 'buildNumber': buildNumber, + 'fullVersion': fullVersion, + 'buildSignature': buildSignature, + 'installerStore': installerStore, + }; + + // 获取应用信息字符串 + String get appInfoString { + return ''' +App Name: $appName +Package: $packageName +Version: $fullVersion +Build Signature: $buildSignature +Installer: $installerStore +''' + .trim(); + } + + // 比较版本号 + int compareVersion(String version1, String version2) { + final v1 = version1.split('.').map(int.parse).toList(); + final v2 = version2.split('.').map(int.parse).toList(); + + for (var i = 0; i < v1.length || i < v2.length; i++) { + final num1 = i < v1.length ? v1[i] : 0; + final num2 = i < v2.length ? v2[i] : 0; + + if (num1 > num2) return 1; + if (num1 < num2) return -1; + } + return 0; + } + + // 检查是否需要更新 + bool needsUpdate(String latestVersion) { + return compareVersion(latestVersion, version) > 0; + } +} diff --git a/lib/src/services/app_service.dart b/lib/src/services/app_service.dart new file mode 100644 index 0000000..c4d40b9 --- /dev/null +++ b/lib/src/services/app_service.dart @@ -0,0 +1,55 @@ +import 'package:mom_kitchen/src/services/api_service.dart'; +import 'package:mom_kitchen/src/services/storage_service.dart'; +import 'package:mom_kitchen/src/services/orientation_service.dart'; +import 'package:mom_kitchen/src/services/theme_service.dart'; +import 'package:mom_kitchen/src/services/notification_service.dart'; +import 'package:mom_kitchen/src/services/app_info_service.dart'; +import 'package:mom_kitchen/src/services/toast_service.dart'; +import 'package:mom_kitchen/src/services/permission_service.dart'; +import 'package:mom_kitchen/src/services/animation_service.dart'; +import 'package:mom_kitchen/src/services/logger_service.dart'; +import 'package:mom_kitchen/src/services/screen_util_config.dart'; + +class AppService { + static final AppService _instance = AppService._internal(); + factory AppService() => _instance; + + late ApiService api; + late StorageService storage; + late OrientationService orientation; + late ThemeService theme; + late NotificationService notification; + late AppInfoService appInfo; + late ToastService toast; + late PermissionService permission; + late AnimationService animation; + late LoggerService logger; + late ScreenUtilConfig screenUtil; + + AppService._internal() { + api = ApiService(); + storage = StorageService(); + orientation = OrientationService(); + theme = ThemeService(); + notification = NotificationService(); + appInfo = AppInfoService(); + toast = ToastService(); + permission = PermissionService(); + animation = AnimationService(); + logger = LoggerService(); + screenUtil = ScreenUtilConfig(); + } + + Future init() async { + await storage.init(); + await theme.init(); + await notification.init(); + await appInfo.init(); + await permission.init(); + await animation.init(); + await logger.init(); + await screenUtil.init(); + } + + static AppService get instance => _instance; +} diff --git a/lib/src/services/logger_service.dart b/lib/src/services/logger_service.dart new file mode 100644 index 0000000..44a4ad3 --- /dev/null +++ b/lib/src/services/logger_service.dart @@ -0,0 +1,297 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +enum LogLevel { debug, info, warning, error, off } + +class LoggerService { + static final LoggerService _instance = LoggerService._internal(); + factory LoggerService() => _instance; + LoggerService._internal(); + + late Logger _logger; + late SharedPreferences _prefs; + late File? _logFile; + + static const String _keyEnabled = 'logger_enabled'; + static const String _keyLevel = 'logger_level'; + static const String _keyWriteToFile = 'logger_write_to_file'; + static const String _keyMaxFileSize = 'logger_max_file_size'; + static const String _keyMaxFileCount = 'logger_max_file_count'; + + bool _enabled = true; + LogLevel _level = LogLevel.debug; + bool _writeToFile = false; + int _maxFileSize = 5 * 1024 * 1024; + int _maxFileCount = 5; + + bool get enabled => _enabled; + LogLevel get level => _level; + bool get writeToFile => _writeToFile; + int get maxFileSize => _maxFileSize; + int get maxFileCount => _maxFileCount; + + Future init() async { + _prefs = await SharedPreferences.getInstance(); + await _loadConfig(); + _initLogger(); + + if (_writeToFile && !kIsWeb) { + await _initLogFile(); + } + + return this; + } + + Future _loadConfig() async { + _enabled = _prefs.getBool(_keyEnabled) ?? true; + _writeToFile = _prefs.getBool(_keyWriteToFile) ?? false; + _maxFileSize = _prefs.getInt(_keyMaxFileSize) ?? 5 * 1024 * 1024; + _maxFileCount = _prefs.getInt(_keyMaxFileCount) ?? 5; + + final levelIndex = _prefs.getInt(_keyLevel) ?? 0; + _level = LogLevel.values[levelIndex.clamp(0, LogLevel.values.length - 1)]; + } + + Future _saveConfig() async { + await _prefs.setBool(_keyEnabled, _enabled); + await _prefs.setInt(_keyLevel, _level.index); + await _prefs.setBool(_keyWriteToFile, _writeToFile); + await _prefs.setInt(_keyMaxFileSize, _maxFileSize); + await _prefs.setInt(_keyMaxFileCount, _maxFileCount); + } + + void _initLogger() { + Level loggerLevel; + switch (_level) { + case LogLevel.debug: + loggerLevel = Level.debug; + break; + case LogLevel.info: + loggerLevel = Level.info; + break; + case LogLevel.warning: + loggerLevel = Level.warning; + break; + case LogLevel.error: + loggerLevel = Level.error; + break; + case LogLevel.off: + loggerLevel = Level.off; + break; + } + + _logger = Logger( + level: loggerLevel, + printer: PrettyPrinter( + methodCount: 2, + errorMethodCount: 8, + lineLength: 120, + colors: true, + printEmojis: true, + ), + output: _writeToFile && !kIsWeb && _logFile != null + ? MultiOutput([ConsoleOutput(), FileOutput(file: _logFile!)]) + : null, + ); + } + + Future _initLogFile() async { + try { + final directory = await getApplicationDocumentsDirectory(); + final logDir = Directory('${directory.path}/logs'); + + if (!await logDir.exists()) { + await logDir.create(recursive: true); + } + + final now = DateTime.now(); + final fileName = + 'log_${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}.txt'; + _logFile = File('${logDir.path}/$fileName'); + + await _cleanOldLogs(logDir); + + _initLogger(); + } catch (e) { + debugPrint('Failed to init log file: $e'); + _logFile = null; + } + } + + Future _cleanOldLogs(Directory logDir) async { + try { + final files = await logDir.list().toList(); + final logFiles = files + .whereType() + .where((f) => f.path.endsWith('.txt')) + .toList(); + + logFiles.sort((a, b) => b.path.compareTo(a.path)); + + for (var i = _maxFileCount; i < logFiles.length; i++) { + await logFiles[i].delete(); + } + + for (var file in logFiles) { + final size = await file.length(); + if (size > _maxFileSize) { + await file.delete(); + } + } + } catch (e) { + debugPrint('Failed to clean old logs: $e'); + } + } + + Future setEnabled(bool enabled) async { + _enabled = enabled; + await _saveConfig(); + } + + Future setLevel(LogLevel level) async { + _level = level; + await _saveConfig(); + _initLogger(); + } + + Future setWriteToFile(bool writeToFile) async { + _writeToFile = writeToFile; + await _saveConfig(); + + if (writeToFile && !kIsWeb) { + await _initLogFile(); + } else { + _logFile = null; + _initLogger(); + } + } + + Future setMaxFileSize(int maxSize) async { + _maxFileSize = maxSize; + await _saveConfig(); + } + + Future setMaxFileCount(int count) async { + _maxFileCount = count; + await _saveConfig(); + } + + void debug(dynamic message, [dynamic error, StackTrace? stackTrace]) { + if (!_enabled) return; + _logger.d(message, error: error, stackTrace: stackTrace); + } + + void info(dynamic message, [dynamic error, StackTrace? stackTrace]) { + if (!_enabled) return; + _logger.i(message, error: error, stackTrace: stackTrace); + } + + void warning(dynamic message, [dynamic error, StackTrace? stackTrace]) { + if (!_enabled) return; + _logger.w(message, error: error, stackTrace: stackTrace); + } + + void error(dynamic message, [dynamic error, StackTrace? stackTrace]) { + if (!_enabled) return; + _logger.e(message, error: error, stackTrace: stackTrace); + } + + void log( + Level level, + dynamic message, [ + dynamic error, + StackTrace? stackTrace, + ]) { + if (!_enabled) return; + _logger.log(level, message, error: error, stackTrace: stackTrace); + } + + Future getLogContent() async { + if (_logFile == null || !await _logFile!.exists()) { + return null; + } + return await _logFile!.readAsString(); + } + + Future> getLogFiles() async { + if (kIsWeb) return []; + + try { + final directory = await getApplicationDocumentsDirectory(); + final logDir = Directory('${directory.path}/logs'); + + if (!await logDir.exists()) { + return []; + } + + final files = await logDir.list().toList(); + return files + .whereType() + .where((f) => f.path.endsWith('.txt')) + .map((f) => f.path) + .toList(); + } catch (e) { + return []; + } + } + + Future clearLogs() async { + if (kIsWeb) return; + + try { + final directory = await getApplicationDocumentsDirectory(); + final logDir = Directory('${directory.path}/logs'); + + if (await logDir.exists()) { + await logDir.delete(recursive: true); + } + + if (_writeToFile) { + await _initLogFile(); + } + } catch (e) { + debugPrint('Failed to clear logs: $e'); + } + } + + void dispose() { + _logger.close(); + } +} + +class FileOutput extends LogOutput { + final File file; + final bool overrideExisting; + final Encoding encoding; + IOSink? _sink; + + FileOutput({ + required this.file, + this.overrideExisting = false, + this.encoding = utf8, + }); + + @override + Future init() async { + _sink = file.openWrite( + mode: overrideExisting ? FileMode.writeOnly : FileMode.writeOnlyAppend, + encoding: encoding, + ); + } + + @override + void output(OutputEvent event) { + _sink?.writeAll(event.lines.map((line) => '$line\n')); + _sink?.flush(); + } + + @override + Future destroy() async { + await _sink?.flush(); + await _sink?.close(); + } +} diff --git a/lib/src/services/notification_service.dart b/lib/src/services/notification_service.dart new file mode 100644 index 0000000..06504ed --- /dev/null +++ b/lib/src/services/notification_service.dart @@ -0,0 +1,238 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class NotificationService { + static final NotificationService _instance = NotificationService._internal(); + factory NotificationService() => _instance; + + NotificationService._internal(); + + final FlutterLocalNotificationsPlugin _notifications = + FlutterLocalNotificationsPlugin(); + + // 初始化通知服务 + Future init() async { + const androidSettings = AndroidInitializationSettings( + '@mipmap/ic_launcher', + ); + const iosSettings = DarwinInitializationSettings( + requestAlertPermission: true, + requestBadgePermission: true, + requestSoundPermission: true, + ); + + const initSettings = InitializationSettings( + android: androidSettings, + iOS: iosSettings, + ); + + await _notifications.initialize( + initSettings, + onDidReceiveNotificationResponse: _onNotificationTapped, + ); + } + + // 通知点击回调 + void _onNotificationTapped(NotificationResponse response) { + debugPrint('Notification tapped: ${response.payload}'); + } + + // 请求通知权限 + Future requestPermission() async { + if (defaultTargetPlatform == TargetPlatform.android) { + final androidPlugin = _notifications + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >(); + return await androidPlugin?.requestNotificationsPermission() ?? false; + } else if (defaultTargetPlatform == TargetPlatform.iOS) { + final iosPlugin = _notifications + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin + >(); + return await iosPlugin?.requestPermissions( + alert: true, + badge: true, + sound: true, + ) ?? + false; + } + return false; + } + + // 显示普通通知 + Future showNotification({ + required int id, + required String title, + required String body, + String? payload, + }) async { + const androidDetails = AndroidNotificationDetails( + 'default_channel', + 'Default Channel', + channelDescription: 'Default notification channel', + importance: Importance.high, + priority: Priority.high, + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + const details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _notifications.show(id, title, body, details, payload: payload); + } + + // 显示定时通知(简化版本) + Future scheduleNotification({ + required int id, + required String title, + required String body, + required Duration delay, + String? payload, + }) async { + const androidDetails = AndroidNotificationDetails( + 'scheduled_channel', + 'Scheduled Channel', + channelDescription: 'Scheduled notification channel', + importance: Importance.high, + priority: Priority.high, + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + const details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _notifications.show(id, title, body, details, payload: payload); + } + + // 取消指定通知 + Future cancelNotification(int id) async { + await _notifications.cancel(id); + } + + // 取消所有通知 + Future cancelAllNotifications() async { + await _notifications.cancelAll(); + } + + // 获取待处理通知列表 + Future> getPendingNotifications() async { + return await _notifications.pendingNotificationRequests(); + } + + // 显示进度通知 + Future showProgressNotification({ + required int id, + required String title, + required String body, + required int progress, + int maxProgress = 100, + }) async { + final androidDetails = AndroidNotificationDetails( + 'progress_channel', + 'Progress Channel', + channelDescription: 'Progress notification channel', + importance: Importance.high, + priority: Priority.high, + showProgress: true, + maxProgress: maxProgress, + progress: progress, + indeterminate: false, + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + final details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _notifications.show(id, title, body, details); + } + + // 显示大文本通知 + Future showBigTextNotification({ + required int id, + required String title, + required String body, + required String bigText, + String? payload, + }) async { + final androidDetails = AndroidNotificationDetails( + 'big_text_channel', + 'Big Text Channel', + channelDescription: 'Big text notification channel', + importance: Importance.high, + priority: Priority.high, + styleInformation: BigTextStyleInformation(bigText), + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + ); + + final details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _notifications.show(id, title, body, details, payload: payload); + } + + // 显示图片通知 + Future showImageNotification({ + required int id, + required String title, + required String body, + required String imageUrl, + String? payload, + }) async { + final androidDetails = AndroidNotificationDetails( + 'image_channel', + 'Image Channel', + channelDescription: 'Image notification channel', + importance: Importance.high, + priority: Priority.high, + styleInformation: BigPictureStyleInformation( + FilePathAndroidBitmap(imageUrl), + largeIcon: FilePathAndroidBitmap(imageUrl), + contentTitle: title, + summaryText: body, + ), + ); + + const iosDetails = DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + attachments: [], + ); + + final details = NotificationDetails( + android: androidDetails, + iOS: iosDetails, + ); + + await _notifications.show(id, title, body, details, payload: payload); + } +} diff --git a/lib/src/services/orientation_service.dart b/lib/src/services/orientation_service.dart new file mode 100644 index 0000000..ba42730 --- /dev/null +++ b/lib/src/services/orientation_service.dart @@ -0,0 +1,39 @@ +import 'package:flutter/services.dart'; + +class OrientationService { + static final OrientationService _instance = OrientationService._internal(); + factory OrientationService() => _instance; + + OrientationService._internal(); + + // 锁定为竖屏 + Future lockPortrait() async { + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + } + + // 锁定为横屏 + Future lockLandscape() async { + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + } + + // 解锁屏幕方向 + Future unlockOrientation() async { + await SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + DeviceOrientation.landscapeLeft, + DeviceOrientation.landscapeRight, + ]); + } + + // 设置特定方向 + Future setOrientation(DeviceOrientation orientation) async { + await SystemChrome.setPreferredOrientations([orientation]); + } +} diff --git a/lib/src/services/permission_service.dart b/lib/src/services/permission_service.dart new file mode 100644 index 0000000..71a9477 --- /dev/null +++ b/lib/src/services/permission_service.dart @@ -0,0 +1,507 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:permission_handler/permission_handler.dart'; + +enum PermissionType { + camera, // 相机 + microphone, // 麦克风 + photos, // 相册 + location, // 位置 + storage, // 存储 + contacts, // 通讯录 + calendar, // 日历 + phone, // 电话 + sms, // 短信 + notification, // 通知 + bluetooth, // 蓝牙 + clipboard, // 剪切板 + network, // 网络 + vibrate, // 震动 + screen, // 屏幕 +} + +enum PermissionStatusType { + granted, // 已授权 + denied, // 已拒绝 + restricted, // 受限制 + limited, // 有限制 + permanentlyDenied, // 永久拒绝 + provisional, // 临时授权 + unknown, // 未知 + notApplicable, // 不适用(该平台无需此权限) +} + +class PermissionStatusInfo { + final PermissionType type; + final PermissionStatusType status; + final DateTime lastChecked; + final bool isPermanentlyDenied; + final String? platformNote; + + PermissionStatusInfo({ + required this.type, + required this.status, + required this.lastChecked, + this.isPermanentlyDenied = false, + this.platformNote, + }); + + Map toJson() { + return { + 'type': type.name, + 'status': status.name, + 'lastChecked': lastChecked.toIso8601String(), + 'isPermanentlyDenied': isPermanentlyDenied, + 'platformNote': platformNote, + }; + } + + factory PermissionStatusInfo.fromJson(Map json) { + return PermissionStatusInfo( + type: PermissionType.values.firstWhere((e) => e.name == json['type']), + status: PermissionStatusType.values.firstWhere( + (e) => e.name == json['status'], + ), + lastChecked: DateTime.parse(json['lastChecked']), + isPermanentlyDenied: json['isPermanentlyDenied'] ?? false, + platformNote: json['platformNote'], + ); + } +} + +class PermissionService { + static final PermissionService _instance = PermissionService._internal(); + factory PermissionService() => _instance; + + PermissionService._internal(); + + SharedPreferences? _prefs; + final Map _permissionCache = {}; + + static const String _cacheKey = 'permission_status_cache'; + + // 初始化服务 + Future init() async { + _prefs = await SharedPreferences.getInstance(); + await _loadCachedPermissions(); + } + + // 加载缓存的权限状态 + Future _loadCachedPermissions() async { + final cacheString = _prefs?.getString(_cacheKey); + if (cacheString != null) { + try { + final cacheMap = json.decode(cacheString) as Map; + cacheMap.forEach((key, value) { + final type = PermissionType.values.firstWhere((e) => e.name == key); + _permissionCache[type] = PermissionStatusInfo.fromJson( + value as Map, + ); + }); + } catch (e) { + debugPrint('Failed to load permission cache: $e'); + } + } + } + + // 保存权限状态到缓存 + Future _saveCachedPermissions() async { + final cacheMap = {}; + _permissionCache.forEach((key, value) { + cacheMap[key.name] = value.toJson(); + }); + await _prefs?.setString(_cacheKey, json.encode(cacheMap)); + } + + // 获取权限对应的 Permission 对象 + Permission? _getPermission(PermissionType type) { + switch (type) { + case PermissionType.camera: + return Permission.camera; + case PermissionType.microphone: + return Permission.microphone; + case PermissionType.photos: + return Permission.photos; + case PermissionType.location: + return Permission.location; + case PermissionType.storage: + return Permission.storage; + case PermissionType.contacts: + return Permission.contacts; + case PermissionType.calendar: + return Permission.calendarFullAccess; + case PermissionType.phone: + return Permission.phone; + case PermissionType.sms: + return Permission.sms; + case PermissionType.notification: + return Permission.notification; + case PermissionType.bluetooth: + return Permission.bluetooth; + case PermissionType.clipboard: + case PermissionType.network: + case PermissionType.vibrate: + case PermissionType.screen: + return null; + } + } + + // 检查平台是否需要此权限 + bool _isPermissionRequiredOnPlatform(PermissionType type) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + switch (type) { + case PermissionType.clipboard: + return false; + case PermissionType.network: + return false; + case PermissionType.vibrate: + return false; + case PermissionType.screen: + return false; + default: + return true; + } + } else if (defaultTargetPlatform == TargetPlatform.android) { + switch (type) { + case PermissionType.clipboard: + return true; + case PermissionType.network: + return false; + case PermissionType.vibrate: + return false; + case PermissionType.screen: + return true; + default: + return true; + } + } + return false; + } + + // 获取平台说明 + String _getPlatformNote(PermissionType type) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + switch (type) { + case PermissionType.clipboard: + return 'iOS 无需申请剪切板权限'; + case PermissionType.network: + return 'iOS 无需申请网络权限'; + case PermissionType.vibrate: + return 'iOS 无需申请震动权限'; + case PermissionType.screen: + return 'iOS 无需申请屏幕权限'; + default: + return ''; + } + } else if (defaultTargetPlatform == TargetPlatform.android) { + switch (type) { + case PermissionType.clipboard: + return 'Android 10+ 需要申请剪切板权限'; + case PermissionType.network: + return 'Android 无需申请网络权限'; + case PermissionType.vibrate: + return 'Android 无需申请震动权限'; + case PermissionType.screen: + return 'Android 需要申请屏幕相关权限'; + default: + return ''; + } + } + return ''; + } + + // 转换权限状态 + PermissionStatusType _convertStatus(PermissionStatus status) { + return switch (status) { + PermissionStatus.granted => PermissionStatusType.granted, + PermissionStatus.denied => PermissionStatusType.denied, + PermissionStatus.restricted => PermissionStatusType.restricted, + PermissionStatus.limited => PermissionStatusType.limited, + PermissionStatus.permanentlyDenied => + PermissionStatusType.permanentlyDenied, + PermissionStatus.provisional => PermissionStatusType.provisional, + }; + } + + // 检查单个权限状态 + Future checkPermission(PermissionType type) async { + final isRequired = _isPermissionRequiredOnPlatform(type); + + if (!isRequired) { + final info = PermissionStatusInfo( + type: type, + status: PermissionStatusType.notApplicable, + lastChecked: DateTime.now(), + platformNote: _getPlatformNote(type), + ); + _permissionCache[type] = info; + await _saveCachedPermissions(); + return info; + } + + final permission = _getPermission(type); + if (permission == null) { + final info = PermissionStatusInfo( + type: type, + status: PermissionStatusType.notApplicable, + lastChecked: DateTime.now(), + platformNote: _getPlatformNote(type), + ); + _permissionCache[type] = info; + await _saveCachedPermissions(); + return info; + } + + final status = await permission.status; + final isPermanentlyDenied = await permission.isPermanentlyDenied; + + final info = PermissionStatusInfo( + type: type, + status: _convertStatus(status), + lastChecked: DateTime.now(), + isPermanentlyDenied: isPermanentlyDenied, + platformNote: _getPlatformNote(type), + ); + + _permissionCache[type] = info; + await _saveCachedPermissions(); + + return info; + } + + // 检查多个权限状态 + Future> checkPermissions( + List types, + ) async { + final results = {}; + + for (final type in types) { + results[type] = await checkPermission(type); + } + + return results; + } + + // 请求单个权限 + Future requestPermission(PermissionType type) async { + final isRequired = _isPermissionRequiredOnPlatform(type); + + if (!isRequired) { + final info = PermissionStatusInfo( + type: type, + status: PermissionStatusType.notApplicable, + lastChecked: DateTime.now(), + platformNote: _getPlatformNote(type), + ); + _permissionCache[type] = info; + await _saveCachedPermissions(); + return info; + } + + final permission = _getPermission(type); + if (permission == null) { + final info = PermissionStatusInfo( + type: type, + status: PermissionStatusType.notApplicable, + lastChecked: DateTime.now(), + platformNote: _getPlatformNote(type), + ); + _permissionCache[type] = info; + await _saveCachedPermissions(); + return info; + } + + final status = await permission.request(); + final isPermanentlyDenied = await permission.isPermanentlyDenied; + + final info = PermissionStatusInfo( + type: type, + status: _convertStatus(status), + lastChecked: DateTime.now(), + isPermanentlyDenied: isPermanentlyDenied, + platformNote: _getPlatformNote(type), + ); + + _permissionCache[type] = info; + await _saveCachedPermissions(); + + return info; + } + + // 请求多个权限 + Future> requestPermissions( + List types, + ) async { + final results = {}; + + for (final type in types) { + results[type] = await requestPermission(type); + } + + return results; + } + + // 获取缓存的权限状态 + PermissionStatusInfo? getCachedPermission(PermissionType type) { + return _permissionCache[type]; + } + + // 获取所有缓存的权限状态 + Map getAllCachedPermissions() { + return Map.from(_permissionCache); + } + + // 检查权限是否已授予 + Future isGranted(PermissionType type) async { + final info = await checkPermission(type); + return info.status == PermissionStatusType.granted || + info.status == PermissionStatusType.notApplicable; + } + + // 检查权限是否被拒绝 + Future isDenied(PermissionType type) async { + final info = await checkPermission(type); + return info.status == PermissionStatusType.denied; + } + + // 检查权限是否被永久拒绝 + Future isPermanentlyDenied(PermissionType type) async { + final info = await checkPermission(type); + return info.isPermanentlyDenied; + } + + // 打开应用设置 + Future openSettings() async { + return await openAppSettings(); + } + + // 获取权限状态描述 + String getStatusDescription(PermissionStatusType status) { + switch (status) { + case PermissionStatusType.granted: + return '已授权'; + case PermissionStatusType.denied: + return '已拒绝'; + case PermissionStatusType.restricted: + return '受限制'; + case PermissionStatusType.limited: + return '有限制'; + case PermissionStatusType.permanentlyDenied: + return '永久拒绝'; + case PermissionStatusType.provisional: + return '临时授权'; + case PermissionStatusType.notApplicable: + return '无需申请'; + default: + return '未知'; + } + } + + // 获取权限名称 + String getPermissionName(PermissionType type) { + switch (type) { + case PermissionType.camera: + return '相机'; + case PermissionType.microphone: + return '麦克风'; + case PermissionType.photos: + return '相册'; + case PermissionType.location: + return '位置'; + case PermissionType.storage: + return '存储'; + case PermissionType.contacts: + return '通讯录'; + case PermissionType.calendar: + return '日历'; + case PermissionType.phone: + return '电话'; + case PermissionType.sms: + return '短信'; + case PermissionType.notification: + return '通知'; + case PermissionType.bluetooth: + return '蓝牙'; + case PermissionType.clipboard: + return '剪切板'; + case PermissionType.network: + return '网络'; + case PermissionType.vibrate: + return '震动'; + case PermissionType.screen: + return '屏幕'; + } + } + + // 获取权限图标 + String getPermissionIcon(PermissionType type) { + switch (type) { + case PermissionType.camera: + return '📷'; + case PermissionType.microphone: + return '🎤'; + case PermissionType.photos: + return '🖼️'; + case PermissionType.location: + return '📍'; + case PermissionType.storage: + return '💾'; + case PermissionType.contacts: + return '👥'; + case PermissionType.calendar: + return '📅'; + case PermissionType.phone: + return '📞'; + case PermissionType.sms: + return '💬'; + case PermissionType.notification: + return '🔔'; + case PermissionType.bluetooth: + return '📶'; + case PermissionType.clipboard: + return '📋'; + case PermissionType.network: + return '🌐'; + case PermissionType.vibrate: + return '📳'; + case PermissionType.screen: + return '🖥️'; + } + } + + // 清除权限缓存 + Future clearCache() async { + _permissionCache.clear(); + await _prefs?.remove(_cacheKey); + } + + // 获取权限摘要 + Map getPermissionSummary() { + final granted = []; + final denied = []; + final permanentlyDenied = []; + final notApplicable = []; + + _permissionCache.forEach((type, info) { + final name = getPermissionName(type); + if (info.status == PermissionStatusType.granted) { + granted.add(name); + } else if (info.status == PermissionStatusType.notApplicable) { + notApplicable.add(name); + } else if (info.isPermanentlyDenied) { + permanentlyDenied.add(name); + } else { + denied.add(name); + } + }); + + return { + 'granted': granted, + 'denied': denied, + 'permanentlyDenied': permanentlyDenied, + 'notApplicable': notApplicable, + 'total': _permissionCache.length, + }; + } +} diff --git a/lib/src/services/screen_util_config.dart b/lib/src/services/screen_util_config.dart new file mode 100644 index 0000000..bbe9d47 --- /dev/null +++ b/lib/src/services/screen_util_config.dart @@ -0,0 +1,178 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ScreenUtilConfig { + static final ScreenUtilConfig _instance = ScreenUtilConfig._internal(); + factory ScreenUtilConfig() => _instance; + ScreenUtilConfig._internal(); + + static const Size _defaultDesignSize = Size(375, 812); + static const bool _defaultMinTextAdapt = true; + static const bool _defaultSplitScreenMode = true; + static const bool _defaultScaleEnabled = true; + + late SharedPreferences _prefs; + + static const String _keyDesignWidth = 'screenutil_design_width'; + static const String _keyDesignHeight = 'screenutil_design_height'; + static const String _keyMinTextAdapt = 'screenutil_min_text_adapt'; + static const String _keySplitScreenMode = 'screenutil_split_screen_mode'; + static const String _keyScaleEnabled = 'screenutil_scale_enabled'; + + Size _designSize = _defaultDesignSize; + bool _minTextAdapt = _defaultMinTextAdapt; + bool _splitScreenMode = _defaultSplitScreenMode; + bool _scaleEnabled = _defaultScaleEnabled; + + Size get designSize => _designSize; + bool get minTextAdapt => _minTextAdapt; + bool get splitScreenMode => _splitScreenMode; + bool get scaleEnabled => _scaleEnabled; + + double get designWidth => _designSize.width; + double get designHeight => _designSize.height; + + Future init() async { + _prefs = await SharedPreferences.getInstance(); + await _loadConfig(); + _applyConfig(); + return this; + } + + Future _loadConfig() async { + final width = _prefs.getDouble(_keyDesignWidth) ?? _defaultDesignSize.width; + final height = _prefs.getDouble(_keyDesignHeight) ?? _defaultDesignSize.height; + _designSize = Size(width, height); + + _minTextAdapt = _prefs.getBool(_keyMinTextAdapt) ?? _defaultMinTextAdapt; + _splitScreenMode = _prefs.getBool(_keySplitScreenMode) ?? _defaultSplitScreenMode; + _scaleEnabled = _prefs.getBool(_keyScaleEnabled) ?? _defaultScaleEnabled; + } + + Future _saveConfig() async { + await _prefs.setDouble(_keyDesignWidth, _designSize.width); + await _prefs.setDouble(_keyDesignHeight, _designSize.height); + await _prefs.setBool(_keyMinTextAdapt, _minTextAdapt); + await _prefs.setBool(_keySplitScreenMode, _splitScreenMode); + await _prefs.setBool(_keyScaleEnabled, _scaleEnabled); + } + + void _applyConfig() { + if (_scaleEnabled) { + ScreenUtil.enableScale( + enableWH: () => true, + enableText: () => true, + ); + } else { + ScreenUtil.enableScale( + enableWH: () => false, + enableText: () => false, + ); + } + } + + Future setDesignSize(Size size) async { + _designSize = size; + await _saveConfig(); + } + + Future setDesignWidth(double width) async { + _designSize = Size(width, _designSize.height); + await _saveConfig(); + } + + Future setDesignHeight(double height) async { + _designSize = Size(_designSize.width, height); + await _saveConfig(); + } + + Future setMinTextAdapt(bool adapt) async { + _minTextAdapt = adapt; + await _saveConfig(); + } + + Future setSplitScreenMode(bool mode) async { + _splitScreenMode = mode; + await _saveConfig(); + } + + Future setScaleEnabled(bool enabled) async { + _scaleEnabled = enabled; + await _saveConfig(); + _applyConfig(); + } + + Future resetToDefault() async { + _designSize = _defaultDesignSize; + _minTextAdapt = _defaultMinTextAdapt; + _splitScreenMode = _defaultSplitScreenMode; + _scaleEnabled = _defaultScaleEnabled; + await _saveConfig(); + _applyConfig(); + } + + void initScreenUtil(BuildContext context) { + ScreenUtil.init( + context, + designSize: _designSize, + minTextAdapt: _minTextAdapt, + splitScreenMode: _splitScreenMode, + ); + } + + Future ensureScreenSizeAndInit(BuildContext context) async { + await ScreenUtil.ensureScreenSizeAndInit( + context, + designSize: _designSize, + minTextAdapt: _minTextAdapt, + splitScreenMode: _splitScreenMode, + ); + } + + Map toMap() { + return { + 'designWidth': designWidth, + 'designHeight': designHeight, + 'minTextAdapt': _minTextAdapt, + 'splitScreenMode': _splitScreenMode, + 'scaleEnabled': _scaleEnabled, + }; + } + + @override + String toString() { + return 'ScreenUtilConfig(designSize: $_designSize, minTextAdapt: $_minTextAdapt, splitScreenMode: $_splitScreenMode, scaleEnabled: $_scaleEnabled)'; + } +} + +extension ScreenUtilExtension on num { + double get w { + final config = ScreenUtilConfig(); + if (!config.scaleEnabled) return toDouble(); + return ScreenUtil().setWidth(this); + } + + double get h { + final config = ScreenUtilConfig(); + if (!config.scaleEnabled) return toDouble(); + return ScreenUtil().setHeight(this); + } + + double get sp { + final config = ScreenUtilConfig(); + if (!config.scaleEnabled) return toDouble(); + return ScreenUtil().setSp(this); + } + + double get r { + final config = ScreenUtilConfig(); + if (!config.scaleEnabled) return toDouble(); + return ScreenUtil().radius(this); + } + + double get sw => ScreenUtil().screenWidth; + double get sh => ScreenUtil().screenHeight; + double get statusBarHeight => ScreenUtil().statusBarHeight; + double get bottomBarHeight => ScreenUtil().bottomBarHeight; +} diff --git a/lib/src/services/storage_service.dart b/lib/src/services/storage_service.dart new file mode 100644 index 0000000..2b6fbe3 --- /dev/null +++ b/lib/src/services/storage_service.dart @@ -0,0 +1,66 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +class StorageService { + static final StorageService _instance = StorageService._internal(); + factory StorageService() => _instance; + + late SharedPreferences _prefs; + + StorageService._internal(); + + Future init() async { + _prefs = await SharedPreferences.getInstance(); + } + + Future setString(String key, String value) async { + await _prefs.setString(key, value); + } + + String? getString(String key) { + return _prefs.getString(key); + } + + Future setInt(String key, int value) async { + await _prefs.setInt(key, value); + } + + int? getInt(String key) { + return _prefs.getInt(key); + } + + Future setBool(String key, bool value) async { + await _prefs.setBool(key, value); + } + + bool? getBool(String key) { + return _prefs.getBool(key); + } + + Future setDouble(String key, double value) async { + await _prefs.setDouble(key, value); + } + + double? getDouble(String key) { + return _prefs.getDouble(key); + } + + Future setStringList(String key, List value) async { + await _prefs.setStringList(key, value); + } + + List? getStringList(String key) { + return _prefs.getStringList(key); + } + + Future remove(String key) async { + await _prefs.remove(key); + } + + Future clear() async { + await _prefs.clear(); + } + + bool containsKey(String key) { + return _prefs.containsKey(key); + } +} \ No newline at end of file diff --git a/lib/src/services/theme_service.dart b/lib/src/services/theme_service.dart new file mode 100644 index 0000000..9134410 --- /dev/null +++ b/lib/src/services/theme_service.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class ThemeService extends GetxService { + static ThemeService get to => Get.find(); + + final _isDarkMode = false.obs; + final _primaryColor = Rx(Colors.blue); + final _secondaryColor = Rx(Colors.orange); + final _fontSize = 16.0.obs; + final _isStatusBarImmersive = false.obs; + final _textColor = Rx(Colors.black); + final _backgroundColor = Rx(Colors.white); + final _animationIntensity = 1.0.obs; + + late SharedPreferences _prefs; + + // 初始化 + Future init() async { + _prefs = await SharedPreferences.getInstance(); + await _loadTheme(); + return this; + } + + // 加载主题 + Future _loadTheme() async { + _isDarkMode.value = _prefs.getBool('is_dark_mode') ?? false; + _primaryColor.value = Color( + _prefs.getInt('primary_color') ?? Colors.blue.value, + ); + _secondaryColor.value = Color( + _prefs.getInt('secondary_color') ?? Colors.orange.value, + ); + _fontSize.value = _prefs.getDouble('font_size') ?? 16.0; + _isStatusBarImmersive.value = + _prefs.getBool('is_status_bar_immersive') ?? false; + _textColor.value = Color( + _prefs.getInt('text_color') ?? + (_isDarkMode.value ? Colors.white.value : Colors.black.value), + ); + _backgroundColor.value = Color( + _prefs.getInt('background_color') ?? + (_isDarkMode.value ? Colors.black.value : Colors.white.value), + ); + _animationIntensity.value = _prefs.getDouble('animation_intensity') ?? 1.0; + } + + // 保存主题 + Future _saveTheme() async { + await _prefs.setBool('is_dark_mode', _isDarkMode.value); + await _prefs.setInt('primary_color', _primaryColor.value.value); + await _prefs.setInt('secondary_color', _secondaryColor.value.value); + await _prefs.setDouble('font_size', _fontSize.value); + await _prefs.setBool( + 'is_status_bar_immersive', + _isStatusBarImmersive.value, + ); + await _prefs.setInt('text_color', _textColor.value.value); + await _prefs.setInt('background_color', _backgroundColor.value.value); + await _prefs.setDouble('animation_intensity', _animationIntensity.value); + } + + // 获取当前主题 + bool get isDarkMode => _isDarkMode.value; + Color get primaryColor => _primaryColor.value; + Color get secondaryColor => _secondaryColor.value; + double get fontSize => _fontSize.value; + bool get isStatusBarImmersive => _isStatusBarImmersive.value; + Color get textColor => _textColor.value; + Color get backgroundColor => _backgroundColor.value; + double get animationIntensity => _animationIntensity.value; + + // 获取当前主题对象 + Map get currentTheme { + return { + 'isDarkMode': isDarkMode, + 'primaryColor': primaryColor, + 'secondaryColor': secondaryColor, + 'fontSize': fontSize, + 'isStatusBarImmersive': isStatusBarImmersive, + 'textColor': textColor, + 'backgroundColor': backgroundColor, + 'animationIntensity': animationIntensity, + }; + } + + // 切换主题模式 + Future toggleThemeMode() async { + _isDarkMode.value = !_isDarkMode.value; + await _saveTheme(); + Get.changeThemeMode(_isDarkMode.value ? ThemeMode.dark : ThemeMode.light); + } + + // 设置主题色 + Future setPrimaryColor(Color color) async { + _primaryColor.value = color; + await _saveTheme(); + Get.changeTheme(getThemeData()); + } + + // 设置次要色 + Future setSecondaryColor(Color color) async { + _secondaryColor.value = color; + await _saveTheme(); + Get.changeTheme(getThemeData()); + } + + // 设置字体大小 + Future setFontSize(double size) async { + _fontSize.value = size; + await _saveTheme(); + Get.changeTheme(getThemeData()); + } + + // 设置状态栏沉浸 + Future setStatusBarImmersive(bool isImmersive) async { + _isStatusBarImmersive.value = isImmersive; + await _saveTheme(); + } + + // 设置动画强度 + Future setAnimationIntensity(double intensity) async { + _animationIntensity.value = intensity; + await _saveTheme(); + } + + // 重置为默认主题 + Future resetToDefault() async { + _isDarkMode.value = false; + _primaryColor.value = Colors.blue; + _secondaryColor.value = Colors.orange; + _fontSize.value = 16.0; + _isStatusBarImmersive.value = false; + _animationIntensity.value = 1.0; + await _saveTheme(); + Get.changeTheme(getThemeData()); + } + + // 获取主题数据 + ThemeData getThemeData() { + return ThemeData( + brightness: _isDarkMode.value ? Brightness.dark : Brightness.light, + primaryColor: _primaryColor.value, + secondaryHeaderColor: _secondaryColor.value, + textTheme: TextTheme( + bodyLarge: TextStyle(fontSize: _fontSize.value), + bodyMedium: TextStyle(fontSize: _fontSize.value - 2), + bodySmall: TextStyle(fontSize: _fontSize.value - 4), + titleLarge: TextStyle(fontSize: _fontSize.value + 4), + titleMedium: TextStyle(fontSize: _fontSize.value + 2), + titleSmall: TextStyle(fontSize: _fontSize.value), + ), + ); + } +} diff --git a/lib/src/services/toast_service.dart b/lib/src/services/toast_service.dart index 16dd8d7..7291901 100644 --- a/lib/src/services/toast_service.dart +++ b/lib/src/services/toast_service.dart @@ -6,17 +6,25 @@ import 'package:mom_kitchen/src/standards/page_standards.dart'; enum ToastType { success, error, warning, info } +enum ToastStyle { minimal, standard, detailed } + class ToastService { static final ToastService _instance = ToastService._internal(); factory ToastService() => _instance; ToastService._internal(); static final FToast _fToast = FToast(); + ToastStyle _currentStyle = ToastStyle.standard; + ToastStyle get currentStyle => _currentStyle; static void init(BuildContext context) { _fToast.init(context); } + void setStyle(ToastStyle style) { + _currentStyle = style; + } + static Future show({ required String message, ToastType type = ToastType.info, @@ -79,28 +87,23 @@ class ToastService { }) { Color backgroundColor; IconData icon; - String emoji; switch (type) { case ToastType.success: backgroundColor = const Color(0xFF4CAF50); icon = CupertinoIcons.checkmark_circle_fill; - emoji = '✅'; break; case ToastType.error: backgroundColor = const Color(0xFFF44336); icon = CupertinoIcons.xmark_circle_fill; - emoji = '❌'; break; case ToastType.warning: backgroundColor = const Color(0xFFFF9800); icon = CupertinoIcons.exclamationmark_triangle_fill; - emoji = '⚠️'; break; case ToastType.info: backgroundColor = standards.primaryColor; icon = CupertinoIcons.info_circle_fill; - emoji = 'ℹ️'; break; } diff --git a/lib/src/standards/app_pages.dart b/lib/src/standards/app_pages.dart new file mode 100644 index 0000000..27e6b0f --- /dev/null +++ b/lib/src/standards/app_pages.dart @@ -0,0 +1,88 @@ +import 'package:flutter/foundation.dart'; +import 'package:mom_kitchen/src/standards/page_validator.dart'; +import 'package:mom_kitchen/src/pages/home_page.dart'; +import 'package:mom_kitchen/src/pages/theme_demo_page.dart'; +import 'package:mom_kitchen/src/pages/example_page.dart'; + +class AppPages { + static final List pages = [ + PageInfo( + route: '/', + name: '首页', + description: '应用主页面', + requiredStandards: [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.fontSize, + StandardCheck.localization, + StandardCheck.darkMode, + ], + builder: () => const HomePage(), + ), + PageInfo( + route: '/theme', + name: '主题设置', + description: '主题设置页面', + requiredStandards: [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.darkMode, + StandardCheck.animation, + ], + builder: () => const ThemeDemoPage(), + ), + PageInfo( + route: '/example', + name: '示例页面', + description: '示例页面展示规范使用', + requiredStandards: [ + StandardCheck.themeColors, + StandardCheck.textColors, + StandardCheck.fontSize, + StandardCheck.spacing, + StandardCheck.animation, + StandardCheck.responsive, + StandardCheck.localization, + StandardCheck.orientation, + StandardCheck.darkMode, + StandardCheck.deviceType, + ], + builder: () => const ExamplePage(), + ), + ]; + + static void registerAll() { + PageRegistry.registerAll(pages); + + if (kDebugMode) { + print('✅ 已注册 ${pages.length} 个页面'); + for (final page in pages) { + print(' - ${page.name} (${page.route})'); + } + } + } + + static bool isRegistered(String route) { + return PageRegistry.hasPage(route); + } + + static void validateRoute(String route) { + if (!isRegistered(route)) { + throw FlutterError(''' +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +❌ 页面未注册错误 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +路由: $route + +该页面未在 AppPages 中注册,请检查: +1. 是否已在 AppPages.pages 中添加页面信息 +2. 路由名称是否正确 +3. 是否调用了 AppPages.registerAll() + +已注册的页面: +${PageRegistry.allPages.map((p) => ' - ${p.route}: ${p.name}').join('\n')} +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +'''); + } + } +} diff --git a/lib/src/standards/page_standards.dart b/lib/src/standards/page_standards.dart new file mode 100644 index 0000000..ae98494 --- /dev/null +++ b/lib/src/standards/page_standards.dart @@ -0,0 +1,357 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:mom_kitchen/src/services/app_service.dart'; +import 'package:mom_kitchen/src/services/theme_service.dart'; +import 'package:mom_kitchen/src/services/animation_service.dart'; +import 'package:mom_kitchen/src/services/screen_util_config.dart'; +import 'package:mom_kitchen/src/services/toast_service.dart'; +import 'package:mom_kitchen/src/l10n/app_localizations.dart'; + +class PageStandards { + final BuildContext context; + + PageStandards._(this.context); + + factory PageStandards.of(BuildContext context) { + return PageStandards._(context); + } + + AppLocalizations get l10n => AppLocalizations.of(context)!; + + ThemeService get _theme => AppService.instance.theme; + AnimationService get _animation => AppService.instance.animation; + ScreenUtilConfig get _screenUtil => AppService.instance.screenUtil; + + bool get isDarkMode => _theme.isDarkMode; + + Color get primaryColor => _theme.primaryColor; + + Color get secondaryColor => _theme.secondaryColor; + + Color get textColor => _theme.textColor; + + Color get backgroundColor => _theme.backgroundColor; + + double get fontSize => _theme.fontSize; + + bool get isStatusBarImmersive => _theme.isStatusBarImmersive; + + double get animationIntensity => _theme.animationIntensity; + + bool get animationEnabled => _animation.enabled; + + double get animationSpeed => _animation.speed; + + AnimationPreset get animationPreset => _animation.preset; + + AnimationCurveType get animationCurve => _animation.curveType; + + Size get designSize => _screenUtil.designSize; + + bool get scaleEnabled => _screenUtil.scaleEnabled; + + bool get minTextAdapt => _screenUtil.minTextAdapt; + + bool get splitScreenMode => _screenUtil.splitScreenMode; + + double get screenWidth => ScreenUtil().screenWidth; + + double get screenHeight => ScreenUtil().screenHeight; + + double get statusBarHeight => ScreenUtil().statusBarHeight; + + double get bottomBarHeight => ScreenUtil().bottomBarHeight; + + Orientation get orientation => MediaQuery.of(context).orientation; + + bool get isPortrait => orientation == Orientation.portrait; + + bool get isLandscape => orientation == Orientation.landscape; + + double get devicePixelRatio => MediaQuery.of(context).devicePixelRatio; + + EdgeInsets get padding => MediaQuery.of(context).padding; + + EdgeInsets get viewInsets => MediaQuery.of(context).viewInsets; + + Locale get currentLocale => Localizations.localeOf(context); + + String get languageCode => currentLocale.languageCode; + + ToastStyle get toastStyle => AppService.instance.toast.currentStyle; + + DeviceType get deviceType => ScreenUtil().deviceType(context); + + bool get isMobile => + deviceType == DeviceType.mobile || deviceType == DeviceType.harmonyOS; + + bool get isTablet => deviceType == DeviceType.tablet; + + bool get isWeb => deviceType == DeviceType.web; + + bool get isDesktop => + deviceType == DeviceType.windows || + deviceType == DeviceType.mac || + deviceType == DeviceType.linux; + + bool get isHarmonyOS => deviceType == DeviceType.harmonyOS; + + double scaledWidth(double width) { + if (!scaleEnabled) return width; + return ScreenUtil().setWidth(width); + } + + double scaledHeight(double height) { + if (!scaleEnabled) return height; + return ScreenUtil().setHeight(height); + } + + double scaledFontSize(double fontSize) { + if (!scaleEnabled) return fontSize; + return ScreenUtil().setSp(fontSize); + } + + double scaledRadius(double radius) { + if (!scaleEnabled) return radius; + return ScreenUtil().radius(radius); + } + + EdgeInsets scaledPadding(EdgeInsets padding) { + if (!scaleEnabled) return padding; + return EdgeInsets.only( + left: ScreenUtil().setWidth(padding.left), + top: ScreenUtil().setHeight(padding.top), + right: ScreenUtil().setWidth(padding.right), + bottom: ScreenUtil().setHeight(padding.bottom), + ); + } + + EdgeInsets scaledMargin(EdgeInsets margin) { + return scaledPadding(margin); + } + + TextStyle get textStyle => + TextStyle(color: textColor, fontSize: ScreenUtil().setSp(fontSize)); + + TextStyle get primaryTextStyle => + TextStyle(color: primaryColor, fontSize: ScreenUtil().setSp(fontSize)); + + TextStyle get secondaryTextStyle => + TextStyle(color: secondaryColor, fontSize: ScreenUtil().setSp(fontSize)); + + ThemeData get themeData => Theme.of(context); + + CupertinoThemeData get cupertinoThemeData => CupertinoTheme.of(context); + + Brightness get brightness => isDarkMode ? Brightness.dark : Brightness.light; + + SystemUiOverlayStyle get systemUiOverlayStyle => + isDarkMode ? SystemUiOverlayStyle.light : SystemUiOverlayStyle.dark; + + void setStatusBarStyle() { + SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle); + } + + void hideStatusBar() { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + } + + void showStatusBar() { + SystemChrome.setEnabledSystemUIMode( + SystemUiMode.manual, + overlays: SystemUiOverlay.values, + ); + } + + Map toMap() { + return { + 'isDarkMode': isDarkMode, + 'primaryColor': primaryColor.toARGB32(), + 'secondaryColor': secondaryColor.toARGB32(), + 'textColor': textColor.toARGB32(), + 'backgroundColor': backgroundColor.toARGB32(), + 'fontSize': fontSize, + 'isStatusBarImmersive': isStatusBarImmersive, + 'animationIntensity': animationIntensity, + 'animationEnabled': animationEnabled, + 'animationSpeed': animationSpeed, + 'animationPreset': animationPreset.name, + 'animationCurve': animationCurve.name, + 'designSize': designSize.toString(), + 'scaleEnabled': scaleEnabled, + 'screenWidth': screenWidth, + 'screenHeight': screenHeight, + 'orientation': orientation.toString(), + 'currentLocale': currentLocale.toString(), + 'toastStyle': toastStyle.name, + 'deviceType': deviceType.name, + }; + } + + @override + String toString() { + return 'PageStandards(${toMap()})'; + } +} + +mixin PageStandardsMixin on State { + late PageStandards standards; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + standards = PageStandards.of(context); + } + + AppLocalizations get l10n => standards.l10n; + + bool get isDarkMode => standards.isDarkMode; + + Color get primaryColor => standards.primaryColor; + + Color get secondaryColor => standards.secondaryColor; + + Color get textColor => standards.textColor; + + Color get backgroundColor => standards.backgroundColor; + + double get fontSize => standards.fontSize; + + bool get isStatusBarImmersive => standards.isStatusBarImmersive; + + double get animationIntensity => standards.animationIntensity; + + bool get animationEnabled => standards.animationEnabled; + + double get animationSpeed => standards.animationSpeed; + + AnimationPreset get animationPreset => standards.animationPreset; + + AnimationCurveType get animationCurve => standards.animationCurve; + + Size get designSize => standards.designSize; + + bool get scaleEnabled => standards.scaleEnabled; + + double get screenWidth => standards.screenWidth; + + double get screenHeight => standards.screenHeight; + + double get statusBarHeight => standards.statusBarHeight; + + double get bottomBarHeight => standards.bottomBarHeight; + + Orientation get orientation => standards.orientation; + + bool get isPortrait => standards.isPortrait; + + bool get isLandscape => standards.isLandscape; + + Locale get currentLocale => standards.currentLocale; + + String get languageCode => standards.languageCode; + + ToastStyle get toastStyle => standards.toastStyle; + + DeviceType get deviceType => standards.deviceType; + + bool get isMobile => standards.isMobile; + + bool get isTablet => standards.isTablet; + + bool get isWeb => standards.isWeb; + + bool get isDesktop => standards.isDesktop; + + bool get isHarmonyOS => standards.isHarmonyOS; + + double scaledWidth(double width) => standards.scaledWidth(width); + + double scaledHeight(double height) => standards.scaledHeight(height); + + double scaledFontSize(double fontSize) => standards.scaledFontSize(fontSize); + + double scaledRadius(double radius) => standards.scaledRadius(radius); + + EdgeInsets scaledPadding(EdgeInsets padding) => + standards.scaledPadding(padding); + + EdgeInsets scaledMargin(EdgeInsets margin) => standards.scaledMargin(margin); + + TextStyle get textStyle => standards.textStyle; + + TextStyle get primaryTextStyle => standards.primaryTextStyle; + + TextStyle get secondaryTextStyle => standards.secondaryTextStyle; + + Brightness get brightness => standards.brightness; + + SystemUiOverlayStyle get systemUiOverlayStyle => + standards.systemUiOverlayStyle; + + void setStatusBarStyle() => standards.setStatusBarStyle(); + + void hideStatusBar() => standards.hideStatusBar(); + + void showStatusBar() => standards.showStatusBar(); +} + +abstract class StandardPage extends StatefulWidget { + const StandardPage({super.key}); +} + +abstract class StandardPageState extends State + with PageStandardsMixin { + @override + Widget build(BuildContext context) { + return buildPage(context); + } + + Widget buildPage(BuildContext context); +} + +class StandardPageWrapper extends StatelessWidget { + final Widget child; + final bool applyStatusBarStyle; + final bool applyBackgroundColor; + + const StandardPageWrapper({ + super.key, + required this.child, + this.applyStatusBarStyle = true, + this.applyBackgroundColor = true, + }); + + @override + Widget build(BuildContext context) { + final standards = PageStandards.of(context); + + if (applyStatusBarStyle) { + standards.setStatusBarStyle(); + } + + Widget result = child; + + if (applyBackgroundColor) { + result = Container(color: standards.backgroundColor, child: result); + } + + return result; + } +} + +extension PageStandardsExtension on BuildContext { + PageStandards get standards => PageStandards.of(this); + + AppLocalizations get l10n => AppLocalizations.of(this)!; + + Color get primaryColor => standards.primaryColor; + + Color get textColor => standards.textColor; + + Color get backgroundColor => standards.backgroundColor; + + double get fontSize => standards.fontSize; +} diff --git a/lib/src/standards/page_validator.dart b/lib/src/standards/page_validator.dart new file mode 100644 index 0000000..6827c65 --- /dev/null +++ b/lib/src/standards/page_validator.dart @@ -0,0 +1,479 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:mom_kitchen/src/standards/page_standards.dart'; +import 'package:mom_kitchen/src/utils/app_logger.dart'; + +enum StandardCheck { + themeColors('主题颜色', '检查是否使用主题色、次要色'), + textColors('字体颜色', '检查是否使用主题字体色'), + fontSize('字体大小', '检查是否使用主题字体大小'), + spacing('间距规范', '检查是否使用标准间距'), + animation('动画配置', '检查是否遵循动画配置'), + responsive('响应式布局', '检查是否适配不同屏幕'), + localization('多语言', '检查是否使用多语言'), + orientation('屏幕方向', '检查是否处理屏幕方向'), + toastStyle('消息样式', '检查是否使用标准消息样式'), + statusBarImmersive('状态栏沉浸', '检查是否处理状态栏沉浸'), + darkMode('深色模式', '检查是否支持深色模式'), + deviceType('设备类型', '检查是否适配不同设备类型'); + + final String label; + final String description; + + const StandardCheck(this.label, this.description); +} + +class PageInfo { + final String route; + final String name; + final String description; + final List requiredStandards; + final Widget Function() builder; + final Map? metadata; + + const PageInfo({ + required this.route, + required this.name, + required this.description, + required this.requiredStandards, + required this.builder, + this.metadata, + }); + + Map toMap() { + return { + 'route': route, + 'name': name, + 'description': description, + 'requiredStandards': requiredStandards.map((e) => e.name).toList(), + 'metadata': metadata, + }; + } +} + +class PageRegistry { + static final PageRegistry _instance = PageRegistry._internal(); + factory PageRegistry() => _instance; + PageRegistry._internal(); + + static final Map _pages = {}; + static final List _routeOrder = []; + + static void register(PageInfo info) { + if (_pages.containsKey(info.route)) { + AppLogger.w('页面路由已存在,将被覆盖: ${info.route}'); + } + _pages[info.route] = info; + _routeOrder.add(info.route); + } + + static void registerAll(List pages) { + for (final page in pages) { + register(page); + } + } + + static void unregister(String route) { + _pages.remove(route); + _routeOrder.remove(route); + } + + static PageInfo? getPage(String route) => _pages[route]; + + static List get allPages => + _routeOrder.map((route) => _pages[route]!).toList(); + + static List get allRoutes => _routeOrder.toList(); + + static bool hasPage(String route) => _pages.containsKey(route); + + static void clear() { + _pages.clear(); + _routeOrder.clear(); + } + + static int get pageCount => _pages.length; + + static List search(String keyword) { + return _pages.values.where((page) { + return page.name.contains(keyword) || + page.description.contains(keyword) || + page.route.contains(keyword); + }).toList(); + } + + static List filterByStandard(StandardCheck check) { + return _pages.values + .where((page) => page.requiredStandards.contains(check)) + .toList(); + } + + static Map exportConfig() { + return { + 'totalPages': pageCount, + 'pages': allPages.map((e) => e.toMap()).toList(), + }; + } +} + +class ValidationResult { + final String pageRoute; + final StandardCheck check; + final bool passed; + final String? message; + final DateTime timestamp; + + ValidationResult({ + required this.pageRoute, + required this.check, + required this.passed, + this.message, + DateTime? timestamp, + }) : timestamp = timestamp ?? DateTime.now(); + + Map toMap() { + return { + 'pageRoute': pageRoute, + 'check': check.name, + 'passed': passed, + 'message': message, + 'timestamp': timestamp.toIso8601String(), + }; + } +} + +class PageValidator { + static final PageValidator _instance = PageValidator._internal(); + factory PageValidator() => _instance; + PageValidator._internal(); + + static final List _validationHistory = []; + static const int _maxHistorySize = 100; + + static List get history => + List.unmodifiable(_validationHistory); + + static void validate(BuildContext context, String pageRoute) { + if (!kDebugMode) return; + + final pageInfo = PageRegistry.getPage(pageRoute); + if (pageInfo == null) { + AppLogger.w('⚠️ 页面未注册: $pageRoute'); + return; + } + + final standards = PageStandards.of(context); + + AppLogger.d('🔍 开始验证页面: ${pageInfo.name} ($pageRoute)'); + + for (final check in pageInfo.requiredStandards) { + _checkStandard(context, standards, pageRoute, check); + } + + AppLogger.i('✅ 页面验证完成: ${pageInfo.name}'); + } + + static void _checkStandard( + BuildContext context, + PageStandards standards, + String pageRoute, + StandardCheck check, + ) { + bool passed = false; + String? message; + + switch (check) { + case StandardCheck.themeColors: + passed = _checkThemeColors(context, standards); + message = passed ? null : '未检测到主题色使用'; + break; + + case StandardCheck.textColors: + passed = _checkTextColors(context, standards); + message = passed ? null : '未检测到主题字体色使用'; + break; + + case StandardCheck.fontSize: + passed = _checkFontSize(context, standards); + message = passed ? null : '未检测到主题字体大小使用'; + break; + + case StandardCheck.spacing: + passed = _checkSpacing(context, standards); + message = passed ? null : '未检测到标准间距使用'; + break; + + case StandardCheck.animation: + passed = _checkAnimation(context, standards); + message = passed ? null : '未遵循动画配置'; + break; + + case StandardCheck.responsive: + passed = _checkResponsive(context, standards); + message = passed ? null : '未检测到响应式布局'; + break; + + case StandardCheck.localization: + passed = _checkLocalization(context, standards); + message = passed ? null : '未检测到多语言使用'; + break; + + case StandardCheck.orientation: + passed = _checkOrientation(context, standards); + message = passed ? null : '未处理屏幕方向变化'; + break; + + case StandardCheck.toastStyle: + passed = _checkToastStyle(context, standards); + message = passed ? null : '未使用标准消息样式'; + break; + + case StandardCheck.statusBarImmersive: + passed = _checkStatusBarImmersive(context, standards); + message = passed ? null : '未处理状态栏沉浸'; + break; + + case StandardCheck.darkMode: + passed = _checkDarkMode(context, standards); + message = passed ? null : '未支持深色模式'; + break; + + case StandardCheck.deviceType: + passed = _checkDeviceType(context, standards); + message = passed ? null : '未适配不同设备类型'; + break; + } + + _addResult( + ValidationResult( + pageRoute: pageRoute, + check: check, + passed: passed, + message: message, + ), + ); + + if (!passed) { + AppLogger.w(' ⚠️ ${check.label}: $message'); + } else { + AppLogger.d(' ✓ ${check.label}'); + } + } + + static bool _checkThemeColors(BuildContext context, PageStandards standards) { + return true; + } + + static bool _checkTextColors(BuildContext context, PageStandards standards) { + return true; + } + + static bool _checkFontSize(BuildContext context, PageStandards standards) { + return true; + } + + static bool _checkSpacing(BuildContext context, PageStandards standards) { + return true; + } + + static bool _checkAnimation(BuildContext context, PageStandards standards) { + return standards.animationEnabled || !standards.animationEnabled; + } + + static bool _checkResponsive(BuildContext context, PageStandards standards) { + return standards.scaleEnabled; + } + + static bool _checkLocalization( + BuildContext context, + PageStandards standards, + ) { + try { + return standards.l10n.appTitle.isNotEmpty; + } catch (e) { + return false; + } + } + + static bool _checkOrientation(BuildContext context, PageStandards standards) { + return true; + } + + static bool _checkToastStyle(BuildContext context, PageStandards standards) { + return true; + } + + static bool _checkStatusBarImmersive( + BuildContext context, + PageStandards standards, + ) { + return true; + } + + static bool _checkDarkMode(BuildContext context, PageStandards standards) { + return true; + } + + static bool _checkDeviceType(BuildContext context, PageStandards standards) { + return true; + } + + static void _addResult(ValidationResult result) { + _validationHistory.add(result); + if (_validationHistory.length > _maxHistorySize) { + _validationHistory.removeAt(0); + } + } + + static void clearHistory() { + _validationHistory.clear(); + } + + static List getFailedResults() { + return _validationHistory.where((r) => !r.passed).toList(); + } + + static List getResultsByPage(String pageRoute) { + return _validationHistory.where((r) => r.pageRoute == pageRoute).toList(); + } + + static Map generateReport() { + final total = _validationHistory.length; + final passed = _validationHistory.where((r) => r.passed).length; + final failed = total - passed; + + return { + 'totalChecks': total, + 'passed': passed, + 'failed': failed, + 'passRate': total > 0 ? (passed / total * 100).toStringAsFixed(2) : '0', + 'failedChecks': getFailedResults().map((r) => r.toMap()).toList(), + }; + } + + static void printReport() { + if (!kDebugMode) return; + + final report = generateReport(); + AppLogger.i(''' +📊 页面规范验证报告 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +总检查数: ${report['totalChecks']} +通过: ${report['passed']} +失败: ${report['failed']} +通过率: ${report['passRate']}% +━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +'''); + } +} + +class ValidatorWidget extends StatelessWidget { + final String pageRoute; + final Widget child; + final bool enableValidation; + + const ValidatorWidget({ + super.key, + required this.pageRoute, + required this.child, + this.enableValidation = true, + }); + + @override + Widget build(BuildContext context) { + if (kDebugMode && enableValidation) { + WidgetsBinding.instance.addPostFrameCallback((_) { + PageValidator.validate(context, pageRoute); + }); + } + return child; + } +} + +class PageValidationDebugPanel extends StatelessWidget { + const PageValidationDebugPanel({super.key}); + + @override + Widget build(BuildContext context) { + if (!kDebugMode) { + return const SizedBox.shrink(); + } + + final report = PageValidator.generateReport(); + final failedChecks = PageValidator.getFailedResults(); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.black87, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '📊 页面规范验证报告', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + Text( + '总检查数: ${report['totalChecks']}', + style: const TextStyle(color: Colors.white70), + ), + Text( + '通过: ${report['passed']}', + style: const TextStyle(color: Colors.green), + ), + Text( + '失败: ${report['failed']}', + style: const TextStyle(color: Colors.red), + ), + Text( + '通过率: ${report['passRate']}%', + style: const TextStyle(color: Colors.white70), + ), + if (failedChecks.isNotEmpty) ...[ + const SizedBox(height: 12), + const Text( + '❌ 失败项:', + style: TextStyle(color: Colors.red, fontWeight: FontWeight.bold), + ), + ...failedChecks + .take(5) + .map( + (r) => Padding( + padding: const EdgeInsets.only(top: 4), + child: Text( + '• ${r.check.label}: ${r.message ?? ""}', + style: const TextStyle( + color: Colors.redAccent, + fontSize: 12, + ), + ), + ), + ), + ], + ], + ), + ); + } +} + +mixin ValidationMixin on State { + String get pageRoute; + + @override + void initState() { + super.initState(); + if (kDebugMode) { + WidgetsBinding.instance.addPostFrameCallback((_) { + PageValidator.validate(context, pageRoute); + }); + } + } +} diff --git a/lib/src/utils/app_logger.dart b/lib/src/utils/app_logger.dart new file mode 100644 index 0000000..0f711ac --- /dev/null +++ b/lib/src/utils/app_logger.dart @@ -0,0 +1,57 @@ +import 'package:mom_kitchen/src/services/app_service.dart'; + +class AppLogger { + static bool get enabled => AppService.instance.logger.enabled; + + static void d(dynamic message, [dynamic error, StackTrace? stackTrace]) { + AppService.instance.logger.debug(message, error, stackTrace); + } + + static void i(dynamic message, [dynamic error, StackTrace? stackTrace]) { + AppService.instance.logger.info(message, error, stackTrace); + } + + static void w(dynamic message, [dynamic error, StackTrace? stackTrace]) { + AppService.instance.logger.warning(message, error, stackTrace); + } + + static void e(dynamic message, [dynamic error, StackTrace? stackTrace]) { + AppService.instance.logger.error(message, error, stackTrace); + } + + static void debug(dynamic message, [dynamic error, StackTrace? stackTrace]) { + AppService.instance.logger.debug(message, error, stackTrace); + } + + static void info(dynamic message, [dynamic error, StackTrace? stackTrace]) { + AppService.instance.logger.info(message, error, stackTrace); + } + + static void warning(dynamic message, [dynamic error, StackTrace? stackTrace]) { + AppService.instance.logger.warning(message, error, stackTrace); + } + + static void error(dynamic message, [dynamic error, StackTrace? stackTrace]) { + AppService.instance.logger.error(message, error, stackTrace); + } + + static Future setEnabled(bool enabled) async { + await AppService.instance.logger.setEnabled(enabled); + } + + static Future setWriteToFile(bool writeToFile) async { + await AppService.instance.logger.setWriteToFile(writeToFile); + } + + static Future getLogContent() async { + return await AppService.instance.logger.getLogContent(); + } + + static Future> getLogFiles() async { + return await AppService.instance.logger.getLogFiles(); + } + + static Future clearLogs() async { + await AppService.instance.logger.clearLogs(); + } +} diff --git a/lib/src/utils/common_utils.dart b/lib/src/utils/common_utils.dart new file mode 100644 index 0000000..39713b6 --- /dev/null +++ b/lib/src/utils/common_utils.dart @@ -0,0 +1,52 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:flutter/services.dart'; + +class CommonUtils { + // 显示 Snackbar + static void showSnackbar( + BuildContext context, + String message, { + Color? backgroundColor, + }) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: backgroundColor, + duration: const Duration(seconds: 3), + ), + ); + } + + // 分享内容 + static Future shareContent(String text, {String? subject}) async { + await Share.share(text, subject: subject); + } + + // 复制文本到剪贴板 + static Future copyToClipboard(BuildContext context, String text) async { + await Clipboard.setData(ClipboardData(text: text)); + showSnackbar(context, 'Copied to clipboard'); + } + + // 获取屏幕宽度 + static double getScreenWidth(BuildContext context) { + return MediaQuery.of(context).size.width; + } + + // 获取屏幕高度 + static double getScreenHeight(BuildContext context) { + return MediaQuery.of(context).size.height; + } + + // 检查是否为深色模式 + static bool isDarkMode(BuildContext context) { + return MediaQuery.of(context).platformBrightness == Brightness.dark; + } + + // 防抖函数 + static void debounce(Function() function, {int milliseconds = 300}) { + final timer = Timer(Duration(milliseconds: milliseconds), function); + } +} diff --git a/lib/src/utils/date_utils.dart b/lib/src/utils/date_utils.dart new file mode 100644 index 0000000..d629f35 --- /dev/null +++ b/lib/src/utils/date_utils.dart @@ -0,0 +1,45 @@ +import 'package:intl/intl.dart'; + +class DateUtils { + // 格式化日期 + static String formatDate(DateTime date, {String format = 'yyyy-MM-dd'}) { + return DateFormat(format).format(date); + } + + // 格式化时间 + static String formatTime(DateTime date, {String format = 'HH:mm'}) { + return DateFormat(format).format(date); + } + + // 格式化日期时间 + static String formatDateTime(DateTime date, {String format = 'yyyy-MM-dd HH:mm'}) { + return DateFormat(format).format(date); + } + + // 解析日期字符串 + static DateTime? parseDate(String dateString, {String format = 'yyyy-MM-dd'}) { + try { + return DateFormat(format).parse(dateString); + } catch (e) { + return null; + } + } + + // 获取相对时间 + static String getRelativeTime(DateTime date) { + final now = DateTime.now(); + final difference = now.difference(date); + + if (difference.inSeconds < 60) { + return 'Just now'; + } else if (difference.inMinutes < 60) { + return '${difference.inMinutes} minutes ago'; + } else if (difference.inHours < 24) { + return '${difference.inHours} hours ago'; + } else if (difference.inDays < 7) { + return '${difference.inDays} days ago'; + } else { + return formatDate(date); + } + } +} \ No newline at end of file diff --git a/lib/src/utils/network_utils.dart b/lib/src/utils/network_utils.dart new file mode 100644 index 0000000..aba7a46 --- /dev/null +++ b/lib/src/utils/network_utils.dart @@ -0,0 +1,34 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; + +class NetworkUtils { + // 检查网络连接状态 + static Future isConnected() async { + final connectivityResult = await Connectivity().checkConnectivity(); + return connectivityResult != ConnectivityResult.none; + } + + // 获取网络连接类型 + static Future getConnectionType() async { + final connectivityResult = await Connectivity().checkConnectivity(); + if (connectivityResult == ConnectivityResult.wifi) { + return 'WiFi'; + } else if (connectivityResult == ConnectivityResult.mobile) { + return 'Mobile'; + } else { + return 'No Connection'; + } + } + + // 格式化网络错误信息 + static String formatNetworkError(dynamic error) { + if (error is String) { + return error; + } else if (error.toString().contains('SocketException')) { + return 'Network connection error. Please check your internet connection.'; + } else if (error.toString().contains('TimeoutException')) { + return 'Request timeout. Please try again later.'; + } else { + return 'An error occurred. Please try again.'; + } + } +} \ No newline at end of file diff --git a/lib/src/utils/platform_utils.dart b/lib/src/utils/platform_utils.dart new file mode 100644 index 0000000..d85c435 --- /dev/null +++ b/lib/src/utils/platform_utils.dart @@ -0,0 +1,119 @@ +import 'dart:io'; + +class PlatformUtils { + static final PlatformUtils _instance = PlatformUtils._internal(); + factory PlatformUtils() => _instance; + + PlatformUtils._internal(); + + // 判断是否为 iOS 平台 + bool get isIOS => Platform.isIOS; + + // 判断是否为 Android 平台 + bool get isAndroid => Platform.isAndroid; + + // 判断是否为鸿蒙平台 + bool get isHarmonyOS { + if (Platform.isAndroid) { + final systemFeatures = Platform.operatingSystemVersion.toLowerCase(); + return systemFeatures.contains('harmony') || + systemFeatures.contains('ohos') || + systemFeatures.contains('openharmony'); + } + return false; + } + + // 判断是否为 Web 平台 + bool get isWeb => false; + + // 判断是否为 Windows 平台 + bool get isWindows => Platform.isWindows; + + // 判断是否为 macOS 平台 + bool get isMacOS => Platform.isMacOS; + + // 判断是否为 Linux 平台 + bool get isLinux => Platform.isLinux; + + // 判断是否为 Fuchsia 平台 + bool get isFuchsia => Platform.isFuchsia; + + // 获取操作系统名称 + String get operatingSystemName { + if (isHarmonyOS) return 'HarmonyOS'; + if (Platform.isIOS) return 'iOS'; + if (Platform.isAndroid) return 'Android'; + if (Platform.isWindows) return 'Windows'; + if (Platform.isMacOS) return 'macOS'; + if (Platform.isLinux) return 'Linux'; + if (Platform.isFuchsia) return 'Fuchsia'; + return 'Unknown'; + } + + // 获取操作系统版本 + String get operatingSystemVersion => Platform.operatingSystemVersion; + + // 获取 Dart 版本 + String get dartVersion => Platform.version; + + // 获取本地主机名 + String get localHostname => Platform.localHostname; + + // 获取环境变量 + Map get environment => Platform.environment; + + // 获取可执行文件路径 + String get executable => Platform.executable; + + // 获取可执行文件所在目录 + String get resolvedExecutable => Platform.resolvedExecutable; + + // 获取脚本路径 + String get script => Platform.script.toString(); + + // 获取包配置路径 + String get packageConfig => Platform.packageConfig?.toString() ?? ''; + + // 获取设备信息 + String get deviceInfo { + return 'Platform: $operatingSystemName, Version: $operatingSystemVersion, Dart: $dartVersion'; + } + + // 判断是否为移动平台 + bool get isMobile => isIOS || isAndroid || isHarmonyOS; + + // 判断是否为桌面平台 + bool get isDesktop => isWindows || isMacOS || isLinux; + + // 判断是否为苹果平台 + bool get isApple => isIOS || isMacOS; + + // 判断是否为谷歌平台 + bool get isGoogle => isAndroid; + + // 获取 CPU 核心数 + int get numberOfProcessors => Platform.numberOfProcessors; + + // 获取路径分隔符 + String get pathSeparator => Platform.pathSeparator; + + // 获取行分隔符 + String get lineTerminator { + if (Platform.isWindows) return '\r\n'; + return '\n'; + } + + // 获取系统特性 + List get systemFeatures { + try { + return Platform.operatingSystemVersion.split(' '); + } catch (e) { + return []; + } + } + + // 检查是否支持特定特性 + bool hasFeature(String feature) { + return systemFeatures.any((f) => f.toLowerCase().contains(feature.toLowerCase())); + } +} diff --git a/lib/src/utils/string_utils.dart b/lib/src/utils/string_utils.dart new file mode 100644 index 0000000..119494a --- /dev/null +++ b/lib/src/utils/string_utils.dart @@ -0,0 +1,36 @@ +class StringUtils { + // 检查字符串是否为空 + static bool isEmpty(String? value) { + return value == null || value.trim().isEmpty; + } + + // 检查字符串是否不为空 + static bool isNotEmpty(String? value) { + return !isEmpty(value); + } + + // 截取字符串 + static String truncate(String value, int maxLength, {String suffix = '...'}) { + if (value.length <= maxLength) { + return value; + } + return '${value.substring(0, maxLength)} $suffix'; + } + + // 格式化价格 + static String formatPrice(double price, {String currency = '\$'}) { + return '$currency${price.toStringAsFixed(2)}'; + } + + // 验证邮箱格式 + static bool isValidEmail(String email) { + final emailRegex = RegExp(r'^[^\s@]+@[^\s@]+\.[^\s@]+$'); + return emailRegex.hasMatch(email); + } + + // 验证手机号格式(简单验证) + static bool isValidPhone(String phone) { + final phoneRegex = RegExp(r'^\d{10,15}$'); + return phoneRegex.hasMatch(phone); + } +} \ No newline at end of file diff --git a/lib/src/widgets/adaptive_page_interface.dart b/lib/src/widgets/adaptive_page_interface.dart new file mode 100644 index 0000000..8a66e51 --- /dev/null +++ b/lib/src/widgets/adaptive_page_interface.dart @@ -0,0 +1,412 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:mom_kitchen/src/services/theme_service.dart'; +import 'package:mom_kitchen/src/widgets/adaptive_scaffold.dart'; + +abstract class AdaptivePageInterface { + String get pageTitle; + IconData get pageIcon; + IconData? get selectedPageIcon => pageIcon; + List get appBarActions => []; + Widget? get floatingActionButton => null; + bool get showSecondaryBody => false; + Widget buildBody(BuildContext context); + Widget? buildSecondaryBody(BuildContext context) => null; +} + +mixin AdaptivePageMixin on State + implements AdaptivePageInterface { + late ThemeService _themeService; + + ThemeService get themeService => _themeService; + Color get backgroundColor => _themeService.backgroundColor; + Color get textColor => _themeService.textColor; + Color get primaryColor => _themeService.primaryColor; + double get fontSize => _themeService.fontSize; + + @override + IconData? get selectedPageIcon => pageIcon; + + @override + List get appBarActions => []; + + @override + Widget? get floatingActionButton => null; + + @override + bool get showSecondaryBody => false; + + @override + Widget? buildSecondaryBody(BuildContext context) => null; + + @override + void initState() { + super.initState(); + _themeService = ThemeService(); + } + + Widget buildAdaptivePage(BuildContext context) { + return Scaffold( + backgroundColor: backgroundColor, + body: AdaptiveLayout( + topNavigation: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('topNavigation'), + builder: (_) => _buildAppBar(), + ), + }, + ), + body: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('bodySmall'), + builder: (_) => buildBody(context), + ), + Breakpoints.medium: SlotLayout.from( + key: const Key('bodyMedium'), + builder: (_) => buildBody(context), + ), + Breakpoints.large: SlotLayout.from( + key: const Key('bodyLarge'), + builder: (_) => buildBody(context), + ), + }, + ), + secondaryBody: showSecondaryBody + ? SlotLayout( + config: { + Breakpoints.large: SlotLayout.from( + key: const Key('secondaryBody'), + builder: (_) => + buildSecondaryBody(context) ?? + _buildDefaultSecondaryBody(), + ), + }, + ) + : null, + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return CupertinoNavigationBar( + middle: Text( + pageTitle, + style: TextStyle( + color: textColor, + fontSize: fontSize, + fontWeight: FontWeight.w600, + ), + ), + trailing: Row(mainAxisSize: MainAxisSize.min, children: appBarActions), + backgroundColor: backgroundColor.withValues(alpha: 0.95), + border: null, + ); + } + + Widget _buildDefaultSecondaryBody() { + return Container( + color: backgroundColor, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + CupertinoIcons.sidebar_right, + size: 48, + color: textColor.withValues(alpha: 0.4), + ), + const SizedBox(height: 16), + Text( + 'Detail Panel', + style: TextStyle( + color: textColor.withValues(alpha: 0.6), + fontSize: fontSize, + ), + ), + ], + ), + ), + ); + } +} + +abstract class AdaptiveTabPageInterface extends AdaptivePageInterface { + List get destinations; + int get initialIndex => 0; + void onDestinationSelected(int index); +} + +mixin AdaptiveTabPageMixin on State + implements AdaptiveTabPageInterface { + late ThemeService _themeService; + late int _selectedIndex; + + ThemeService get themeService => _themeService; + Color get backgroundColor => _themeService.backgroundColor; + Color get textColor => _themeService.textColor; + Color get primaryColor => _themeService.primaryColor; + double get fontSize => _themeService.fontSize; + int get selectedIndex => _selectedIndex; + + @override + IconData? get selectedPageIcon => pageIcon; + + @override + List get appBarActions => []; + + @override + Widget? get floatingActionButton => null; + + @override + bool get showSecondaryBody => false; + + @override + Widget? buildSecondaryBody(BuildContext context) => null; + + @override + void initState() { + super.initState(); + _themeService = ThemeService(); + _selectedIndex = initialIndex; + } + + void setSelectedIndex(int index) { + setState(() { + _selectedIndex = index; + }); + onDestinationSelected(index); + } + + Widget buildAdaptiveTabPage(BuildContext context) { + return Scaffold( + backgroundColor: backgroundColor, + body: AdaptiveLayout( + topNavigation: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('topNavigation'), + builder: (_) => _buildAppBar(), + ), + }, + ), + primaryNavigation: SlotLayout( + config: { + Breakpoints.medium: SlotLayout.from( + key: const Key('primaryNavigationMedium'), + builder: (_) => _buildNavigationRail(extended: false), + ), + Breakpoints.large: SlotLayout.from( + key: const Key('primaryNavigationLarge'), + builder: (_) => _buildNavigationRail(extended: true), + ), + }, + ), + bottomNavigation: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('bottomNavigation'), + builder: (_) => _buildBottomNavigationBar(), + ), + }, + ), + body: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('bodySmall'), + builder: (_) => buildBody(context), + ), + Breakpoints.medium: SlotLayout.from( + key: const Key('bodyMedium'), + builder: (_) => buildBody(context), + ), + Breakpoints.large: SlotLayout.from( + key: const Key('bodyLarge'), + builder: (_) => buildBody(context), + ), + }, + ), + secondaryBody: showSecondaryBody + ? SlotLayout( + config: { + Breakpoints.large: SlotLayout.from( + key: const Key('secondaryBody'), + builder: (_) => + buildSecondaryBody(context) ?? + _buildDefaultSecondaryBody(), + ), + }, + ) + : null, + ), + ); + } + + PreferredSizeWidget _buildAppBar() { + return CupertinoNavigationBar( + middle: Text( + pageTitle, + style: TextStyle( + color: textColor, + fontSize: fontSize, + fontWeight: FontWeight.w600, + ), + ), + trailing: Row(mainAxisSize: MainAxisSize.min, children: appBarActions), + backgroundColor: backgroundColor.withValues(alpha: 0.95), + border: null, + ); + } + + Widget _buildNavigationRail({required bool extended}) { + return Container( + color: backgroundColor, + child: NavigationRail( + selectedIndex: _selectedIndex, + onDestinationSelected: setSelectedIndex, + extended: extended, + backgroundColor: backgroundColor, + indicatorColor: primaryColor.withValues(alpha: 0.15), + selectedIconTheme: IconThemeData(color: primaryColor, size: 28), + unselectedIconTheme: IconThemeData( + color: textColor.withValues(alpha: 0.6), + size: 24, + ), + selectedLabelTextStyle: TextStyle( + color: primaryColor, + fontWeight: FontWeight.w600, + fontSize: fontSize, + ), + unselectedLabelTextStyle: TextStyle( + color: textColor.withValues(alpha: 0.6), + fontSize: fontSize, + ), + leading: floatingActionButton, + destinations: destinations.map((dest) { + return NavigationRailDestination( + icon: Icon(dest.icon), + selectedIcon: Icon(dest.selectedIcon ?? dest.icon), + label: Text(dest.label), + ); + }).toList(), + ), + ); + } + + Widget _buildBottomNavigationBar() { + return CupertinoTabBar( + currentIndex: _selectedIndex, + onTap: setSelectedIndex, + backgroundColor: backgroundColor.withValues(alpha: 0.95), + activeColor: primaryColor, + inactiveColor: textColor.withValues(alpha: 0.6), + items: destinations.map((dest) { + return BottomNavigationBarItem( + icon: Icon(dest.icon), + activeIcon: Icon(dest.selectedIcon ?? dest.icon), + label: dest.label, + ); + }).toList(), + ); + } + + Widget _buildDefaultSecondaryBody() { + return Container( + color: backgroundColor, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + CupertinoIcons.sidebar_right, + size: 48, + color: textColor.withValues(alpha: 0.4), + ), + const SizedBox(height: 16), + Text( + 'Detail Panel', + style: TextStyle( + color: textColor.withValues(alpha: 0.6), + fontSize: fontSize, + ), + ), + ], + ), + ), + ); + } +} + +class AdaptivePageController { + static Widget buildWithLayout({ + required String title, + required Widget Function(BuildContext context) bodyBuilder, + List appBarActions = const [], + Widget? floatingActionButton, + bool showSecondaryBody = false, + Widget Function(BuildContext context)? secondaryBodyBuilder, + Color? backgroundColor, + Color? textColor, + }) { + final themeService = ThemeService(); + final bgColor = backgroundColor ?? themeService.backgroundColor; + final txtColor = textColor ?? themeService.textColor; + + return Scaffold( + backgroundColor: bgColor, + body: AdaptiveLayout( + topNavigation: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('topNavigation'), + builder: (_) => CupertinoNavigationBar( + middle: Text( + title, + style: TextStyle( + color: txtColor, + fontSize: themeService.fontSize, + fontWeight: FontWeight.w600, + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: appBarActions, + ), + backgroundColor: bgColor.withValues(alpha: 0.95), + border: null, + ), + ), + }, + ), + body: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('bodySmall'), + builder: bodyBuilder, + ), + Breakpoints.medium: SlotLayout.from( + key: const Key('bodyMedium'), + builder: bodyBuilder, + ), + Breakpoints.large: SlotLayout.from( + key: const Key('bodyLarge'), + builder: bodyBuilder, + ), + }, + ), + secondaryBody: showSecondaryBody && secondaryBodyBuilder != null + ? SlotLayout( + config: { + Breakpoints.large: SlotLayout.from( + key: const Key('secondaryBody'), + builder: secondaryBodyBuilder, + ), + }, + ) + : null, + ), + ); + } +} diff --git a/lib/src/widgets/adaptive_scaffold.dart b/lib/src/widgets/adaptive_scaffold.dart new file mode 100644 index 0000000..8efb14e --- /dev/null +++ b/lib/src/widgets/adaptive_scaffold.dart @@ -0,0 +1,385 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; +import 'package:mom_kitchen/src/services/theme_service.dart'; + +enum AdaptiveLayoutType { + compact, // 手机竖屏 (< 600dp) + medium, // 手机横屏/小平板 (600-840dp) + expanded, // 平板/大屏幕 (> 840dp) +} + +class AdaptiveDestination { + final IconData icon; + final IconData? selectedIcon; + final String label; + final Widget? badge; + + const AdaptiveDestination({ + required this.icon, + this.selectedIcon, + required this.label, + this.badge, + }); +} + +class AdaptiveScaffoldWidget extends StatefulWidget { + final List destinations; + final Widget? appBarTitle; + final List? appBarActions; + final int initialIndex; + final void Function(int index)? onDestinationSelected; + final Widget Function(int selectedIndex)? bodyBuilder; + final Widget? floatingActionButton; + + const AdaptiveScaffoldWidget({ + super.key, + required this.destinations, + this.appBarTitle, + this.appBarActions, + this.initialIndex = 0, + this.onDestinationSelected, + this.bodyBuilder, + this.floatingActionButton, + }); + + @override + State createState() => _AdaptiveScaffoldWidgetState(); +} + +class _AdaptiveScaffoldWidgetState extends State { + late int _selectedIndex; + late ThemeService _themeService; + + @override + void initState() { + super.initState(); + _selectedIndex = widget.initialIndex; + _themeService = ThemeService(); + } + + void _onDestinationSelected(int index) { + setState(() { + _selectedIndex = index; + }); + widget.onDestinationSelected?.call(index); + } + + @override + Widget build(BuildContext context) { + final backgroundColor = _themeService.backgroundColor; + final textColor = _themeService.textColor; + final primaryColor = _themeService.primaryColor; + + return Scaffold( + backgroundColor: backgroundColor, + body: AdaptiveLayout( + topNavigation: SlotLayout( + config: { + Breakpoints.standard: SlotLayout.from( + key: const Key('topNavigation'), + builder: (_) => _buildAppBar(backgroundColor, textColor), + ), + }, + ), + primaryNavigation: SlotLayout( + config: { + Breakpoints.medium: SlotLayout.from( + key: const Key('primaryNavigationMedium'), + builder: (_) => _buildNavigationRail( + backgroundColor, + textColor, + primaryColor, + extended: false, + ), + ), + Breakpoints.large: SlotLayout.from( + key: const Key('primaryNavigationLarge'), + builder: (_) => _buildNavigationRail( + backgroundColor, + textColor, + primaryColor, + extended: true, + ), + ), + }, + ), + bottomNavigation: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('bottomNavigation'), + builder: (_) => _buildBottomNavigationBar( + backgroundColor, + textColor, + primaryColor, + ), + ), + }, + ), + body: SlotLayout( + config: { + Breakpoints.small: SlotLayout.from( + key: const Key('bodySmall'), + builder: (_) => _buildBody(), + ), + Breakpoints.medium: SlotLayout.from( + key: const Key('bodyMedium'), + builder: (_) => _buildBody(), + ), + Breakpoints.large: SlotLayout.from( + key: const Key('bodyLarge'), + builder: (_) => _buildBody(), + ), + }, + ), + secondaryBody: SlotLayout( + config: { + Breakpoints.large: SlotLayout.from( + key: const Key('secondaryBody'), + builder: (_) => + _buildSecondaryBody(backgroundColor, textColor) ?? + const SizedBox.shrink(), + ), + }, + ), + ), + ); + } + + PreferredSizeWidget _buildAppBar(Color backgroundColor, Color textColor) { + return CupertinoNavigationBar( + middle: + widget.appBarTitle ?? + Text( + widget.destinations[_selectedIndex].label, + style: TextStyle( + color: textColor, + fontSize: _themeService.fontSize, + fontWeight: FontWeight.w600, + ), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: widget.appBarActions ?? [], + ), + backgroundColor: backgroundColor.withValues(alpha: 0.95), + border: null, + ); + } + + Widget _buildNavigationRail( + Color backgroundColor, + Color textColor, + Color primaryColor, { + required bool extended, + }) { + return Container( + color: backgroundColor, + child: NavigationRail( + selectedIndex: _selectedIndex, + onDestinationSelected: _onDestinationSelected, + extended: extended, + backgroundColor: backgroundColor, + indicatorColor: primaryColor.withValues(alpha: 0.15), + selectedIconTheme: IconThemeData(color: primaryColor, size: 28), + unselectedIconTheme: IconThemeData( + color: textColor.withValues(alpha: 0.6), + size: 24, + ), + selectedLabelTextStyle: TextStyle( + color: primaryColor, + fontWeight: FontWeight.w600, + fontSize: _themeService.fontSize, + ), + unselectedLabelTextStyle: TextStyle( + color: textColor.withValues(alpha: 0.6), + fontSize: _themeService.fontSize, + ), + leading: widget.floatingActionButton, + destinations: widget.destinations.map((dest) { + return NavigationRailDestination( + icon: Icon(dest.icon), + selectedIcon: Icon(dest.selectedIcon ?? dest.icon), + label: Text(dest.label), + ); + }).toList(), + ), + ); + } + + Widget _buildBottomNavigationBar( + Color backgroundColor, + Color textColor, + Color primaryColor, + ) { + return CupertinoTabBar( + currentIndex: _selectedIndex, + onTap: _onDestinationSelected, + backgroundColor: backgroundColor.withValues(alpha: 0.95), + activeColor: primaryColor, + inactiveColor: textColor.withValues(alpha: 0.6), + items: widget.destinations.map((dest) { + return BottomNavigationBarItem( + icon: Icon(dest.icon), + activeIcon: Icon(dest.selectedIcon ?? dest.icon), + label: dest.label, + ); + }).toList(), + ); + } + + Widget _buildBody() { + if (widget.bodyBuilder != null) { + return widget.bodyBuilder!(_selectedIndex); + } + return Center( + child: Text( + 'Page ${_selectedIndex + 1}', + style: TextStyle( + color: _themeService.textColor, + fontSize: _themeService.fontSize, + ), + ), + ); + } + + Widget? _buildSecondaryBody(Color backgroundColor, Color textColor) { + return Container( + color: backgroundColor, + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + CupertinoIcons.sidebar_right, + size: 48, + color: textColor.withValues(alpha: 0.4), + ), + const SizedBox(height: 16), + Text( + 'Detail Panel', + style: TextStyle( + color: textColor.withValues(alpha: 0.6), + fontSize: _themeService.fontSize, + ), + ), + ], + ), + ), + ); + } +} + +class AdaptiveLayoutBuilder extends StatelessWidget { + final Widget Function(BuildContext context, AdaptiveLayoutType layoutType) + builder; + + const AdaptiveLayoutBuilder({super.key, required this.builder}); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final layoutType = _getLayoutType(constraints.maxWidth); + return builder(context, layoutType); + }, + ); + } + + AdaptiveLayoutType _getLayoutType(double width) { + if (width < 600) { + return AdaptiveLayoutType.compact; + } else if (width < 840) { + return AdaptiveLayoutType.medium; + } else { + return AdaptiveLayoutType.expanded; + } + } +} + +class ResponsiveGrid extends StatelessWidget { + final List children; + final double spacing; + final double runSpacing; + final int crossAxisCount; + final int? crossAxisCountMedium; + final int? crossAxisCountLarge; + final double childAspectRatio; + + const ResponsiveGrid({ + super.key, + required this.children, + this.spacing = 16.0, + this.runSpacing = 16.0, + this.crossAxisCount = 2, + this.crossAxisCountMedium, + this.crossAxisCountLarge, + this.childAspectRatio = 1.0, + }); + + @override + Widget build(BuildContext context) { + return AdaptiveLayoutBuilder( + builder: (context, layoutType) { + final count = _getCrossAxisCount(layoutType); + return GridView.count( + crossAxisCount: count, + mainAxisSpacing: runSpacing, + crossAxisSpacing: spacing, + childAspectRatio: childAspectRatio, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + children: children, + ); + }, + ); + } + + int _getCrossAxisCount(AdaptiveLayoutType layoutType) { + switch (layoutType) { + case AdaptiveLayoutType.compact: + return crossAxisCount; + case AdaptiveLayoutType.medium: + return crossAxisCountMedium ?? crossAxisCount + 1; + case AdaptiveLayoutType.expanded: + return crossAxisCountLarge ?? + (crossAxisCountMedium ?? crossAxisCount) + 2; + } + } +} + +class ResponsivePadding extends StatelessWidget { + final Widget child; + final double compactPadding; + final double? mediumPadding; + final double? expandedPadding; + + const ResponsivePadding({ + super.key, + required this.child, + this.compactPadding = 16.0, + this.mediumPadding, + this.expandedPadding, + }); + + @override + Widget build(BuildContext context) { + return AdaptiveLayoutBuilder( + builder: (context, layoutType) { + final padding = _getPadding(layoutType); + return Padding(padding: EdgeInsets.all(padding), child: child); + }, + ); + } + + double _getPadding(AdaptiveLayoutType layoutType) { + switch (layoutType) { + case AdaptiveLayoutType.compact: + return compactPadding; + case AdaptiveLayoutType.medium: + return mediumPadding ?? compactPadding * 1.5; + case AdaptiveLayoutType.expanded: + return expandedPadding ?? (mediumPadding ?? compactPadding) * 2; + } + } +} diff --git a/lib/src/widgets/error_widget.dart b/lib/src/widgets/error_widget.dart new file mode 100644 index 0000000..7d296eb --- /dev/null +++ b/lib/src/widgets/error_widget.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +class ErrorWidget extends StatelessWidget { + final String message; + final VoidCallback? onRetry; + + const ErrorWidget({ + Key? key, + required this.message, + this.onRetry, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + size: 64, + color: Colors.red, + ), + const SizedBox(height: 16), + Text( + message, + textAlign: TextAlign.center, + style: const TextStyle( + fontSize: 16, + color: Colors.grey, + ), + ), + if (onRetry != null) + Padding( + padding: const EdgeInsets.only(top: 16), + child: ElevatedButton( + onPressed: onRetry, + child: const Text('Retry'), + ), + ), + ], + ), + ); + } +} + +// 显示错误 toast +void showErrorToast(String message) { + Fluttertoast.showToast( + msg: message, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: Colors.red, + textColor: Colors.white, + fontSize: 16.0, + ); +} + +// 显示成功 toast +void showSuccessToast(String message) { + Fluttertoast.showToast( + msg: message, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.CENTER, + timeInSecForIosWeb: 1, + backgroundColor: Colors.green, + textColor: Colors.white, + fontSize: 16.0, + ); +} \ No newline at end of file diff --git a/lib/src/widgets/responsive_grid.dart b/lib/src/widgets/responsive_grid.dart new file mode 100644 index 0000000..c76f582 --- /dev/null +++ b/lib/src/widgets/responsive_grid.dart @@ -0,0 +1 @@ +export 'adaptive_scaffold.dart'; diff --git a/lib/src/widgets/skeleton_loader.dart b/lib/src/widgets/skeleton_loader.dart new file mode 100644 index 0000000..694d8d9 --- /dev/null +++ b/lib/src/widgets/skeleton_loader.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/cupertino.dart'; + +class SkeletonLoader extends StatelessWidget { + final double width; + final double height; + final BorderRadius borderRadius; + + const SkeletonLoader({ + Key? key, + required this.width, + required this.height, + this.borderRadius = const BorderRadius.all(Radius.circular(8)), + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + color: CupertinoColors.systemGrey5, + borderRadius: borderRadius, + ), + child: const CupertinoActivityIndicator(), + ); + } +} + +class SkeletonContainer extends StatelessWidget { + final Widget child; + final bool isLoading; + final Widget? skeleton; + + const SkeletonContainer({ + Key? key, + required this.child, + required this.isLoading, + this.skeleton, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return isLoading ? skeleton ?? const SkeletonLoader(width: 200, height: 200) : child; + } +} \ No newline at end of file diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..65fcdb7 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "mom_kitchen") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.mom_kitchen") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..f6f23bf --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..df8d2f7 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..48e1fc0 --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,144 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView *view) +{ + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "mom_kitchen"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "mom_kitchen"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + //MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..72271d5 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..dc83cc9 --- /dev/null +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,24 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import connectivity_plus +import device_info_plus +import flutter_local_notifications +import package_info_plus +import path_provider_foundation +import share_plus +import shared_preferences_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) +} diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8922b07 --- /dev/null +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* mom_kitchen.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "mom_kitchen.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* mom_kitchen.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* mom_kitchen.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.momKitchen.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mom_kitchen.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mom_kitchen"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.momKitchen.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mom_kitchen.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mom_kitchen"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.momKitchen.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/mom_kitchen.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/mom_kitchen"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..511e255 --- /dev/null +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..86535a4 --- /dev/null +++ b/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = mom_kitchen + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.momKitchen + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +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/ohos/.gitignore b/ohos/.gitignore new file mode 100644 index 0000000..e326491 --- /dev/null +++ b/ohos/.gitignore @@ -0,0 +1,20 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +**/BuildProfile.ets +**/oh-package-lock.json5 +/package.json +/package-lock.json + +**/src/main/resources/rawfile/flutter_assets/ +**/libs/**/libapp.so +**/libs/**/libflutter.so +**/libs/**/libvmservice_snapshot.so diff --git a/ohos/AppScope/app.json5 b/ohos/AppScope/app.json5 new file mode 100644 index 0000000..eed8ae4 --- /dev/null +++ b/ohos/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.example.mom_kitchen", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:app_icon", + "label": "$string:app_name" + } +} diff --git a/ohos/AppScope/resources/base/element/string.json b/ohos/AppScope/resources/base/element/string.json new file mode 100644 index 0000000..2d8085a --- /dev/null +++ b/ohos/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "mom_kitchen" + } + ] +} diff --git a/ohos/AppScope/resources/base/media/app_icon.png b/ohos/AppScope/resources/base/media/app_icon.png new file mode 100644 index 0000000..ce307a8 Binary files /dev/null and b/ohos/AppScope/resources/base/media/app_icon.png differ diff --git a/ohos/build-profile.json5 b/ohos/build-profile.json5 new file mode 100644 index 0000000..06ad881 --- /dev/null +++ b/ohos/build-profile.json5 @@ -0,0 +1,53 @@ +{ + "app": { + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + "material": { + "certpath": "C:\\Users\\无书\\.ohos\\config\\default_ohos_h8eBDwGJTRHPEcoIPNZ4JJ58-IFgoGWW5H7lci4Iucs=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001B087BBDA2745E325A450A934473E20769755C58121A8AAC16F2A3D1CB393E2AA8D4A123AB60FCDF", + "profile": "C:\\Users\\无书\\.ohos\\config\\default_ohos_h8eBDwGJTRHPEcoIPNZ4JJ58-IFgoGWW5H7lci4Iucs=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "C:\\Users\\无书\\.ohos\\config\\default_ohos_h8eBDwGJTRHPEcoIPNZ4JJ58-IFgoGWW5H7lci4Iucs=.p12", + "storePassword": "0000001BE1D7FC4F0BBF710594D0D491A689575E5B7A1DED5682D50FFFF53411AD0A49B84BEB3680F60217" + } + } + ], + "products": [ + { + "name": "default", + "signingConfig": "default", + "compatibleSdkVersion": "5.1.0(18)", + "runtimeOS": "HarmonyOS", + "targetSdkVersion": "6.0.2(22)" + } + ], + "buildModeSet": [ + { + "name": "debug" + }, + { + "name": "profile" + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/ohos/entry/.gitignore b/ohos/entry/.gitignore new file mode 100644 index 0000000..5254b88 --- /dev/null +++ b/ohos/entry/.gitignore @@ -0,0 +1,7 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test +GeneratedPluginRegistrant.ets \ No newline at end of file diff --git a/ohos/entry/build-profile.json5 b/ohos/entry/build-profile.json5 new file mode 100644 index 0000000..6de31ee --- /dev/null +++ b/ohos/entry/build-profile.json5 @@ -0,0 +1,15 @@ + +{ + "apiType": 'stageMode', + "buildOption": { + }, + "targets": [ + { + "name": "default", + "runtimeOS": "HarmonyOS" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/ohos/entry/hvigorfile.ts b/ohos/entry/hvigorfile.ts new file mode 100644 index 0000000..ff3bfe0 --- /dev/null +++ b/ohos/entry/hvigorfile.ts @@ -0,0 +1,7 @@ + +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/ohos/entry/oh-package.json5 b/ohos/entry/oh-package.json5 new file mode 100644 index 0000000..854eec2 --- /dev/null +++ b/ohos/entry/oh-package.json5 @@ -0,0 +1,11 @@ + +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": {}, +} + diff --git a/ohos/entry/src/main/ets/entryability/EntryAbility.ets b/ohos/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000..f85a655 --- /dev/null +++ b/ohos/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,10 @@ + +import { FlutterAbility, FlutterEngine } from '@ohos/flutter_ohos'; +import { GeneratedPluginRegistrant } from '../plugins/GeneratedPluginRegistrant'; + +export default class EntryAbility extends FlutterAbility { + configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + GeneratedPluginRegistrant.registerWith(flutterEngine) + } +} diff --git a/ohos/entry/src/main/ets/pages/Index.ets b/ohos/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..7bb6543 --- /dev/null +++ b/ohos/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,24 @@ + +import common from '@ohos.app.ability.common'; +import { FlutterPage } from '@ohos/flutter_ohos' + +let storage = LocalStorage.getShared() +const EVENT_BACK_PRESS = 'EVENT_BACK_PRESS' + +@Entry(storage) +@Component +struct Index { + private context = getContext(this) as common.UIAbilityContext + @LocalStorageLink('viewId') viewId: string = ""; + + build() { + Column() { + FlutterPage({ viewId: this.viewId }) + } + } + + onBackPress(): boolean { + this.context.eventHub.emit(EVENT_BACK_PRESS) + return true + } +} \ No newline at end of file diff --git a/ohos/entry/src/main/module.json5 b/ohos/entry/src/main/module.json5 new file mode 100644 index 0000000..5bd7950 --- /dev/null +++ b/ohos/entry/src/main/module.json5 @@ -0,0 +1,40 @@ + +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:icon", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:icon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ], + "requestPermissions": [ + {"name" : "ohos.permission.INTERNET"}, + ] + } +} \ No newline at end of file diff --git a/ohos/entry/src/main/resources/base/element/color.json b/ohos/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000..3c71296 --- /dev/null +++ b/ohos/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/ohos/entry/src/main/resources/base/element/string.json b/ohos/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000..793a9db --- /dev/null +++ b/ohos/entry/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "mom_kitchen" + } + ] +} \ No newline at end of file diff --git a/ohos/entry/src/main/resources/base/media/icon.png b/ohos/entry/src/main/resources/base/media/icon.png new file mode 100644 index 0000000..ce307a8 Binary files /dev/null and b/ohos/entry/src/main/resources/base/media/icon.png differ diff --git a/ohos/entry/src/main/resources/base/profile/main_pages.json b/ohos/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000..1898d94 --- /dev/null +++ b/ohos/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/ohos/entry/src/main/resources/en_US/element/string.json b/ohos/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000..793a9db --- /dev/null +++ b/ohos/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "mom_kitchen" + } + ] +} \ No newline at end of file diff --git a/ohos/entry/src/main/resources/rawfile/buildinfo.json5 b/ohos/entry/src/main/resources/rawfile/buildinfo.json5 new file mode 100644 index 0000000..6125c99 --- /dev/null +++ b/ohos/entry/src/main/resources/rawfile/buildinfo.json5 @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "enable_impeller", + "value": "true" + } + ] +} \ No newline at end of file diff --git a/ohos/entry/src/main/resources/rawfile/framesconfig.json b/ohos/entry/src/main/resources/rawfile/framesconfig.json new file mode 100644 index 0000000..34d41c8 --- /dev/null +++ b/ohos/entry/src/main/resources/rawfile/framesconfig.json @@ -0,0 +1,37 @@ +{ + "SWITCH": 1, + "TRANSLATE": [ + { + "serial_number": 1, + "min": 800, + "max": -1, + "preferred_fps": 90 + }, + { + "serial_number": 2, + "min": 77, + "max": 800, + "preferred_fps": 120 + }, + { + "serial_number": 3, + "min": 46, + "max": 77, + "preferred_fps": 90 + }, + { + "serial_number": 4, + "min": 10, + "max": 46, + "preferred_fps": 72 + }, + { + "serial_number": 5, + "min": 0, + "max": 10, + "preferred_fps": 60 + } + ], + "SCALE": [], + "ROTATION": [] +} \ No newline at end of file diff --git a/ohos/entry/src/main/resources/zh_CN/element/string.json b/ohos/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000..1b21649 --- /dev/null +++ b/ohos/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "模块描述" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "mom_kitchen" + } + ] +} \ No newline at end of file diff --git a/ohos/entry/src/ohosTest/ets/test/Ability.test.ets b/ohos/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000..8abf7f2 --- /dev/null +++ b/ohos/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,35 @@ +import hilog from '@ohos.hilog'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium' + +export default function abilityTest() { + describe('ActsAbilityTest', function () { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(function () { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(function () { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(function () { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(function () { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain',0, function () { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc' + let b = 'b' + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b) + expect(a).assertEqual(a) + }) + }) +} \ No newline at end of file diff --git a/ohos/entry/src/ohosTest/ets/test/List.test.ets b/ohos/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000..d766fe2 --- /dev/null +++ b/ohos/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,5 @@ +import abilityTest from './Ability.test' + +export default function testsuite() { + abilityTest() +} \ No newline at end of file diff --git a/ohos/entry/src/ohosTest/ets/testability/TestAbility.ets b/ohos/entry/src/ohosTest/ets/testability/TestAbility.ets new file mode 100644 index 0000000..e3f6e91 --- /dev/null +++ b/ohos/entry/src/ohosTest/ets/testability/TestAbility.ets @@ -0,0 +1,48 @@ +import UIAbility from '@ohos.app.ability.UIAbility'; +import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; +import hilog from '@ohos.hilog'; +import { Hypium } from '@ohos/hypium'; +import testsuite from '../test/List.test'; +import window from '@ohos.window'; + +export default class TestAbility extends UIAbility { + onCreate(want, launchParam) { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onCreate'); + hilog.info(0x0000, 'testTag', '%{public}s', 'want param:' + JSON.stringify(want) ?? ''); + hilog.info(0x0000, 'testTag', '%{public}s', 'launchParam:'+ JSON.stringify(launchParam) ?? ''); + var abilityDelegator: any + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() + var abilityDelegatorArguments: any + abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() + hilog.info(0x0000, 'testTag', '%{public}s', 'start run testcase!!!'); + Hypium.hypiumTest(abilityDelegator, abilityDelegatorArguments, testsuite) + } + + onDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage) { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageCreate'); + windowStage.loadContent('testability/pages/Index', (err, data) => { + if (err.code) { + hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', + JSON.stringify(data) ?? ''); + }); + } + + onWindowStageDestroy() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onWindowStageDestroy'); + } + + onForeground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onForeground'); + } + + onBackground() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility onBackground'); + } +} \ No newline at end of file diff --git a/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets b/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets new file mode 100644 index 0000000..545843d --- /dev/null +++ b/ohos/entry/src/ohosTest/ets/testability/pages/Index.ets @@ -0,0 +1,36 @@ + + +import hilog from '@ohos.hilog'; + +@Entry +@Component +struct Index { + aboutToAppear() { + hilog.info(0x0000, 'testTag', '%{public}s', 'TestAbility index aboutToAppear'); + } + @State message: string = 'Hello World' + build() { + Row() { + Column() { + Text(this.message) + .fontSize(50) + .fontWeight(FontWeight.Bold) + Button() { + Text('next page') + .fontSize(20) + .fontWeight(FontWeight.Bold) + }.type(ButtonType.Capsule) + .margin({ + top: 20 + }) + .backgroundColor('#0D9FFB') + .width('35%') + .height('5%') + .onClick(()=>{ + }) + } + .width('100%') + } + .height('100%') + } + } \ No newline at end of file diff --git a/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts b/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts new file mode 100644 index 0000000..47fc89b --- /dev/null +++ b/ohos/entry/src/ohosTest/ets/testrunner/OpenHarmonyTestRunner.ts @@ -0,0 +1,50 @@ + +import hilog from '@ohos.hilog'; +import TestRunner from '@ohos.application.testRunner'; +import AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; + +var abilityDelegator = undefined +var abilityDelegatorArguments = undefined + +async function onAbilityCreateCallback() { + hilog.info(0x0000, 'testTag', '%{public}s', 'onAbilityCreateCallback'); +} + +async function addAbilityMonitorCallback(err: any) { + hilog.info(0x0000, 'testTag', 'addAbilityMonitorCallback : %{public}s', JSON.stringify(err) ?? ''); +} + +export default class OpenHarmonyTestRunner implements TestRunner { + constructor() { + } + + onPrepare() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner OnPrepare '); + } + + async onRun() { + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun run'); + abilityDelegatorArguments = AbilityDelegatorRegistry.getArguments() + abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator() + var testAbilityName = abilityDelegatorArguments.bundleName + '.TestAbility' + let lMonitor = { + abilityName: testAbilityName, + onAbilityCreate: onAbilityCreateCallback, + }; + abilityDelegator.addAbilityMonitor(lMonitor, addAbilityMonitorCallback) + var cmd = 'aa start -d 0 -a TestAbility' + ' -b ' + abilityDelegatorArguments.bundleName + var debug = abilityDelegatorArguments.parameters['-D'] + if (debug == 'true') + { + cmd += ' -D' + } + hilog.info(0x0000, 'testTag', 'cmd : %{public}s', cmd); + abilityDelegator.executeShellCommand(cmd, + (err: any, d: any) => { + hilog.info(0x0000, 'testTag', 'executeShellCommand : err : %{public}s', JSON.stringify(err) ?? ''); + hilog.info(0x0000, 'testTag', 'executeShellCommand : data : %{public}s', d.stdResult ?? ''); + hilog.info(0x0000, 'testTag', 'executeShellCommand : data : %{public}s', d.exitCode ?? ''); + }) + hilog.info(0x0000, 'testTag', '%{public}s', 'OpenHarmonyTestRunner onRun end'); + } +} \ No newline at end of file diff --git a/ohos/entry/src/ohosTest/module.json5 b/ohos/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000..3b02a75 --- /dev/null +++ b/ohos/entry/src/ohosTest/module.json5 @@ -0,0 +1,37 @@ + +{ + "module": { + "name": "entry_test", + "type": "feature", + "description": "$string:module_test_desc", + "mainElement": "TestAbility", + "deviceTypes": [ + "phone" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:test_pages", + "abilities": [ + { + "name": "TestAbility", + "srcEntry": "./ets/testability/TestAbility.ets", + "description": "$string:TestAbility_desc", + "icon": "$media:icon", + "label": "$string:TestAbility_label", + "exported": true, + "startWindowIcon": "$media:icon", + "startWindowBackground": "$color:start_window_background", + "skills": [ + { + "actions": [ + "action.system.home" + ], + "entities": [ + "entity.system.home" + ] + } + ] + } + ] + } +} diff --git a/ohos/entry/src/ohosTest/resources/base/element/color.json b/ohos/entry/src/ohosTest/resources/base/element/color.json new file mode 100644 index 0000000..3c71296 --- /dev/null +++ b/ohos/entry/src/ohosTest/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/ohos/entry/src/ohosTest/resources/base/element/string.json b/ohos/entry/src/ohosTest/resources/base/element/string.json new file mode 100644 index 0000000..65d8fa5 --- /dev/null +++ b/ohos/entry/src/ohosTest/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_test_desc", + "value": "test ability description" + }, + { + "name": "TestAbility_desc", + "value": "the test ability" + }, + { + "name": "TestAbility_label", + "value": "test label" + } + ] +} \ No newline at end of file diff --git a/ohos/entry/src/ohosTest/resources/base/media/icon.png b/ohos/entry/src/ohosTest/resources/base/media/icon.png new file mode 100644 index 0000000..ce307a8 Binary files /dev/null and b/ohos/entry/src/ohosTest/resources/base/media/icon.png differ diff --git a/ohos/entry/src/ohosTest/resources/base/profile/test_pages.json b/ohos/entry/src/ohosTest/resources/base/profile/test_pages.json new file mode 100644 index 0000000..b7e7343 --- /dev/null +++ b/ohos/entry/src/ohosTest/resources/base/profile/test_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "testability/pages/Index" + ] +} diff --git a/ohos/hvigor/hvigor-config.json5 b/ohos/hvigor/hvigor-config.json5 new file mode 100644 index 0000000..ad3ecab --- /dev/null +++ b/ohos/hvigor/hvigor-config.json5 @@ -0,0 +1,6 @@ + +{ + "modelVersion": "5.1.0", + "dependencies": { + } +} \ No newline at end of file diff --git a/ohos/hvigorconfig.ts b/ohos/hvigorconfig.ts new file mode 100644 index 0000000..fc2b208 --- /dev/null +++ b/ohos/hvigorconfig.ts @@ -0,0 +1,4 @@ +import path from 'path' +import { injectNativeModules } from 'flutter-hvigor-plugin'; + +injectNativeModules(__dirname, path.dirname(__dirname)) \ No newline at end of file diff --git a/ohos/hvigorfile.ts b/ohos/hvigorfile.ts new file mode 100644 index 0000000..a64db46 --- /dev/null +++ b/ohos/hvigorfile.ts @@ -0,0 +1,8 @@ +import path from 'path' +import { appTasks } from '@ohos/hvigor-ohos-plugin'; +import { flutterHvigorPlugin } from 'flutter-hvigor-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[flutterHvigorPlugin(path.dirname(__dirname))] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/ohos/oh-package.json5 b/ohos/oh-package.json5 new file mode 100644 index 0000000..6dd1730 --- /dev/null +++ b/ohos/oh-package.json5 @@ -0,0 +1,13 @@ +{ + "modelVersion": "5.1.0", + "name": "mom_kitchen", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": {}, + "devDependencies": { + "@ohos/hypium": "1.0.6" + } +} diff --git a/packages/flutter_screenutil b/packages/flutter_screenutil new file mode 160000 index 0000000..0120309 --- /dev/null +++ b/packages/flutter_screenutil @@ -0,0 +1 @@ +Subproject commit 0120309a5f02137ba4f84aecddd53b82bde38c60 diff --git a/packages/ohos平台适配flutter三方库指导.md b/packages/ohos平台适配flutter三方库指导.md new file mode 100644 index 0000000..4f99062 --- /dev/null +++ b/packages/ohos平台适配flutter三方库指导.md @@ -0,0 +1,320 @@ +# ohos平台适配flutter三方库指导 + +## 1. 准备工作 + +flutter开发环境已配置:[参考](https://gitcode.com/openharmony-sig/flutter_flutter/blob/master/README.md) + +下载待适配的三方插件:[官方插件库](https://pub.dev/) + +本指导书, +以适配 [path_provider 2.1.0](https://pub-web.flutter-io.cn/packages/path_provider/versions/2.1.0) 为例 + +## 2. 插件目录 + +![image-20240410105254011](../media/07_1/01_Plugin_Directory.png) + +lib:是对接dart端代码的入口,由此文件接收到参数后,通过channel将数据发送到原生端; + +android:安卓端代码实现目录; + +ios:ios原生端实现目录; + +example:一个依赖于该插件的Flutter应用程序,来说明如何使用它; + +README.md:介绍包的文件; + +CHANGELOG.md:记录每个版本中的更改; + +LICENSE:包含软件包许可条款的文件。 + +## 3. 创建插件的ohos模块 + +命令:`flutter create --platforms ohos,android,ios --org ` + +步骤: + +1. 用Android Studio打开刚刚下载好的插件。 + +2. 打开Terminal,cd到插件目录下。 + +3. 执行命令`flutter create --platforms ohos path_provider_ohos` 创建一个ohos平台的flutter模块。 + + 执行创建命令前: + + ![image-20240410105254011](../media/07_1/02_Flutter_plugin_structure.png) + + 执行创建命令后,可以将path_provider_ohos目录下的.dart_tool和.ldea文件删除。 + + ![image-20240410105254011](../media/07_1/03_OpenHarmony_plugin_structure.png) + +## 4. 编写ohos插件的dart接口和pubspec.yaml文件 + +可直接复制path_provider_android目录下lib的dart代码和pubspec.yaml文件进行修改。 + +dart代码基本不需要修改,只需要将android字样改为ohos。 + +lib目录dart代码: + +![image-20240410105254011](../media/07_1/04_ohos_plugin_dart_side_structure.png) + +pubspec.yaml文件: + +``` +# 仅做参考 +name: path_provider_ohos +description: Ohos implementation of the path_provider plugin. +repository: https://gitcode.com/openharmony-tpc/flutter_packages/tree/master/packages/path_provider/path_provider_ohos +issue_tracker: https://gitcode.com/openharmony-tpc/flutter_packages/issues +version: 2.2.1 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +flutter: + plugin: + implements: path_provider + platforms: + ohos: + package: io.flutter.plugins.pathprovider + pluginClass: PathProviderPlugin + dartPluginClass: PathProviderOhos + +dependencies: + flutter: + sdk: flutter + path_provider_platform_interface: ^2.0.1 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + pigeon: ^9.2.4 + test: ^1.16.0 +``` + +## 5. 编写ohos插件的原生ets模块 + +### 5.1 创建ohos的插件模块 + +由于是写ohos平台的flutter插件,而不是写一个应用,需要将原来的entry模块删除,新建一个path_provider插件的静态模块,用来写ets原生代码逻辑。 + +步骤: + +1. 用DevEco Studio打开path_provider_ohos下的ohos项目: + + ![image-20240410105254011](../media/07_1/05_The_ohos_project_in_the_OpenHarmony_plugin.png) + +2. 新建一个名称为path_provider的静态模块: + + 在DevEco Studio左上角点击`Flie > New > Module > Static Library > Next`,module name填写为`path_provider`,其他选项为默认,点击Finish,完成创建。 + + ![image-20240410105254011](../media/07_1/06_Create_new_path_provider_module.png) + +3. 删除entry以及其他多余目录: + + entry目录(entry是用来写应用的,现在是要写插件,此处已不需要,应该删除),将`path_provider > src > main > ets`目录下的文件全部删除(此处是一些模板代码可删除)。 + + ![image-20240410105254011](../media/07_1/07_Delete_entry_and_other_redundant_directories.png) + +### 5.2 修改相关配置文件 + +1. 在path_provider目录内的oh-package.json5添加libs/flutter.har 依赖: + + ``` + { + "name": "path_provider", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "Index.ets", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "@ohos/flutter_ohos": "file:libs/flutter.har" //此处为添加的依赖 + } + } + ``` + +2. 将path_provider目录外侧的oh-package.json5的dependencies中的flutter.har依赖删除: + + ``` + { + "name": "path_provider_ohos", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.6" + }, + } + ``` + +3. 在path_provider目录下添加flutter.har: + + ![image-20240410105254011](../media/07_1/08_Add_flutter.har_file.png) + +### 5.3 编写ets代码 + +文件结构,和代码逻辑可参考安卓或ios:https://gitcode.com/openharmony-tpc/flutter_packages/tree/master/packages/path_provider/path_provider_android + +ohos的api可以参考:https://gitcode.com/openharmony/docs + +![image-20240410105254011](../media/07_1/09_Write_ets_code.png) + + +### 5.4 修改index文件 + +``` +# 仅作参考 +import PathProviderPlugin from './src/main/ets/io/flutter/plugins/pathprovider/PathProviderPlugin' + +export default PathProviderPlugin +``` + +### 5.5 打har + +写完代码,改完配置文件后,即可打har包。 + +打包工具:DevEco Studio。 + +打包步骤: +1. 鼠标定位到path_provider目录。 +2. 点击DevEco Studio中的Build。 +3. 点击Make Module 'pathprovider'选项。 +4. 等待打包完成。 + +![image-20240410105254011](../media/07_1/10_Steps_for_creating_har_packages_for_the_project.png) + +预期结果: + +在`path_provider > build > default > outputs `中有path_provider.har生成,即为打har包成功。 + +![image-20240410105254011](../media/07_1/11_Successfully_converted_to_har_package.png) + +## 6. 编写example + +### 6.1 创建一个ohos平台的flutter example应用,用来验证刚刚适配的插件功能 + +cd 到path_provider_ohos目录下 ; + +命令:`flutter create --platforms ohos example` + +工具:Android Studio + +![image-20240410105254011](../media/07_1/12_Create_example_command.png) + +![image-20240410105254011](../media/07_1/13_example.png) + +### 6.2 修改dart代码 + +复制`path_provider_android\example\lib`下的main.dart代码,替换`path_provider_ohos\example\lib`下的main.dart代码。 + +### 6.3 修改example pubspec.yaml文件 + +``` +#仅作参考 +name: path_provider_example +description: Demonstrates how to use the path_provider plugin. +publish_to: none + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + path_provider: + path: ../../path_provider + path_provider_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true +``` + +## 7. 修改path_provider的pubspec.yaml文件 + +flutter: plugin:platforms添加ohos。 + +dependencies:添加path_provider_ohos依赖。 + +``` +name: path_provider +description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. +repository: https://github.com/flutter/packages/tree/main/packages/path_provider/path_provider +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 +version: 2.1.0 + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +flutter: + plugin: + platforms: + android: + default_package: path_provider_android + ios: + default_package: path_provider_foundation + linux: + default_package: path_provider_linux + macos: + default_package: path_provider_foundation + windows: + default_package: path_provider_windows + ohos: + default_package: path_provider_ohos #此处为添加 + +dependencies: + flutter: + sdk: flutter + path_provider_android: ^2.1.0 + path_provider_foundation: ^2.3.0 + path_provider_linux: ^2.2.0 + path_provider_platform_interface: ^2.1.0 + path_provider_windows: ^2.2.0 + path_provider_ohos: + path: ../path_provider_ohos #此处为添加 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 + test: ^1.16.0 +``` + +## 8. 运行example + +### 8.1 签名 + +用 `Deveco Studio` 打开三方库的 `example > ohos` 目录。 + +单击 `File > Project Structure > Project > Signing Configs` 界面勾选 `Automatically generate signature`,等待自动签名完成即可,单击 `OK`。 + +![image-20240410105254011](../media/07_1/14_Signature.png) + +### 8.2 运行 + +cd到`path_provider_ohos\example > ohos`目录,使用下列指令运行: + +``` +flutter pub get +flutter run -d +``` + +**运行成功效果如下:** + +![image-20240410105254011](../media/07_1/15_Successful_effect.png) diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..3e48a54 --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,908 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + url: "https://pub.flutter-io.cn" + source: hosted + version: "61.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.13.0" + animations: + dependency: "direct main" + description: + path: "packages/animations" + ref: "br_animations-v2.0.11_ohos" + resolved-ref: "5f82759ca95db72decad27f0253ee50814ff8d7d" + url: "https://gitcode.com/openharmony-sig/flutter_packages.git" + source: git + version: "2.0.11" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + path: "packages/connectivity_plus/connectivity_plus" + ref: HEAD + resolved-ref: "33d37b3ab716467ac77366c184b3a3c3650d9eff" + url: "https://gitcode.com/openharmony-sig/flutter_plus_plugins.git" + source: git + version: "5.0.1" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.4" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.3.5+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.9" + dbus: + dependency: transitive + description: + name: dbus + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.12" + device_info_plus: + dependency: "direct main" + description: + path: "packages/device_info_plus/device_info_plus" + ref: HEAD + resolved-ref: "33d37b3ab716467ac77366c184b3a3c3650d9eff" + url: "https://gitcode.com/openharmony-sig/flutter_plus_plugins.git" + source: git + version: "9.1.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.3" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.9.2" + dio_cache_interceptor: + dependency: "direct main" + description: + name: dio_cache_interceptor + sha256: "1346705a2057c265014d7696e3e2318b560bfb00b484dac7f9b01e2ceaebb07d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.5.1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_adaptive_scaffold: + dependency: "direct main" + description: + path: "packages/flutter_adaptive_scaffold" + ref: HEAD + resolved-ref: a7dd1d3a77d66233629742a301eaf7ec32e50023 + url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" + source: git + version: "0.1.7+1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + path: flutter_local_notifications + ref: "br_flutter_local_notifications-v17.2.4_ohos" + resolved-ref: b2056b29243e05c4c0774360933929a4836da9a3 + url: "https://gitcode.com/openharmony-sig/fluttertpc_flutter_local_notifications.git" + source: git + version: "17.2.4" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.2.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_screenutil: + dependency: "direct main" + description: + path: "packages/flutter_screenutil" + relative: true + source: path + version: "5.9.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + path: "packages/fluttertoast" + relative: true + source: path + version: "9.0.0" + fluttertoast_ohos: + dependency: transitive + description: + path: "packages/fluttertoast_ohos" + relative: true + source: path + version: "9.0.0" + get: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: be2738d5711fc351eb51d753db1dfef0cfb38fc0 + url: "https://gitcode.com/openharmony-sig/fluttertpc_get" + source: git + version: "4.6.5" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.3" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.20.2" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.0.1" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.6.7" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.flutter-io.cn" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.1.0" + logger: + dependency: "direct main" + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.7.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.0.0" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.5.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + package_info_plus: + dependency: "direct main" + description: + path: "packages/package_info_plus/package_info_plus" + ref: "br_package_info_plus-v8.1.0_ohos" + resolved-ref: "2180f5bd763cc26478d4f0baba4acdc1528f4647" + url: "https://gitcode.com/openharmony-sig/flutter_plus_plugins.git" + source: git + version: "8.1.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.1" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.9.1" + path_provider: + dependency: "direct main" + description: + path: "packages/path_provider/path_provider" + ref: master + resolved-ref: a7dd1d3a77d66233629742a301eaf7ec32e50023 + url: "https://gitcode.com/openharmony-sig/flutter_packages.git" + source: git + version: "2.1.0" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "914a07484c4380e572998d30486e77e0d9cd2faec72fee268086d07bf7f302c9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.1" + path_provider_ohos: + dependency: transitive + description: + path: "packages/path_provider/path_provider_ohos" + ref: HEAD + resolved-ref: a7dd1d3a77d66233629742a301eaf7ec32e50023 + url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" + source: git + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + path: permission_handler + ref: "br_permission_handler_v11.3.1_ohos" + resolved-ref: "86eae5711f8c561d2a4ef2a11030ff1330175b1b" + url: "https://gitcode.com/openharmony-sig/flutter_permission_handler.git" + source: git + version: "11.3.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc + url: "https://pub.flutter-io.cn" + source: hosted + version: "12.1.0" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.1.3+5" + permission_handler_ohos: + dependency: transitive + description: + path: permission_handler_ohos + ref: "br_permission_handler_v11.3.1_ohos" + resolved-ref: "86eae5711f8c561d2a4ef2a11030ff1330175b1b" + url: "https://gitcode.com/openharmony-sig/flutter_permission_handler.git" + source: git + version: "10.3.2" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.2.1" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.flutter-io.cn" + source: hosted + version: "7.0.2" + pigeon: + dependency: "direct main" + description: + path: "packages/pigeon" + ref: HEAD + resolved-ref: a7dd1d3a77d66233629742a301eaf7ec32e50023 + url: "https://gitcode.com/openharmony-sig/flutter_packages.git" + source: git + version: "11.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.6" + platform_info: + dependency: "direct main" + description: + name: platform_info + sha256: "99274ab08fb3b88527b51c998d34a10c46582242c34a869bbfaac84a891ae216" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.8" + pretty_dio_logger: + dependency: "direct main" + description: + name: pretty_dio_logger + sha256: "36f2101299786d567869493e2f5731de61ce130faa14679473b26905a92b6407" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + share_plus: + dependency: "direct main" + description: + path: "packages/share_plus/share_plus" + ref: "br_share_plus-v10.1.1_ohos" + resolved-ref: "46cf97a5decaa48d8b95baee41f9f83d25a313ad" + url: "https://gitcode.com/openharmony-sig/flutter_plus_plugins.git" + source: git + version: "10.1.1" + share_plus_platform_interface: + dependency: transitive + description: + path: "packages/share_plus/share_plus_platform_interface" + ref: "46cf97a5decaa48d8b95baee41f9f83d25a313ad" + resolved-ref: "46cf97a5decaa48d8b95baee41f9f83d25a313ad" + url: "https://gitcode.com/openharmony-sig/flutter_plus_plugins.git" + source: git + version: "5.0.1" + shared_preferences: + dependency: "direct main" + description: + path: "packages/shared_preferences/shared_preferences" + ref: HEAD + resolved-ref: a7dd1d3a77d66233629742a301eaf7ec32e50023 + url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" + source: git + version: "2.2.0" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + shared_preferences_ohos: + dependency: transitive + description: + path: "packages/shared_preferences/shared_preferences_ohos" + ref: HEAD + resolved-ref: a7dd1d3a77d66233629742a301eaf7ec32e50023 + url: "https://gitcode.com/openharmony-tpc/flutter_packages.git" + source: git + version: "2.2.0" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.7.6" + timezone: + dependency: transitive + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.9.4" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.4.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.2.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.5" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.5.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.flutter-io.cn" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.flutter-io.cn" + source: hosted + version: "15.0.2" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.15.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.5" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.2 <4.0.0" + flutter: ">=3.35.6" diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..b627242 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility 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:mom_kitchen/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} 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..584f18a --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + mom_kitchen + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..ec69f64 --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "mom_kitchen", + "short_name": "mom_kitchen", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "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" + } + ] +} diff --git a/windows/.gitignore b/windows/.gitignore new file mode 100644 index 0000000..d492d0d --- /dev/null +++ b/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt new file mode 100644 index 0000000..62b3ca3 --- /dev/null +++ b/windows/CMakeLists.txt @@ -0,0 +1,108 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(mom_kitchen LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "mom_kitchen") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt new file mode 100644 index 0000000..903f489 --- /dev/null +++ b/windows/flutter/CMakeLists.txt @@ -0,0 +1,109 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..c9a23e9 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,23 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SharePlusWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..dc139d8 --- /dev/null +++ b/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake new file mode 100644 index 0000000..371949c --- /dev/null +++ b/windows/flutter/generated_plugins.cmake @@ -0,0 +1,28 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus + permission_handler_windows + share_plus + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt new file mode 100644 index 0000000..394917c --- /dev/null +++ b/windows/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc new file mode 100644 index 0000000..7b60569 --- /dev/null +++ b/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "mom_kitchen" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "mom_kitchen" "\0" + VALUE "LegalCopyright", "Copyright (C) 2026 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "mom_kitchen.exe" "\0" + VALUE "ProductName", "mom_kitchen" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp new file mode 100644 index 0000000..955ee30 --- /dev/null +++ b/windows/runner/flutter_window.cpp @@ -0,0 +1,71 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h new file mode 100644 index 0000000..6da0652 --- /dev/null +++ b/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp new file mode 100644 index 0000000..061d9f7 --- /dev/null +++ b/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"mom_kitchen", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/windows/runner/resource.h b/windows/runner/resource.h new file mode 100644 index 0000000..66a65d1 --- /dev/null +++ b/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000..c04e20c Binary files /dev/null and b/windows/runner/resources/app_icon.ico differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest new file mode 100644 index 0000000..153653e --- /dev/null +++ b/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp new file mode 100644 index 0000000..3a0b465 --- /dev/null +++ b/windows/runner/utils.cpp @@ -0,0 +1,65 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/windows/runner/utils.h b/windows/runner/utils.h new file mode 100644 index 0000000..3879d54 --- /dev/null +++ b/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp new file mode 100644 index 0000000..60608d0 --- /dev/null +++ b/windows/runner/win32_window.cpp @@ -0,0 +1,288 @@ +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h new file mode 100644 index 0000000..e901dde --- /dev/null +++ b/windows/runner/win32_window.h @@ -0,0 +1,102 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_