feat: initial commit - Habo habit tracking app

- Complete MVP with Repository Pattern, SQLite storage
- Provider + ChangeNotifier state management
- Navigation 2.0 with deep link support
- Habit CRUD with twoDayRule, notifications, categories
- Backup/Restore via JSON
- Statistics with streak tracking
- Material You theme support
- Biometric lock support
- Desktop widget support
- 27 languages i18n structure
- Comprehensive test suite (87/89 passing)
This commit is contained in:
2026-04-13 15:02:30 +00:00
commit aa69f2a91e
212 changed files with 16694 additions and 0 deletions

File diff suppressed because one or more lines are too long

49
.gitignore vendored Normal file
View File

@@ -0,0 +1,49 @@
# Flutter/Dart
.dart_tool/
.packages
.pub/
build/
*.iml
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Local config
*.local
.env
# Coverage
coverage/
# Generated (but we keep l10n and generated code)
# lib/generated/
# linux/flutter/generated_*.h
# linux/flutter/generated_plugin_registrant.cc
# Android
android/.gradle
android/local.properties
android/*/build/
android/build/
# iOS
ios/.symlinks/
ios/Pods/
ios/Flutter/Generated.xcconfig
ios/Flutter/ephemeral/
ios/Flutter/Flutter.framework/
# Linux
linux/flutter/ephemeral/
# macOS
macos/Flutter/ephemeral/
macos/Flutter/GeneratedPluginRegistrant.swift

14
android/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -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.habo.habo"
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.habo.habo"
// 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 = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="habo"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.habo.habo
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View File

@@ -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<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -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

View File

@@ -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")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,93 @@
Copyright 2014 The Nunito Project Authors (https://github.com/googlefonts/nunito)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,3 @@
<svg width="900" height="900" viewBox="0 0 900 900" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M363 300C328.206 300 300 328.206 300 363V537C300 571.794 328.206 600 363 600H537C571.794 600 600 571.794 600 537V363C600 328.206 571.794 300 537 300H363ZM372.277 450L417.48 495.202L532.273 380.517L547.558 395.801L417.48 525.879L356.885 465.284L372.277 450Z" fill="#09BF2F"/>
</svg>

After

Width:  |  Height:  |  Size: 431 B

BIN
assets/images/app_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
assets/images/ios_icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,26 @@
<svg width="206" height="185" viewBox="0 0 206 185" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M105.499 0.403167L98.7962 2.13279L16.2154 23.4421L9.51251 25.1717C6.25901 26.0152 3.47273 28.1156 1.76494 31.0121C0.0571475 33.9086 -0.432678 37.3647 0.402924 40.6221L29.4681 153.401C30.311 156.657 32.4101 159.445 35.3047 161.154C38.1994 162.862 41.6533 163.353 44.9087 162.516L44.926 162.512L140.878 137.752L140.895 137.748C144.149 136.904 146.935 134.804 148.643 131.908C150.351 129.011 150.84 125.555 150.005 122.298L120.94 9.51854C120.097 6.26299 117.998 3.47494 115.103 1.76607C112.208 0.0571904 108.754 -0.432954 105.499 0.403167Z" fill="#F2F2F2"/>
<path d="M106.411 3.93923L98.4715 5.98785L18.3629 26.6592L10.4239 28.7078C8.10704 29.3085 6.12285 30.8042 4.90669 32.8669C3.69052 34.9295 3.3417 37.3907 3.93675 39.7104L33.0019 152.489C33.6022 154.808 35.097 156.793 37.1583 158.01C39.2197 159.227 41.6793 159.576 43.9975 158.981L44.0149 158.976L139.967 134.216L139.984 134.212C142.301 133.611 144.285 132.116 145.501 130.053C146.717 127.99 147.066 125.529 146.471 123.209L117.406 10.4305C116.806 8.11217 115.311 6.12675 113.25 4.90982C111.188 3.69289 108.729 3.34383 106.411 3.93923Z" fill="white"/>
<path d="M100.416 40.46L51.8762 52.9853C51.334 53.1252 50.7585 53.0439 50.2763 52.7592C49.7941 52.4746 49.4447 52.0099 49.3049 51.4674C49.1651 50.9249 49.2464 50.3491 49.5309 49.8666C49.8153 49.3841 50.2797 49.0344 50.8219 48.8945L99.3616 36.3692C99.9036 36.2299 100.479 36.3115 100.96 36.5962C101.442 36.881 101.791 37.3454 101.931 37.8876C102.071 38.4298 101.99 39.0053 101.706 39.4876C101.421 39.97 100.958 40.3197 100.416 40.46Z" fill="#F2F2F2"/>
<path d="M110.627 45.1876L53.6552 59.8887C53.1131 60.0286 52.5376 59.9473 52.0554 59.6626C51.5732 59.3779 51.2238 58.9132 51.084 58.3707C50.9442 57.8283 51.0254 57.2524 51.3099 56.7699C51.5944 56.2874 52.0588 55.9377 52.601 55.7978L109.573 41.0967C110.115 40.9568 110.69 41.0381 111.173 41.3228C111.655 41.6075 112.004 42.0722 112.144 42.6147C112.284 43.1571 112.203 43.733 111.918 44.2155C111.634 44.698 111.169 45.0477 110.627 45.1876Z" fill="#F2F2F2"/>
<path d="M108.389 71.3972L59.8492 83.9225C59.5807 83.9917 59.3013 84.0074 59.0268 83.9686C58.7523 83.9298 58.4881 83.8373 58.2494 83.6963C58.0106 83.5554 57.8019 83.3688 57.6352 83.1471C57.4686 82.9254 57.3471 82.6731 57.2779 82.4045C57.2087 82.1359 57.193 81.8563 57.2318 81.5816C57.2705 81.3069 57.363 81.0426 57.5039 80.8037C57.6447 80.5648 57.8313 80.3559 58.0528 80.1892C58.2743 80.0224 58.5265 79.9009 58.7949 79.8316L107.335 67.3063C107.603 67.237 107.883 67.2213 108.157 67.2601C108.432 67.2989 108.696 67.3915 108.935 67.5324C109.173 67.6734 109.382 67.86 109.549 68.0817C109.715 68.3033 109.837 68.5556 109.906 68.8243C109.975 69.0929 109.991 69.3725 109.952 69.6472C109.913 69.9218 109.821 70.1862 109.68 70.4251C109.539 70.664 109.353 70.8728 109.131 71.0396C108.91 71.2064 108.657 71.3279 108.389 71.3972Z" fill="#F2F2F2"/>
<path d="M118.6 76.1246L61.6283 90.8257C61.3599 90.895 61.0804 90.9107 60.8059 90.8719C60.5314 90.8331 60.2672 90.7406 60.0285 90.5996C59.7897 90.4587 59.581 90.272 59.4143 90.0504C59.2477 89.8287 59.1262 89.5764 59.057 89.3078C58.9878 89.0392 58.9721 88.7595 59.0109 88.4849C59.0497 88.2102 59.1421 87.9459 59.283 87.7069C59.4239 87.468 59.6104 87.2592 59.8319 87.0924C60.0534 86.9256 60.3056 86.8041 60.574 86.7349L117.546 72.0337C118.088 71.8938 118.663 71.9752 119.146 72.2598C119.628 72.5445 119.977 73.0092 120.117 73.5517C120.257 74.0942 120.176 74.67 119.891 75.1525C119.607 75.635 119.142 75.9847 118.6 76.1246Z" fill="#F2F2F2"/>
<path d="M116.362 102.334L67.8222 114.859C67.2802 114.999 66.7051 114.917 66.2232 114.633C65.7413 114.348 65.3921 113.883 65.2524 113.341C65.1126 112.799 65.1938 112.223 65.478 111.741C65.7622 111.259 66.2262 110.909 66.768 110.769L115.308 98.2433C115.85 98.1034 116.425 98.1848 116.908 98.4694C117.39 98.7541 117.739 99.2188 117.879 99.7613C118.019 100.304 117.938 100.88 117.653 101.362C117.369 101.845 116.904 102.194 116.362 102.334Z" fill="#F2F2F2"/>
<path d="M126.573 107.062L69.6013 121.763C69.3328 121.832 69.0532 121.848 68.7785 121.81C68.5038 121.771 68.2395 121.678 68.0005 121.538C67.7615 121.397 67.5527 121.21 67.3859 120.988C67.219 120.766 67.0975 120.514 67.0282 120.245C66.959 119.976 66.9433 119.697 66.9822 119.422C67.021 119.147 67.1136 118.883 67.2547 118.644C67.3957 118.405 67.5825 118.196 67.8042 118.029C68.026 117.862 68.2784 117.741 68.5471 117.672L125.519 102.971C126.061 102.831 126.637 102.912 127.119 103.197C127.601 103.482 127.95 103.946 128.09 104.489C128.23 105.031 128.149 105.607 127.864 106.09C127.58 106.572 127.115 106.922 126.573 107.062Z" fill="#F2F2F2"/>
<path d="M43.5897 65.8282L26.412 70.2608C26.152 70.3275 25.8762 70.2884 25.6451 70.1519C25.4139 70.0155 25.2463 69.7929 25.179 69.5329L21.2359 54.2327C21.1692 53.9726 21.2083 53.6966 21.3446 53.4653C21.481 53.234 21.7035 53.0663 21.9633 52.999L39.1411 48.5664C39.401 48.4996 39.6768 48.5388 39.9079 48.6752C40.1391 48.8117 40.3067 49.0343 40.374 49.2942L44.3171 64.5945C44.3839 64.8546 44.3447 65.1305 44.2084 65.3618C44.072 65.5931 43.8495 65.7608 43.5897 65.8282Z" fill="#E6E6E6"/>
<path d="M51.5627 96.7653L34.385 101.198C34.125 101.265 33.8492 101.226 33.6181 101.089C33.387 100.953 33.2194 100.73 33.1521 100.47L29.2089 85.1698C29.1422 84.9097 29.1813 84.6337 29.3177 84.4025C29.454 84.1712 29.6765 84.0035 29.9363 83.9361L47.1141 79.5035C47.374 79.4368 47.6498 79.4759 47.8809 79.6123C48.1121 79.7488 48.2797 79.9714 48.347 80.2314L52.2901 95.5316C52.3569 95.7917 52.3177 96.0677 52.1814 96.299C52.045 96.5302 51.8225 96.6979 51.5627 96.7653Z" fill="#E6E6E6"/>
<path d="M59.5358 127.702L42.358 132.135C42.0981 132.202 41.8223 132.163 41.5911 132.026C41.36 131.89 41.1924 131.667 41.1251 131.407L37.1819 116.107C37.1152 115.847 37.1543 115.571 37.2907 115.339C37.4271 115.108 37.6495 114.94 37.9093 114.873L55.0871 110.441C55.347 110.374 55.6228 110.413 55.854 110.549C56.0851 110.686 56.2527 110.908 56.32 111.168L60.2632 126.469C60.3299 126.729 60.2908 127.005 60.1544 127.236C60.0181 127.467 59.7956 127.635 59.5358 127.702Z" fill="#E6E6E6"/>
<path d="M172.445 28.9913H73.3182C69.9573 28.9951 66.7351 30.3328 64.3586 32.7108C61.9821 35.0888 60.6453 38.313 60.6415 41.676V158.145C60.6453 161.508 61.9821 164.732 64.3586 167.11C66.7351 169.488 69.9573 170.826 73.3182 170.829H172.445C175.806 170.826 179.028 169.488 181.405 167.11C183.781 164.732 185.118 161.508 185.122 158.145V41.676C185.118 38.313 183.781 35.0888 181.405 32.7108C179.028 30.3328 175.806 28.9951 172.445 28.9913Z" fill="#E6E6E6"/>
<path d="M172.445 32.6431H73.3182C70.9248 32.6458 68.6302 33.5983 66.9379 35.2918C65.2455 36.9852 64.2935 39.2812 64.2908 41.6761V158.145C64.2935 160.54 65.2455 162.836 66.9379 164.529C68.6302 166.223 70.9248 167.175 73.3182 167.178H172.445C174.839 167.175 177.133 166.223 178.826 164.529C180.518 162.836 181.47 160.54 181.473 158.145V41.6761C181.47 39.2813 180.518 36.9852 178.826 35.2918C177.133 33.5983 174.839 32.6458 172.445 32.6431Z" fill="white"/>
<path d="M182.859 185C195.639 185 206 174.633 206 161.845C206 149.056 195.639 138.689 182.859 138.689C170.079 138.689 159.718 149.056 159.718 161.845C159.718 174.633 170.079 185 182.859 185Z" fill="#09BF30"/>
<path d="M193.926 158.824H185.878V150.77C185.878 149.969 185.559 149.201 184.993 148.635C184.427 148.068 183.66 147.75 182.859 147.75C182.059 147.75 181.291 148.068 180.725 148.635C180.159 149.201 179.841 149.969 179.841 150.77V158.824H171.792C170.991 158.824 170.223 159.143 169.657 159.709C169.091 160.275 168.773 161.044 168.773 161.845C168.773 162.646 169.091 163.414 169.657 163.98C170.223 164.547 170.991 164.865 171.792 164.865H179.841V172.919C179.841 173.72 180.159 174.488 180.725 175.055C181.291 175.621 182.059 175.939 182.859 175.939C183.66 175.939 184.427 175.621 184.993 175.055C185.559 174.488 185.878 173.72 185.878 172.919V164.865H193.926C194.727 164.865 195.495 164.547 196.061 163.98C196.627 163.414 196.945 162.646 196.945 161.845C196.945 161.044 196.627 160.275 196.061 159.709C195.495 159.143 194.727 158.824 193.926 158.824Z" fill="white"/>
<path d="M157.526 96.6423H107.399C107.121 96.6426 106.847 96.5882 106.59 96.4822C106.334 96.3762 106.101 96.2206 105.904 96.0244C105.708 95.8283 105.552 95.5953 105.446 95.3389C105.34 95.0824 105.285 94.8075 105.285 94.5299C105.285 94.2523 105.34 93.9775 105.446 93.721C105.552 93.4646 105.708 93.2316 105.904 93.0355C106.101 92.8393 106.334 92.6837 106.59 92.5777C106.847 92.4717 107.121 92.4173 107.399 92.4176H157.526C158.086 92.4182 158.622 92.6411 159.018 93.0371C159.413 93.4332 159.635 93.9701 159.635 94.5299C159.635 95.0898 159.413 95.6267 159.018 96.0228C158.622 96.4188 158.086 96.6417 157.526 96.6423Z" fill="#E6E6E6"/>
<path d="M166.234 103.771H107.399C107.121 103.772 106.847 103.717 106.59 103.611C106.334 103.505 106.101 103.35 105.904 103.154C105.708 102.957 105.552 102.724 105.446 102.468C105.34 102.212 105.285 101.937 105.285 101.659C105.285 101.381 105.34 101.107 105.446 100.85C105.552 100.594 105.708 100.361 105.904 100.165C106.101 99.9684 106.334 99.8129 106.59 99.7069C106.847 99.6008 107.121 99.5464 107.399 99.5468H166.234C166.794 99.5468 167.331 99.7693 167.727 100.165C168.123 100.562 168.345 101.099 168.345 101.659C168.345 102.219 168.123 102.757 167.727 103.153C167.331 103.549 166.794 103.771 166.234 103.771Z" fill="#E6E6E6"/>
<path d="M157.526 128.592H107.399C107.121 128.592 106.847 128.537 106.59 128.431C106.334 128.325 106.101 128.17 105.904 127.974C105.708 127.777 105.552 127.545 105.446 127.288C105.34 127.032 105.285 126.757 105.285 126.479C105.285 126.202 105.34 125.927 105.446 125.67C105.552 125.414 105.708 125.181 105.904 124.985C106.101 124.788 106.334 124.633 106.59 124.527C106.847 124.421 107.121 124.367 107.399 124.367H157.526C158.086 124.367 158.623 124.589 159.019 124.986C159.415 125.382 159.637 125.919 159.637 126.479C159.637 127.039 159.415 127.577 159.019 127.973C158.623 128.369 158.086 128.592 157.526 128.592Z" fill="#E6E6E6"/>
<path d="M166.234 135.721H107.399C107.121 135.721 106.847 135.666 106.59 135.56C106.334 135.454 106.101 135.299 105.904 135.103C105.708 134.907 105.552 134.674 105.446 134.417C105.34 134.161 105.285 133.886 105.285 133.608C105.285 133.331 105.34 133.056 105.446 132.799C105.552 132.543 105.708 132.31 105.904 132.114C106.101 131.918 106.334 131.762 106.59 131.656C106.847 131.55 107.121 131.496 107.399 131.496H166.234C166.512 131.496 166.787 131.55 167.043 131.656C167.299 131.762 167.532 131.918 167.729 132.114C167.925 132.31 168.081 132.543 168.187 132.799C168.293 133.056 168.348 133.331 168.348 133.608C168.348 133.886 168.293 134.161 168.187 134.417C168.081 134.674 167.925 134.907 167.729 135.103C167.532 135.299 167.299 135.454 167.043 135.56C166.787 135.666 166.512 135.721 166.234 135.721Z" fill="#E6E6E6"/>
<path d="M96.1698 107.008H78.43C78.1617 107.007 77.9044 106.901 77.7146 106.711C77.5249 106.521 77.4181 106.263 77.4178 105.995V90.1941C77.4181 89.9256 77.5249 89.6681 77.7146 89.4783C77.9044 89.2884 78.1617 89.1816 78.43 89.1813H96.1698C96.4382 89.1816 96.6955 89.2884 96.8852 89.4783C97.075 89.6681 97.1817 89.9256 97.182 90.1941V105.995C97.1817 106.263 97.075 106.521 96.8852 106.711C96.6955 106.901 96.4382 107.007 96.1698 107.008Z" fill="#E6E6E6"/>
<path d="M96.1698 138.957H78.43C78.1617 138.957 77.9044 138.85 77.7146 138.66C77.5249 138.47 77.4181 138.213 77.4178 137.944V122.143C77.4181 121.875 77.5249 121.617 77.7146 121.427C77.9044 121.237 78.1617 121.131 78.43 121.13H96.1698C96.4382 121.131 96.6955 121.237 96.8852 121.427C97.075 121.617 97.1817 121.875 97.182 122.143V137.944C97.1817 138.213 97.075 138.47 96.8852 138.66C96.6955 138.85 96.4382 138.957 96.1698 138.957Z" fill="#E6E6E6"/>
<path d="M157.582 61.0765H120.625C120.065 61.0765 119.528 60.8539 119.132 60.4578C118.736 60.0617 118.514 59.5244 118.514 58.9641C118.514 58.4039 118.736 57.8666 119.132 57.4705C119.528 57.0744 120.065 56.8518 120.625 56.8518H157.582C158.142 56.8518 158.679 57.0744 159.075 57.4705C159.471 57.8666 159.693 58.4039 159.693 58.9641C159.693 59.5244 159.471 60.0617 159.075 60.4578C158.679 60.8539 158.142 61.0765 157.582 61.0765Z" fill="#CCCCCC"/>
<path d="M166.29 68.2056H120.625C120.348 68.2056 120.073 68.151 119.817 68.0448C119.561 67.9387 119.328 67.7831 119.132 67.5869C118.936 67.3908 118.781 67.1579 118.675 66.9017C118.569 66.6454 118.514 66.3707 118.514 66.0933C118.514 65.8159 118.569 65.5412 118.675 65.2849C118.781 65.0287 118.936 64.7958 119.132 64.5996C119.328 64.4035 119.561 64.2479 119.817 64.1417C120.073 64.0356 120.348 63.981 120.625 63.981H166.29C166.85 63.981 167.387 64.2035 167.783 64.5996C168.179 64.9958 168.401 65.5331 168.401 66.0933C168.401 66.6535 168.179 67.1908 167.783 67.5869C167.387 67.9831 166.85 68.2056 166.29 68.2056Z" fill="#CCCCCC"/>
<path d="M112.446 76.8453H78.3744C78.106 76.845 77.8488 76.7382 77.659 76.5484C77.4692 76.3585 77.3625 76.101 77.3622 75.8325V49.225C77.3625 48.9565 77.4692 48.699 77.659 48.5091C77.8488 48.3193 78.106 48.2125 78.3744 48.2122H112.446C112.714 48.2125 112.971 48.3193 113.161 48.5091C113.351 48.699 113.458 48.9565 113.458 49.225V75.8325C113.458 76.101 113.351 76.3585 113.161 76.5484C112.971 76.7382 112.714 76.845 112.446 76.8453Z" fill="#09BF30"/>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,48 @@
<svg width="247" height="198" viewBox="0 0 247 198" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_626_42)">
<path d="M245.17 114.774H56.5757C56.0906 114.774 55.6255 114.581 55.2824 114.24C54.9394 113.898 54.7464 113.434 54.7459 112.951V1.82374C54.7464 1.34022 54.9394 0.876649 55.2824 0.534745C55.6255 0.192841 56.0906 0.000527838 56.5757 0H245.17C245.655 0.000527839 246.12 0.192842 246.463 0.534746C246.806 0.87665 246.999 1.34022 247 1.82374V112.951C246.999 113.434 246.806 113.898 246.463 114.24C246.12 114.581 245.655 114.774 245.17 114.774ZM56.5757 0.729497C56.2847 0.729814 56.0056 0.845202 55.7998 1.05034C55.5939 1.25549 55.4782 1.53363 55.4778 1.82374V112.951C55.4782 113.241 55.5939 113.519 55.7998 113.724C56.0056 113.929 56.2847 114.044 56.5757 114.045H245.17C245.461 114.044 245.74 113.929 245.946 113.724C246.152 113.519 246.268 113.241 246.268 112.951V1.82374C246.268 1.53363 246.152 1.25549 245.946 1.05035C245.74 0.845203 245.461 0.729814 245.17 0.729497H56.5757Z" fill="#E6E6E6"/>
<path d="M132.694 8.8752C132.313 8.88155 131.951 9.03668 131.684 9.30714C131.417 9.5776 131.267 9.94174 131.267 10.3211C131.267 10.7004 131.417 11.0645 131.684 11.335C131.951 11.6055 132.313 11.7606 132.694 11.7669H169.067C169.451 11.7726 169.822 11.6263 170.098 11.3601C170.374 11.0939 170.533 10.7296 170.54 10.3469C170.546 9.9642 170.401 9.59442 170.135 9.31857C169.868 9.04272 169.503 8.88331 169.119 8.87527L169.115 8.8752C169.099 8.87493 169.083 8.87493 169.067 8.8752H132.694Z" fill="#B0B0B0"/>
<path d="M177.316 8.47261C177.262 8.52596 177.232 8.59832 177.232 8.67378C177.232 8.74923 177.262 8.8216 177.316 8.87496L177.316 8.87498L178.541 10.0964H175.52C175.444 10.0966 175.372 10.1267 175.318 10.18C175.265 10.2333 175.235 10.3056 175.235 10.3809C175.235 10.4562 175.265 10.5284 175.318 10.5818C175.372 10.6351 175.444 10.6652 175.52 10.6654H178.541L177.316 11.8868C177.289 11.9132 177.268 11.9446 177.254 11.9791C177.24 12.0136 177.232 12.0506 177.232 12.088C177.232 12.1253 177.24 12.1623 177.254 12.1969C177.268 12.2314 177.289 12.2628 177.316 12.2892C177.342 12.3156 177.374 12.3365 177.409 12.3508C177.443 12.3651 177.48 12.3725 177.518 12.3725C177.555 12.3725 177.592 12.3652 177.627 12.3509C177.662 12.3366 177.693 12.3156 177.72 12.2892L179.432 10.5821C179.486 10.5287 179.516 10.4564 179.516 10.3809C179.516 10.3054 179.486 10.2331 179.432 10.1797L177.72 8.47261C177.693 8.44619 177.662 8.42524 177.627 8.41094C177.592 8.39664 177.555 8.38928 177.518 8.38928C177.48 8.38928 177.443 8.39664 177.409 8.41094C177.374 8.42524 177.343 8.44619 177.316 8.47261L177.316 8.47261Z" fill="#E6E6E6"/>
<path d="M124.43 8.47262C124.483 8.52597 124.513 8.59833 124.513 8.67379C124.513 8.74924 124.483 8.82161 124.43 8.87497L124.43 8.87499L123.204 10.0964H126.226C126.264 10.0962 126.301 10.1035 126.336 10.1177C126.371 10.1319 126.402 10.1528 126.429 10.1792C126.455 10.2056 126.476 10.237 126.491 10.2715C126.505 10.3061 126.513 10.3431 126.513 10.3806C126.513 10.418 126.506 10.4551 126.491 10.4896C126.477 10.5242 126.456 10.5557 126.429 10.5821C126.403 10.6086 126.371 10.6296 126.336 10.6439C126.302 10.6581 126.264 10.6655 126.227 10.6654H123.204L124.43 11.8868C124.456 11.9132 124.477 11.9446 124.492 11.9791C124.506 12.0136 124.513 12.0506 124.513 12.088C124.513 12.1254 124.506 12.1624 124.492 12.1969C124.477 12.2314 124.456 12.2628 124.43 12.2892C124.403 12.3156 124.372 12.3366 124.337 12.3509C124.303 12.3652 124.265 12.3725 124.228 12.3725C124.191 12.3725 124.153 12.3652 124.119 12.3509C124.084 12.3366 124.053 12.3156 124.026 12.2892L122.313 10.5821C122.26 10.5287 122.23 10.4564 122.23 10.3809C122.23 10.3054 122.26 10.2331 122.313 10.1797L124.026 8.47262C124.08 8.41926 124.152 8.38929 124.228 8.38928C124.304 8.38928 124.376 8.41925 124.43 8.4726L124.43 8.47262Z" fill="#E6E6E6"/>
<path d="M54.6367 120.896C52.9829 120.896 51.3663 120.407 49.9912 119.491C48.6161 118.575 47.5444 117.274 46.9115 115.751C46.2786 114.228 46.1131 112.552 46.4357 110.936C46.7583 109.319 47.5547 107.834 48.7241 106.669C49.8935 105.503 51.3834 104.709 53.0055 104.388C54.6275 104.066 56.3087 104.231 57.8366 104.862C59.3645 105.493 60.6704 106.561 61.5892 107.932C62.508 109.302 62.9984 110.913 62.9984 112.562C62.9984 114.772 62.1175 116.892 60.5494 118.455C58.9812 120.018 56.8544 120.896 54.6367 120.896Z" fill="#09BF30"/>
<path d="M57.936 111.74H55.4616V109.273C55.4616 109.055 55.3747 108.846 55.22 108.692C55.0653 108.538 54.8555 108.451 54.6367 108.451C54.418 108.451 54.2082 108.538 54.0535 108.692C53.8988 108.846 53.8119 109.055 53.8119 109.273V111.74H51.3374C51.1186 111.74 50.9088 111.826 50.7542 111.98C50.5995 112.134 50.5126 112.344 50.5126 112.562C50.5126 112.78 50.5995 112.989 50.7542 113.143C50.9088 113.297 51.1186 113.384 51.3374 113.384H53.8119V115.85C53.8119 116.068 53.8988 116.277 54.0535 116.431C54.2082 116.585 54.418 116.672 54.6367 116.672C54.8555 116.672 55.0653 116.585 55.22 116.431C55.3747 116.277 55.4616 116.068 55.4616 115.85V113.384H57.936C58.1548 113.384 58.3646 113.297 58.5193 113.143C58.674 112.989 58.7609 112.78 58.7609 112.562C58.7609 112.344 58.674 112.134 58.5193 111.98C58.3646 111.826 58.1548 111.74 57.936 111.74Z" fill="white"/>
<path d="M95.1242 23.6788H69.9945V40.7004H95.1242V23.6788Z" fill="#F1F1F1"/>
<path d="M129.281 23.6788H104.151V40.7004H129.281V23.6788Z" fill="#F1F1F1"/>
<path d="M163.438 23.6788H138.308V40.7004H163.438V23.6788Z" fill="#F1F1F1"/>
<path d="M197.595 23.6788H172.465V40.7004H197.595V23.6788Z" fill="#F1F1F1"/>
<path d="M231.751 23.6788H206.622V40.7004H231.751V23.6788Z" fill="#F1F1F1"/>
<path d="M95.2163 53.5883H70.0866V70.6099H95.2163V53.5883Z" fill="#F1F1F1"/>
<path d="M129.373 53.5883H104.243V70.6099H129.373V53.5883Z" fill="#F1F1F1"/>
<path d="M163.53 53.5883H138.4V70.6099H163.53V53.5883Z" fill="#F1F1F1"/>
<path d="M197.687 53.5883H172.557V70.6099H197.687V53.5883Z" fill="#F1F1F1"/>
<path d="M231.844 53.5883H206.714V70.6099H231.844V53.5883Z" fill="#F1F1F1"/>
<path d="M95.3084 83.4977H70.1788V100.519H95.3084V83.4977Z" fill="#F1F1F1"/>
<path d="M129.465 83.4977H104.336V100.519H129.465V83.4977Z" fill="#F1F1F1"/>
<path d="M163.622 83.4977H138.492V100.519H163.622V83.4977Z" fill="#F1F1F1"/>
<path d="M197.779 83.4977H172.649V100.519H197.779V83.4977Z" fill="#F1F1F1"/>
<path d="M231.936 83.4977H206.806V100.519H231.936V83.4977Z" fill="#F1F1F1"/>
<path d="M143.524 32.1897C145.275 32.1897 146.695 30.7744 146.695 29.0286C146.695 27.2827 145.275 25.8674 143.524 25.8674C141.772 25.8674 140.352 27.2827 140.352 29.0286C140.352 30.7744 141.772 32.1897 143.524 32.1897Z" fill="#09BF30"/>
<path d="M108.879 62.099C110.631 62.099 112.051 60.6837 112.051 58.9379C112.051 57.192 110.631 55.7767 108.879 55.7767C107.127 55.7767 105.707 57.192 105.707 58.9379C105.707 60.6837 107.127 62.099 108.879 62.099Z" fill="#09BF30"/>
<path d="M143.524 92.0085C145.275 92.0085 146.695 90.5932 146.695 88.8473C146.695 87.1015 145.275 85.6862 143.524 85.6862C141.772 85.6862 140.352 87.1015 140.352 88.8473C140.352 90.5932 141.772 92.0085 143.524 92.0085Z" fill="#09BF30"/>
<path d="M178.413 62.099C180.164 62.099 181.584 60.6837 181.584 58.9379C181.584 57.192 180.164 55.7767 178.413 55.7767C176.661 55.7767 175.241 57.192 175.241 58.9379C175.241 60.6837 176.661 62.099 178.413 62.099Z" fill="#3F3D56"/>
<path d="M212.325 62.099C214.077 62.099 215.497 60.6837 215.497 58.9379C215.497 57.192 214.077 55.7767 212.325 55.7767C210.574 55.7767 209.154 57.192 209.154 58.9379C209.154 60.6837 210.574 62.099 212.325 62.099Z" fill="#3F3D56"/>
<path d="M108.879 92.0085C110.631 92.0085 112.051 90.5932 112.051 88.8473C112.051 87.1015 110.631 85.6862 108.879 85.6862C107.127 85.6862 105.707 87.1015 105.707 88.8473C105.707 90.5932 107.127 92.0085 108.879 92.0085Z" fill="#3F3D56"/>
<path d="M25.7192 119.04C28.9727 119.04 31.6102 116.412 31.6102 113.169C31.6102 109.926 28.9727 107.297 25.7192 107.297C22.4657 107.297 19.8282 109.926 19.8282 113.169C19.8282 116.412 22.4657 119.04 25.7192 119.04Z" fill="#FFB8B8"/>
<path d="M23.5923 147.911C23.5181 147.911 23.4436 147.908 23.3687 147.901C22.9982 147.87 22.6384 147.762 22.3129 147.583C21.9875 147.404 21.7036 147.158 21.48 146.862C21.2563 146.566 21.098 146.226 21.0153 145.865C20.9327 145.504 20.9275 145.129 21.0002 144.766C21.0174 144.681 21.039 144.598 21.065 144.516L12.3704 134.267C12.0871 133.93 11.9202 133.511 11.8945 133.073C11.8689 132.634 11.9857 132.199 12.2278 131.832L12.2436 131.813L20.0697 124.122C20.4971 123.702 21.074 123.469 21.6737 123.474C22.2734 123.478 22.8468 123.72 23.2678 124.145C23.6889 124.571 23.9232 125.146 23.9192 125.743C23.9151 126.341 23.6732 126.913 23.2464 127.333C23.2366 127.342 23.2267 127.352 23.2167 127.362L17.5485 132.732L23.7538 142.658C23.8713 142.665 23.9883 142.679 24.1038 142.701C24.7457 142.829 25.317 143.19 25.7069 143.714C26.0969 144.238 26.2775 144.888 26.2137 145.537C26.15 146.186 25.8464 146.789 25.362 147.227C24.8775 147.666 24.2468 147.91 23.5923 147.911Z" fill="#FFB8B8"/>
<path d="M35.4215 194.884H38.3622L39.761 183.579L35.421 183.58L35.4215 194.884Z" fill="#FFB8B8"/>
<path d="M34.6716 193.927L40.4625 193.927H40.4627C40.9474 193.927 41.4273 194.022 41.875 194.207C42.3227 194.391 42.7296 194.662 43.0722 195.004C43.4149 195.345 43.6868 195.751 43.8722 196.197C44.0577 196.643 44.1532 197.122 44.1532 197.605V197.724L34.6718 197.725L34.6716 193.927Z" fill="#1A192F"/>
<path d="M6.19458 190.832L8.72113 192.331L15.726 183.331L11.9969 181.119L6.19458 190.832Z" fill="#FFB8B8"/>
<path d="M6.04125 189.627L11.017 192.58L11.0172 192.58C11.8581 193.079 12.4657 193.89 12.7063 194.836C12.9469 195.781 12.8008 196.784 12.3001 197.622L12.3 197.622L12.2386 197.725L4.09192 192.89L6.04125 189.627Z" fill="#1A192F"/>
<path d="M39.056 190.891H35.7633C35.4967 190.891 35.2394 190.793 35.0411 190.616C34.8429 190.438 34.7178 190.193 34.6901 189.929L31.3705 158.829C31.3622 158.751 31.3282 158.677 31.2738 158.62C31.2194 158.563 31.1477 158.525 31.0697 158.513C30.9918 158.5 30.9118 158.514 30.8422 158.551C30.7727 158.588 30.7174 158.648 30.6848 158.719L24.6963 171.936L14.0939 189.166C13.9567 189.387 13.7443 189.552 13.4955 189.629C13.2467 189.707 12.9782 189.692 12.7392 189.589L8.50361 187.731C8.36356 187.67 8.23822 187.579 8.1359 187.466C8.03357 187.353 7.9566 187.219 7.91009 187.073C7.86358 186.928 7.84859 186.775 7.86611 186.623C7.88363 186.472 7.93327 186.326 8.01173 186.195L17.7582 169.929L20.7474 156.225C19.8072 150.282 23.8376 141.446 23.8784 141.357L23.8984 141.314L35 136.989L35.0557 137.057C39.4339 151.377 42.1018 166.788 40.1319 189.905C40.1076 190.174 39.9839 190.423 39.785 190.605C39.5861 190.788 39.3262 190.889 39.056 190.891Z" fill="#1F1F24"/>
<path d="M23.4511 142.762L23.3994 142.654C23.3747 142.602 20.9062 137.413 19.0033 131.867C18.6375 130.794 18.4993 129.658 18.5973 128.529C18.6952 127.401 19.0273 126.305 19.5724 125.311C20.1246 124.307 20.88 123.428 21.7908 122.729C22.7016 122.031 23.748 121.53 24.8636 121.256C26.8003 120.793 28.8394 121.039 30.6092 121.949C32.3791 122.859 33.762 124.373 34.506 126.214C36.0155 129.976 35.5344 133.992 35.0817 137.15L35.0727 137.213L35.0149 137.24L23.4511 142.762Z" fill="#09BF30"/>
<path d="M32.8618 109.852H23.5077V105.788C25.5608 104.975 27.5699 104.284 28.7844 105.788C29.8658 105.788 30.9029 106.216 31.6676 106.979C32.4322 107.741 32.8618 108.774 32.8618 109.852H32.8618Z" fill="#2F2E41"/>
<path d="M22.9898 105.071C17.3977 105.071 15.8325 112.057 15.8325 115.999C15.8325 118.197 16.8298 118.983 18.397 119.249L18.9505 116.307L20.2468 119.375C20.6871 119.378 21.1496 119.369 21.629 119.36L22.0685 118.458L23.0487 119.344C26.9742 119.35 30.147 119.92 30.147 115.999C30.147 112.057 28.7743 105.071 22.9898 105.071Z" fill="#1F1E2A"/>
<path d="M47.8462 112.89C47.8817 112.955 47.9146 113.022 47.9447 113.09C48.0952 113.429 48.1722 113.796 48.1706 114.166C48.1689 114.537 48.0888 114.903 47.9353 115.24C47.7819 115.578 47.5586 115.879 47.2802 116.125C47.0017 116.37 46.6743 116.555 46.3195 116.665C46.2369 116.691 46.1531 116.712 46.0683 116.728L41.2126 129.247C41.0521 129.656 40.7635 130.003 40.3896 130.235C40.0158 130.468 39.5766 130.575 39.1373 130.539L39.1129 130.534L28.5886 127.377C28.0141 127.204 27.5318 126.811 27.2479 126.284C26.964 125.758 26.9017 125.14 27.0747 124.567C27.2477 123.995 27.6418 123.514 28.1704 123.231C28.6989 122.948 29.3186 122.886 29.8932 123.059C29.9063 123.062 29.9194 123.067 29.9324 123.071L37.379 125.453L43.144 115.267C43.0931 115.161 43.0496 115.052 43.014 114.94C42.8187 114.318 42.8626 113.645 43.137 113.053C43.4114 112.46 43.8969 111.991 44.4991 111.735C45.1013 111.48 45.7773 111.457 46.3959 111.67C47.0144 111.883 47.5313 112.318 47.8462 112.89L47.8462 112.89Z" fill="#FFB8B8"/>
<path d="M49.0666 198H0.254231C0.186805 198 0.12214 197.973 0.0744624 197.926C0.0267848 197.878 0 197.814 0 197.747C0 197.679 0.0267848 197.615 0.0744624 197.567C0.12214 197.52 0.186805 197.493 0.254231 197.493H49.0666C49.1341 197.493 49.1987 197.52 49.2464 197.567C49.2941 197.615 49.3209 197.679 49.3209 197.747C49.3209 197.814 49.2941 197.878 49.2464 197.926C49.1987 197.973 49.1341 198 49.0666 198Z" fill="#CCCCCC"/>
</g>
<defs>
<clipPath id="clip0_626_42">
<rect width="247" height="198" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,38 @@
<svg width="221" height="178" viewBox="0 0 221 178" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_626_83)">
<path d="M153.891 173.253L149.8 173.253L147.854 157.45H153.891L153.891 173.253Z" fill="#FFB6B6"/>
<path d="M153.781 171.915L149.414 170.136L149.289 170.084L146.877 171.915C145.516 171.915 144.21 172.457 143.247 173.421C142.284 174.386 141.743 175.693 141.743 177.057V177.224H154.933V171.915H153.781Z" fill="#1A192F"/>
<path d="M190.853 167.926L187.192 169.754L178.411 156.48L183.815 153.782L190.853 167.926Z" fill="#FFB6B6"/>
<path d="M191.192 166.262L190.256 166.728L190.159 166.776L185.465 167.139L185.323 167.149L183.98 169.863C183.376 170.164 182.838 170.581 182.396 171.091C181.953 171.6 181.616 172.192 181.402 172.833C181.188 173.473 181.102 174.149 181.15 174.823C181.197 175.497 181.376 176.154 181.677 176.758L181.751 176.906L193.555 171.016L191.192 166.262Z" fill="#1A192F"/>
<path d="M151.116 88.3116L173.673 89.1024L173.83 88.8736C173.83 88.8736 175.754 93.3701 175.754 97.2243C175.754 98.5058 174.865 106.887 175.317 108.473C175.373 108.67 174.986 109.153 175.113 110.072C175.195 110.667 175.65 110.209 175.754 111.035C175.829 111.628 175.029 111.955 175.113 112.641C175.204 113.39 176.305 115.817 176.404 116.651C177.459 125.524 178.863 138.378 178.863 138.378C178.863 138.378 184.502 153.506 185.695 153.752C187.698 154.165 189.05 157.812 187.94 158.57C187.412 158.931 180.582 161.361 180.582 161.361C180.207 161.429 179.033 160.839 179.781 159.444C179.919 159.187 179.098 159.841 178.961 159.534C178.816 159.207 179.101 157.906 178.935 157.543C177.442 154.285 175.151 149.771 172.979 147.453C169.771 144.027 167.739 139.665 167.18 135.002L162.199 107.34L160.492 115.097L159.079 116.495L159.467 119.755L159.4 122.919L155.872 136.087C155.872 136.087 155.343 160.68 156.193 161.461C156.653 161.884 158.606 167.093 154.91 167.563L148.196 167.044C148.196 167.044 147.291 165.135 147.743 164.455C148.196 163.776 148.169 163.961 147.617 162.849C147.065 161.737 147.291 159.852 147.291 159.852C146.412 150.996 146.065 142.095 146.252 133.197C146.589 118.582 148.628 101.792 148.628 101.45C148.645 101.101 148.581 100.752 148.44 100.431V99.5088L149.307 96.2402L151.116 88.3116Z" fill="#1F1F24"/>
<path d="M182.859 91.0742C187.779 91.0742 191.768 87.0795 191.768 82.1518C191.768 77.2241 187.779 73.2294 182.859 73.2294C177.939 73.2294 173.951 77.2241 173.951 82.1518C173.951 87.0795 177.939 91.0742 182.859 91.0742Z" fill="#09BF30"/>
<path d="M181.644 86.7954C181.49 86.7954 181.338 86.7595 181.201 86.6906C181.063 86.6217 180.944 86.5216 180.851 86.3983L178.421 83.1527C178.343 83.0484 178.286 82.9298 178.254 82.8035C178.221 82.6773 178.214 82.5459 178.233 82.4169C178.251 82.2878 178.295 82.1637 178.361 82.0516C178.427 81.9394 178.515 81.8414 178.619 81.7633C178.723 81.6851 178.842 81.6282 178.968 81.5958C179.094 81.5635 179.225 81.5563 179.354 81.5748C179.483 81.5932 179.607 81.6369 179.719 81.7033C179.831 81.7697 179.928 81.8575 180.006 81.9618L181.596 84.085L185.68 77.9498C185.752 77.8412 185.845 77.7479 185.953 77.6753C186.061 77.6027 186.183 77.5521 186.311 77.5264C186.438 77.5008 186.57 77.5006 186.697 77.5259C186.825 77.5512 186.947 77.6014 187.055 77.6738C187.163 77.7461 187.256 77.8391 187.329 77.9475C187.401 78.0559 187.452 78.1775 187.477 78.3054C187.503 78.4333 187.503 78.5649 187.477 78.6929C187.452 78.8208 187.402 78.9424 187.329 79.0509L182.469 86.3534C182.381 86.4855 182.262 86.5945 182.124 86.6714C181.985 86.7482 181.83 86.7906 181.672 86.7951C181.662 86.7952 181.653 86.7954 181.644 86.7954Z" fill="white"/>
<path d="M187.264 91.5844C187.351 91.2195 187.357 90.84 187.282 90.4726C187.206 90.1052 187.051 89.7589 186.828 89.458C186.604 89.1571 186.317 88.9089 185.988 88.731C185.658 88.553 185.293 88.4496 184.92 88.4281L180.355 72.8514L177.267 76.4406L182.18 91.0251C182.185 91.6566 182.422 92.2641 182.845 92.7325C183.268 93.2009 183.847 93.4977 184.474 93.5667C185.101 93.6356 185.731 93.4719 186.245 93.1065C186.76 92.7412 187.122 92.1996 187.264 91.5844Z" fill="#FFB8B8"/>
<path d="M184.044 83.9852L183.451 81.8076L184.131 80.5068L184.079 80.3559L183.874 79.7713L182.809 78.917L183.191 77.8282L181.844 73.9965L181.514 73.3316L173.782 57.7833L171.929 54.0543L171.499 53.3799L170.162 51.2793L168.334 48.4112C168.043 47.9528 167.665 47.5568 167.22 47.2464C166.775 46.9359 166.273 46.7172 165.743 46.6029C165.572 46.5652 165.398 46.5384 165.223 46.5226L165.098 46.513C164.786 46.4957 164.473 46.5141 164.165 46.5676H164.159C163.616 46.6619 163.098 46.8675 162.639 47.1714C162.277 47.406 161.954 47.6966 161.683 48.0322C161.059 48.8078 160.734 49.7827 160.769 50.7783L160.827 52.3456L160.885 53.7717L163.598 58.6858L163.774 59.007L163.777 59.0166L172.124 74.1442L174.642 78.705L175.023 79.3988L176.717 80.5229L176.264 81.6534L176.396 83.4135L177.836 84.5023L179.923 86.6253L180.007 86.7988L180.648 86.4744L180.709 86.4423L180.853 86.4005L182.838 85.7999L184.044 85.437L184.083 85.4241L184.413 85.3406L184.044 83.9852Z" fill="#E6E6E6"/>
<path d="M218.36 154.696C217.755 153.926 216.934 153.355 216.002 153.057C215.071 152.759 214.071 152.747 213.133 153.023L212.928 153.084L211.087 160.282C210.811 159.209 210.221 156.787 209.849 154.417L209.817 154.221L209.634 154.301C207.71 155.153 205.901 156.245 204.25 157.552C201.325 159.901 198.947 162.861 197.279 166.224C195.612 169.588 194.696 173.274 194.594 177.028L194.591 177.166L194.588 177.253L194.832 177.166L200.062 175.294C202.378 175.498 204.71 175.182 206.888 174.369C209.066 173.555 211.035 172.265 212.652 170.592C214.765 168.36 216.214 165.488 217.616 162.71C218.026 161.901 218.446 161.066 218.876 160.263C219.355 159.389 219.563 158.392 219.471 157.4C219.378 156.408 218.992 155.466 218.36 154.696Z" fill="#F2F2F2"/>
<path d="M84.1414 35.2144H83.5V10.4696H129.888V11.112H84.1414V35.2144Z" fill="#CECECE"/>
<path d="M137.002 17.8448C141.922 17.8448 145.911 13.8501 145.911 8.92242C145.911 3.9947 141.922 0 137.002 0C132.082 0 128.094 3.9947 128.094 8.92242C128.094 13.8501 132.082 17.8448 137.002 17.8448Z" fill="#09BF30"/>
<path d="M135.787 13.5662C135.633 13.5662 135.481 13.5303 135.344 13.4613C135.206 13.3924 135.086 13.2924 134.994 13.1691L132.564 9.92345C132.486 9.81918 132.429 9.70053 132.397 9.57428C132.364 9.44802 132.357 9.31663 132.376 9.1876C132.394 9.05858 132.438 8.93445 132.504 8.82231C132.57 8.71016 132.658 8.61219 132.762 8.53399C132.866 8.4558 132.985 8.3989 133.111 8.36656C133.237 8.33422 133.368 8.32706 133.497 8.3455C133.626 8.36393 133.749 8.40761 133.861 8.47401C133.973 8.54042 134.071 8.62827 134.149 8.73254L135.739 10.8557L139.823 4.72054C139.895 4.61197 139.988 4.51869 140.096 4.44605C140.204 4.37341 140.326 4.32283 140.453 4.2972C140.581 4.27156 140.712 4.27138 140.84 4.29666C140.968 4.32194 141.09 4.37218 141.198 4.44452C141.306 4.51687 141.399 4.60988 141.472 4.71826C141.544 4.82664 141.594 4.94825 141.62 5.07614C141.645 5.20403 141.646 5.3357 141.62 5.46362C141.595 5.59154 141.545 5.71319 141.472 5.82164L136.611 13.1242C136.524 13.2562 136.405 13.3653 136.267 13.4421C136.128 13.5189 135.973 13.5614 135.814 13.5658C135.805 13.566 135.796 13.5662 135.787 13.5662Z" fill="white"/>
<path d="M52.7148 95.2897H52.0735V120.034H98.462V119.392H52.7148V95.2897Z" fill="#CECECE"/>
<path d="M105.576 128.974C110.496 128.974 114.484 124.979 114.484 120.051C114.484 115.123 110.496 111.129 105.576 111.129C100.656 111.129 96.6671 115.123 96.6671 120.051C96.6671 124.979 100.656 128.974 105.576 128.974Z" fill="#09BF30"/>
<path d="M104.36 124.695C104.206 124.695 104.055 124.659 103.917 124.59C103.78 124.521 103.66 124.421 103.568 124.298L101.137 121.052C101.059 120.948 101.002 120.829 100.97 120.703C100.938 120.577 100.931 120.445 100.949 120.316C100.967 120.187 101.011 120.063 101.077 119.951C101.144 119.839 101.231 119.741 101.335 119.663C101.44 119.584 101.558 119.528 101.684 119.495C101.81 119.463 101.941 119.456 102.07 119.474C102.199 119.493 102.323 119.536 102.435 119.603C102.547 119.669 102.645 119.757 102.723 119.861L104.313 121.984L108.397 115.849C108.469 115.741 108.561 115.647 108.67 115.575C108.778 115.502 108.899 115.451 109.027 115.426C109.154 115.4 109.286 115.4 109.414 115.425C109.541 115.451 109.663 115.501 109.771 115.573C109.88 115.646 109.973 115.739 110.045 115.847C110.118 115.955 110.168 116.077 110.193 116.205C110.219 116.333 110.219 116.464 110.194 116.592C110.168 116.72 110.118 116.842 110.046 116.95L105.185 124.253C105.097 124.385 104.979 124.494 104.84 124.571C104.701 124.648 104.546 124.69 104.388 124.694C104.379 124.695 104.369 124.695 104.36 124.695Z" fill="white"/>
<path d="M176.075 96.1997C176.084 96.876 175.976 97.5488 175.754 98.1878L175.706 98.1782C170.011 99.1648 164.93 99.3263 162.58 95.8247C159.514 91.9141 155.296 91.9584 150.591 93.6752L150.033 93.0714L149.084 92.0468L149.956 91.0576L149.084 89.1498L149.812 86.7955L150.742 86.6253L150.491 84.5986L150.1 83.4135L151.248 82.1512L152.223 79.0004L151.521 72.4066C150.56 63.3683 152.422 54.254 156.85 46.3202L159.515 43.9242L160.622 42.9286L162.106 40.1536L163.674 40.3366L167.92 40.828L168.065 41.1749L169.107 43.693L174.597 49.5995L174.35 52.0919L174.241 53.1903L174.161 53.9804L173.782 57.7832L172.24 73.3187L172.124 74.1442L170.883 83.0762C172.9 85.029 174.683 88.7901 175.54 92.2267C175.636 92.6089 175.363 92.9975 175.434 93.3701C175.533 93.8968 175.966 94.3979 176.014 94.8893C176.059 95.3246 176.079 95.7621 176.075 96.1997Z" fill="#E6E6E6"/>
<path d="M165.201 39.1589C169.274 39.1589 172.576 35.8521 172.576 31.7729C172.576 27.6938 169.274 24.387 165.201 24.387C161.129 24.387 157.827 27.6938 157.827 31.7729C157.827 35.8521 161.129 39.1589 165.201 39.1589Z" fill="#FFB6B6"/>
<path d="M174.091 29.758C173.655 28.3952 172.457 27.2421 171.036 27.0914C171.144 26.1863 171.002 25.2689 170.625 24.4391C170.248 23.6094 169.651 22.8991 168.899 22.3856C168.147 21.8721 167.269 21.5751 166.36 21.5269C165.451 21.4787 164.547 21.6811 163.745 22.1122C163.574 21.9071 163.354 21.7496 163.105 21.6548C162.856 21.5601 162.587 21.5313 162.324 21.5713C161.797 21.6582 161.301 21.876 160.88 22.2047C159.836 22.97 159.018 24.0048 158.513 25.1988C158.009 26.3928 157.838 27.7014 158.017 28.9853C158.014 28.334 158.302 29.594 159.128 29.7054C159.816 29.8441 160.472 30.1148 161.057 30.5026C161.643 30.8903 162.296 31.1645 162.983 31.311C163.676 31.4125 163.855 34.1952 163.36 35.9874C163.266 36.3271 163.218 36.7828 163.531 36.9447C163.916 37.144 164.305 36.6428 164.736 36.594C164.929 36.5909 165.116 36.6631 165.257 36.7953C165.398 36.9276 165.482 37.1095 165.492 37.3027C165.492 37.4967 165.446 37.6879 165.358 37.8604C165.27 38.033 165.142 38.182 164.984 38.2949C164.668 38.5176 164.317 38.6876 163.947 38.7984L164.071 38.9752C164.18 39.1772 164.355 39.3351 164.568 39.4218C164.78 39.5085 165.016 39.5185 165.235 39.4501C165.728 40.0459 170.809 38.3917 171.057 37.3043C172.122 36.3439 173.005 35.2003 173.667 33.9277C174.328 32.641 174.48 31.1517 174.091 29.758Z" fill="#1F1E2B"/>
<path d="M180.055 52.2524C179.952 53.1132 179.147 53.7555 178.301 53.9354C177.87 54.0184 177.429 54.026 176.996 53.9579C176.572 53.895 176.154 53.8016 175.745 53.6785C175.238 53.5339 174.737 53.3712 174.241 53.1903C172.824 52.6836 171.458 52.0437 170.162 51.2792C169.182 50.706 168.243 50.0642 167.353 49.3586C166.891 48.9924 166.443 48.6102 166.009 48.2119L166.551 48.4689C166.067 47.8169 165.583 47.1649 165.098 46.5129C164.63 45.8834 164.163 45.2539 163.697 44.6244C163.668 44.5858 163.646 44.5601 163.636 44.5473V44.5441C163.421 44.2582 163.209 43.9724 162.998 43.6865C162.36 42.829 161.731 41.5699 162.466 40.7959C162.622 40.6451 162.812 40.5337 163.02 40.4706C163.228 40.4075 163.448 40.3946 163.662 40.4329C163.892 40.4629 164.115 40.5347 164.319 40.6449C164.88 41.0087 165.356 41.4897 165.714 42.0549C166.214 41.3258 167.186 41.056 168.065 41.1749C168.113 41.1781 168.158 41.1845 168.202 41.1909C169.116 41.3483 169.511 40.8537 170.303 41.3387C170.672 41.5635 170.896 42.7197 171.265 42.9446C171.473 43.073 172.246 43.2658 172.458 43.3974C172.875 43.6544 172.769 44.6147 173.189 44.8717C173.5 45.0612 174.334 44.5505 174.645 44.74C174.972 44.9423 175.427 46.2784 175.754 46.4776C176.405 46.879 176.928 46.1468 177.579 46.545C178.416 47.0621 179.308 47.637 179.66 48.5556C179.971 49.365 179.571 50.4731 178.798 50.7043C179.167 50.7485 179.504 50.9347 179.739 51.2236C179.974 51.5125 180.087 51.8814 180.055 52.2524Z" fill="#E6E6E6"/>
<path opacity="0.2" d="M174.241 53.1903L174.161 53.9804C173.252 53.8928 172.358 53.691 171.499 53.3798C170.369 52.9998 169.388 52.2699 168.699 51.2954C168.263 50.6402 167.808 49.9882 167.353 49.3587C166.596 48.3052 165.845 47.3224 165.223 46.5226C164.463 45.5559 163.893 44.8557 163.697 44.6244C163.668 44.5859 163.646 44.5602 163.636 44.5474L163.626 44.5377L163.636 44.5441C167.446 49.3742 171.045 52.1595 174.35 52.0919C174.35 52.0919 174.738 53.3702 174.241 53.1903Z" fill="black"/>
<path d="M139.372 14.511C139.278 14.8741 139.265 15.2534 139.333 15.6222C139.401 15.991 139.55 16.3402 139.767 16.6454C139.985 16.9506 140.267 17.2043 140.593 17.3886C140.919 17.5729 141.282 17.6833 141.655 17.7121L145.918 33.3743L149.075 29.8457L144.444 15.1687C144.451 14.5372 144.226 13.9252 143.813 13.4487C143.399 12.9722 142.825 12.6642 142.2 12.5831C141.574 12.5021 140.941 12.6535 140.42 13.0088C139.899 13.3642 139.526 13.8986 139.372 14.511Z" fill="#FFB8B8"/>
<path d="M164.399 54.5617L164.37 53.1357L161.683 48.0321L161.584 47.8458L161.577 47.8362L159.515 43.9242L149.565 25.0304L148.497 24.6373L148.55 23.1025L148.497 21.4254L147.213 20.5634L146.666 19.524L145.89 19.7457H145.886L145.864 19.7521L142.471 20.706H142.468L141.762 21.1043L141.121 23.0313L140.981 23.9437L141.121 25.6008L142.725 25.922L142.193 27.6212L143.805 32.5094C145.822 41.0528 148.577 48.4272 153.333 52.6378L153.724 53.2802L156.818 58.3517C157.241 59.0454 157.859 59.5983 158.594 59.9407C159.33 60.2831 160.15 60.3996 160.952 60.2755H160.955C161.499 60.1909 162.021 59.9964 162.488 59.7038C162.919 59.4377 163.295 59.0922 163.598 58.6857C164.15 57.9507 164.442 57.0522 164.428 56.1323L164.399 54.5617Z" fill="#E6E6E6"/>
<path d="M220.618 177.901L133.474 178C133.372 178 133.275 177.96 133.204 177.888C133.132 177.816 133.092 177.719 133.092 177.618C133.092 177.516 133.132 177.419 133.204 177.347C133.275 177.275 133.372 177.235 133.474 177.235L220.618 177.136C220.719 177.136 220.817 177.177 220.888 177.248C220.96 177.32 221 177.417 221 177.519C221 177.62 220.96 177.718 220.888 177.789C220.817 177.861 220.719 177.901 220.618 177.901Z" fill="#CACACA"/>
<path d="M26.8179 49.3682C26.8125 44.5402 28.6542 39.8937 31.9642 36.3839L20.6047 20.7246C20.1371 20.0794 19.5362 19.5428 18.8429 19.1512C18.1495 18.7596 17.3801 18.5222 16.587 18.4553C15.7938 18.3883 14.9956 18.4933 14.2466 18.7632C13.4976 19.033 12.8155 19.4613 12.2467 20.019C8.35538 23.8182 5.26609 28.362 3.16229 33.3803C1.05849 38.3987 -0.0169172 43.7893 -4.873e-05 49.2321C-1.70457e-05 51.2874 0.152974 53.3399 0.457644 55.3725C0.577168 56.1761 0.870269 56.9439 1.31651 57.6223C1.76276 58.3008 2.35141 58.8736 3.0414 59.3008C3.7236 59.7237 4.48971 59.9925 5.28628 60.0883C6.08284 60.1842 6.8907 60.1048 7.65351 59.8558L27.268 53.4727C26.9695 52.125 26.8186 50.7487 26.8179 49.3682Z" fill="#F2F2F2"/>
<path d="M27.861 55.5731L7.46907 62.2092C6.71439 62.4536 6.02096 62.8575 5.43578 63.3938C4.8506 63.93 4.38733 64.5859 4.07738 65.3171C3.76743 66.0483 3.61804 66.8377 3.63932 67.6318C3.66061 68.4259 3.85207 69.2061 4.20074 69.9196C7.10472 75.8204 11.3855 80.9335 16.6807 84.826C21.9759 88.7185 28.1296 91.2758 34.6208 92.2814C35.4113 92.4031 36.2188 92.3521 36.9878 92.1318C37.7569 91.9116 38.4692 91.5273 39.076 91.0055C39.682 90.485 40.1684 89.8393 40.5017 89.1128C40.8351 88.3863 41.0075 87.5961 41.0071 86.7965V67.7208C37.987 66.9359 35.2069 65.4166 32.9137 63.2975C30.6204 61.1784 28.8849 58.5251 27.861 55.5731Z" fill="#E6E6E6"/>
<path d="M89.1849 67.4918C88.8406 66.673 88.3243 65.938 87.671 65.3369C87.0178 64.7359 86.243 64.2827 85.3993 64.0083L63.1592 56.7708C61.8609 59.8287 59.7832 62.4914 57.1342 64.492C54.4851 66.4926 51.3577 67.761 48.0652 68.17V91.3784C48.0656 92.2692 48.2584 93.1495 48.6302 93.9588C49.0019 94.7681 49.544 95.4873 50.2192 96.0672C50.8944 96.6471 51.6868 97.074 52.5421 97.3186C53.3974 97.5632 54.2953 97.6197 55.1745 97.4843C62.4003 96.3648 69.2505 93.518 75.1451 89.1849C81.0396 84.8518 85.805 79.1601 89.0378 72.5914C89.4264 71.8018 89.6407 70.9376 89.6661 70.0577C89.6915 69.1777 89.5274 68.3025 89.1849 67.4918Z" fill="#6A6884"/>
<path d="M83.6595 14.2583C83.0263 13.6375 82.267 13.1606 81.4332 12.8602C80.5995 12.5598 79.7108 12.4429 78.8279 12.5174C77.945 12.592 77.0884 12.8562 76.3166 13.2921C75.5448 13.7281 74.8759 14.3255 74.3554 15.0437L59.1504 36.0044C60.8996 37.7606 62.2863 39.8447 63.2313 42.1376C64.1763 44.4306 64.6611 46.8876 64.658 49.3682C64.658 49.8297 64.6358 50.2856 64.6033 50.7389L88.7727 58.6044C89.6273 58.8806 90.5317 58.967 91.423 58.8574C92.3144 58.7478 93.1711 58.445 93.9336 57.9699C94.6961 57.4948 95.3461 56.8589 95.8382 56.1065C96.3302 55.3542 96.6527 54.5034 96.7829 53.6135C97.1221 51.3508 97.2924 49.0658 97.2924 46.7777C97.3112 40.719 96.114 34.7182 93.7721 29.1319C91.4301 23.5455 87.9912 18.4875 83.6595 14.2583Z" fill="#09BF30"/>
<path d="M45.7379 30.4185C48.9931 30.4148 52.1936 31.2566 55.027 32.8616L64.3074 20.0681C64.7805 19.4166 65.1068 18.67 65.2638 17.8799C65.4207 17.0897 65.4047 16.2749 65.2167 15.4916C65.0347 14.7232 64.6884 14.0036 64.2016 13.3823C63.7149 12.761 63.0994 12.2529 62.3976 11.8931C56.6679 8.97451 50.3323 7.44875 43.9042 7.43937C37.4761 7.43 31.1361 8.93728 25.398 11.8391C24.6743 12.2048 24.0387 12.7237 23.5352 13.3599C23.0316 13.996 22.6722 14.7342 22.4818 15.5233C22.294 16.3011 22.2773 17.1105 22.4329 17.8955C22.5884 18.6804 22.9126 19.4221 23.3828 20.0691L33.9107 34.5823C37.2627 31.8824 41.4365 30.4131 45.7379 30.4185Z" fill="#E6E6E6"/>
</g>
<defs>
<clipPath id="clip0_626_83">
<rect width="221" height="178" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
assets/sounds/check.wav Normal file

Binary file not shown.

BIN
assets/sounds/click.wav Normal file

Binary file not shown.

View File

@@ -0,0 +1,2 @@
check.wav - https://freesound.org/s/456161/
click.wav - https://freesound.org/s/268108/

194
docs/01-REQUIREMENTS.md Normal file
View File

@@ -0,0 +1,194 @@
# Habo 需求分析文档
> 版本: 3.1.2 | 最后更新: 2026-04-13
---
## 1. 项目定位
### 1.1 产品定义
**Habo** 是一款**极简主义的习惯追踪应用**,帮助用户通过每日打卡、进度可视化和心理模型引导,建立并维持长期习惯。
### 1.2 解决的核心问题
| 痛点 | 描述 |
|------|------|
| **习惯难以坚持** | 人们制定了计划但缺乏日常反馈机制,几天后放弃 |
| **缺乏正向激励** | 完成习惯后没有即时奖励感,大脑无法建立正向反馈回路 |
| **数据孤岛** | 习惯数据存储在云端,依赖网络,存在隐私顾虑 |
| **功能过度** | 现有习惯类 App 功能繁杂,设置成本高,反而降低使用意愿 |
| **缺乏科学方法** | 用户不了解习惯形成的心理学原理,只靠意志力 |
### 1.3 目标用户画像
| 画像 | 特征 | 核心需求 |
|------|------|----------|
| **自律建设者** | 想建立新习惯(运动、阅读、冥想)但经常中断的成年人 | 简单打卡 + 可视化连续天数 |
| **数据驱动型** | 喜欢用数据量化自我、追踪进步的用户 | 统计图表 + 导出功能 |
| **隐私敏感型** | 不信任云服务,希望所有数据留在本地的用户 | 纯本地存储 + 本地备份 |
| **多设备用户** | 在手机、平板、桌面端都需要使用 | 跨平台支持Android/iOS/macOS/Linux |
### 1.4 差异化定位
与 Habitica、Streaks、Loop 等竞品相比Habo 的核心差异化:
- **极简设计** — 无社交、无游戏化、无广告,聚焦习惯本身
- **心理学模型内置** — Cue-Routine-Reward 习惯循环、两天法则、习惯合约(惩罚/问责伙伴)
- **纯本地架构** — 零网络依赖,数据完全由用户掌控
- **跨平台开源** — Flutter 实现的真正跨平台体验
- **数值型习惯** — 不仅支持"做了/没做",还支持"做了多少"(如跑步 5km、喝水 8 杯)
---
## 2. 功能需求
### 2.1 功能矩阵
#### P0 — 核心功能MVP 必须)
| ID | 功能 | 描述 |
|----|------|------|
| F01 | 习惯创建 | 创建新习惯,设置标题和基础配置 |
| F02 | 每日打卡 | 日历视图中对每一天标记状态:完成 / 失败 / 跳过 |
| F03 | 日历视图 | 月度日历展示,每天显示对应的打卡状态标记 |
| F04 | 连续天数 | 计算并展示当前连续完成天数和最高连续记录 |
| F05 | 习惯列表 | 展示所有活跃习惯,支持拖拽排序 |
| F06 | 编辑/删除 | 编辑习惯属性或永久删除 |
| F07 | 持久化存储 | 所有数据本地 SQLite 存储,关闭应用不丢失 |
| F08 | 浅色/深色主题 | 跟随系统或手动切换浅色/深色主题 |
#### P1 — 重要功能
| ID | 功能 | 描述 |
|----|------|------|
| F09 | 两天法则 | 允许用户启用"两天法则"——允许间隔一天失败而不打断连续天数 |
| F10 | 习惯循环 (Cue-Routine-Reward) | 基于《原子习惯》的习惯循环模型,设置提示、例行、奖励 |
| F11 | 习惯合约 | 设置惩罚和问责伙伴,失败时显示惩罚提醒 |
| F12 | 统计总览 | 饼图展示所有习惯的整体完成率分布 |
| F13 | 个人统计 | 每个习惯的详细统计:最高连续、当前连续、月度柱状图 |
| F14 | 数值型习惯 | 支持设置目标值和单位的习惯类型(如 10000 步) |
| F15 | 进度追踪 | 数值型习惯可以记录部分进度,不要求一次性完成 |
| F16 | 分类系统 | 习惯可归属多个分类,支持按分类筛选显示 |
| F17 | 通知提醒 | 每日定时提醒用户打卡 |
| F18 | 备份/恢复 | 导出/导入 JSON 备份文件,支持跨设备数据迁移 |
| F19 | 引导页 | 首次使用时展示三步引导(定义习惯 → 记录天数 → 观察进步) |
| F20 | 归档功能 | 将不再追踪的习惯归档而非删除,保留历史数据 |
| F21 | 音效反馈 | 打卡时播放音效,增强即时满足感 |
| F22 | 备注/日记 | 每日打卡时可添加文字备注 |
#### P2 — 增强功能
| ID | 功能 | 描述 |
|----|------|------|
| F23 | Material You 主题 | Android 12+ 动态取色主题 |
| F24 | OLED 黑色主题 | 纯黑背景,适配 OLED 屏幕省电 |
| F25 | 自定义颜色 | 用户可自定义完成、失败、跳过、进度的颜色 |
| F26 | 生物识别锁 | 指纹/面容锁定应用,保护隐私 |
| F27 | 桌面小组件 | iOS/Android 主屏幕小组件显示今日完成进度 |
| F28 | 一键打卡 | 无需打开菜单,单击直接标记完成 |
| F29 | 深度链接 | 支持 `habo://` URL scheme 跳转到指定页面 |
| F30 | 周起始日设置 | 用户可选择周日或周一作为日历的起始日 |
| F31 | 显示月份名称 | 可选在日历中显示月份名称 |
| F32 | 27 种语言 | 支持中英日韩等 27 种语言的完整界面翻译 |
### 2.2 功能依赖关系
```
F01 (创建) ──→ F02 (打卡) ──→ F04 (连续天数)
│ │
│ ├──→ F12 (统计总览)
│ └──→ F13 (个人统计)
├──→ F03 (日历视图)
├──→ F05 (列表) ──→ F16 (分类)
├──→ F06 (编辑/删除)
├──→ F10 (习惯循环) ──→ F11 (习惯合约)
├──→ F14 (数值型) ──→ F15 (进度追踪)
└──→ F20 (归档)
```
---
## 3. 非功能性需求
### 3.1 平台支持
| 平台 | 最低版本 | 说明 |
|------|----------|------|
| Android | API 21 (Android 5.0) | 支持 split-per-abi APK 分包 |
| iOS | 12.0+ | 完整功能 |
| Linux | - | 通过 sqflite_common_ffi 支持 |
| macOS | - | 原生窗口管理 |
### 3.2 性能要求
| 指标 | 要求 |
|------|------|
| 启动时间 | 冷启动 < 2 秒(含 splash screen |
| 日历切换 | 月份切换流畅 60fps无明显卡顿 |
| 数据容量 | 支持至少 100 个习惯 × 365 天的事件记录 |
| 备份文件 | 支持 10MB 以内的备份文件导入 |
| 内存占用 | 日常使用 < 100MB RAM |
### 3.3 安全与隐私
| 要求 | 实现方式 |
|------|----------|
| 数据不离开设备 | 纯 SQLite 本地存储,无网络请求 |
| 生物识别保护 | 通过 `local_auth` 调用系统指纹/面容识别 |
| 备份文件控制 | 用户手动导出/导入,应用不自动上传 |
| 无第三方分析 | 不集成 Google Analytics、Firebase 等追踪服务 |
### 3.4 可访问性
| 要求 | 说明 |
|------|------|
| Material Design 规范 | 遵循 Material Design 无障碍指南 |
| 高对比度 | 深色/OLED 主题提供高对比度 |
| 大字体支持 | 响应系统字体缩放设置 |
| 语义化标签 | Flutter Semantics 为屏幕阅读器提供语义信息 |
---
## 4. 用户故事
### 4.1 核心用户故事
| 编号 | 用户故事 | 验收标准 |
|------|---------|----------|
| US01 | 作为用户,我想快速创建一个新习惯,以便开始追踪 | 输入标题 → 点击保存 → 习惯出现在列表中 |
| US02 | 作为用户,我想在日历上点击某天标记完成,以便记录我的每日进度 | 点击日期 → 选择"完成" → 日历显示绿色标记 |
| US03 | 作为用户,我想看到当前连续天数,以便获得坚持的动力 | 连续 2 天以上完成时显示绿色连续天数徽章 |
| US04 | 作为用户,我想查看月度统计图表,以便了解长期趋势 | 统计页显示柱状图,可切换年份和事件类型 |
| US05 | 作为用户,我想导出数据备份,以便在更换设备时迁移数据 | 设置页点击备份 → 生成 JSON 文件 → 保存到文件系统 |
| US06 | 作为用户,我想设置每日提醒通知,以便不忘记打卡 | 设置通知时间 → 每天定时收到通知 |
| US07 | 作为用户,我想追踪数值型目标(如每天 8 杯水),以便记录部分完成 | 创建数值习惯 → 输入进度值 → 查看百分比完成度 |
| US08 | 作为用户,我想启用两天法则,以便在偶尔失败时不中断连续记录 | 开启两天法则 → 失败一天后次日完成 → 连续天数不中断 |
| US09 | 作为用户,我想用分类管理习惯,以便快速筛选查看 | 创建分类 → 习惯关联分类 → 按分类筛选列表 |
| US10 | 作为用户,我想归档不再追踪的习惯,以便保持列表整洁但不丢失历史 | 点击归档 → 习惯从主列表消失 → 可在归档列表中查看 |
### 4.2 边界场景
| 场景 | 期望行为 |
|------|----------|
| 用户 31 天没有任何事件记录 | 连续天数显示为 0无徽章 |
| 用户连续完成 100 天 | 连续天数正确显示 100 |
| 用户在两天法则下连续两天失败 | 连续天数归零 |
| 数值习惯进度超过目标值 | 标记为已完成,进度条满格 |
| 备份文件格式错误 | 显示错误提示,不覆盖现有数据 |
| 应用从后台恢复且已启用生物识别 | 弹出认证对话框,认证失败可重试 |
| 跨午夜使用应用23:59 → 00:01 | 自动检测日变化,刷新日历视图 |
| 空标题尝试保存 | 显示错误提示"The habit title cannot be empty" |
---
## 5. 设计原则
| 原则 | 说明 |
|------|------|
| **极简优先** | 默认只显示标题和日历,高级选项折叠隐藏 |
| **即时反馈** | 每次打卡有视觉(颜色变化)和听觉(音效)反馈 |
| **正向强化** | 奖励通知优先于惩罚通知,连续天数优先于失败天数 |
| **零配置可用** | 安装后即可使用,不需要注册、登录或网络 |
| **用户控制** | 所有数据可导出、可删除,用户完全掌控 |

482
docs/02-ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,482 @@
# Habo 架构设计文档
> 基于 REQUIREMENTS.md 中的需求,说明架构决策及其理由
---
## 1. 架构决策总览
### 1.1 为什么选择这个架构
| 决策 | 理由 |
|------|------|
| **纯客户端架构** | 需求零网络依赖、数据不离开设备NFR-安全)。没有服务端意味着无运维成本、无数据泄露风险 |
| **SQLite 本地存储** | 需求持久化存储、离线可用。SQLite 是最成熟的嵌入式数据库,无需额外进程 |
| **Flutter 跨平台** | 需求:支持 Android/iOS/Linux/macOS 四个平台。一套代码覆盖所有目标平台 |
| **Provider + ChangeNotifier** | 需求:响应式 UI 更新。Flutter 官方推荐方案,学习曲线低,适合中等复杂度应用 |
| **Repository Pattern** | 需求:业务逻辑与数据访问解耦。便于替换数据源和编写单元测试 |
| **Navigation 2.0** | 需求深度链接支持habo://settings。声明式路由更易管理页面栈 |
### 1.2 架构约束
- **无网络** — 所有功能离线可用,备份通过文件系统完成
- **单用户** — 无需多用户系统,简化数据模型
- **单数据库** — 一个 SQLite 文件 `habo_db0.db`,数据库版本 9
- **无实时同步** — 数据只存在本地,跨设备迁移依赖手动备份/恢复
---
## 2. 分层架构
```
┌─────────────────────────────────────────────────────────────────┐
│ Presentation Layer (展示层) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
│ │ Screens │ │ Widgets │ │ Onboarding│ │ Dialogs │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │
│ │ │ │ │ │
├───────┴─────────────┴────────────┴──────────────┴─────────────────┤
│ Business Logic Layer (业务逻辑层) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ HabitsManager│ │SettingsManager│ │ Statistics │ │
│ │ (习惯 CRUD) │ │ (设置管理) │ │ (统计计算) │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
├─────────┴──────────────────┴────────────────────┴─────────────────┤
│ Service Layer (服务层) │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ Notification│ │ Backup │ │UIFeedback│ │ BiometricAuth │ │
│ │ Service │ │ Service │ │ Service │ │ Service │ │
│ └─────┬──────┘ └─────┬──────┘ └────┬─────┘ └──────┬────────┘ │
│ │ │ │ │ │
├────────┴───────────────┴─────────────┴───────────────┴────────────┤
│ Repository Layer (数据访问层) │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ HabitRepo │ │ EventRepo │ │ CategoryRepo │ │
│ │ (接口) │ │ (接口) │ │ (接口) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬───────┘ │
│ │ │ │ │
├─────────┴───────────────┴───────────────┴─────────────────────────┤
│ Data Layer (数据层) │
│ ┌──────────────────────────────────────────┐ │
│ │ HaboModel → SQLite (sqflite / ffi) │ │
│ │ 数据库: habo_db0.db (版本 9) │ │
│ └──────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────┘
```
---
## 3. 模块职责划分
### 3.1 Presentation Layer
| 模块 | 文件 | 职责 |
|------|------|------|
| **HabitsScreen** | `habits/habits_screen.dart` | 主屏幕入口承载习惯列表、导航栏、FAB |
| **CalendarColumn** | `habits/calendar_column.dart` | 可拖拽排序的习惯列表容器,含分类筛选 |
| **Habit Card** | `habits/habit.dart` | 单个习惯卡片组件(含日历、连续天数、事件标记) |
| **EditHabit** | `habits/edit_habit.dart` | 创建/编辑习惯的表单页面 |
| **StatisticsScreen** | `statistics/statistics_screen.dart` | 统计总览页面 |
| **SettingsScreen** | `settings/settings_screen.dart` | 设置页面 |
| **Onboarding** | `onboarding/onboarding.dart` | 三步引导流程 |
**设计原则**
- 展示层不直接操作数据库,只通过 Manager 类
- 每个 Screen 是独立的 StatefulWidget
- 可复用的 UI 片段提取到 `widgets/` 目录
### 3.2 Business Logic Layer
#### HabitsManager核心管理器
**为什么需要 Manager 而非直接操作 Repository**
```
┌─────────────────────────────────────────────┐
│ HabitsManager │
│ ┌─────────────────────────────────────┐ │
│ │ 1. 内存状态管理 (allHabits 列表) │ │
│ │ 2. 业务规则执行 (排序、归档、undo) │ │
│ │ 3. 服务协调 (通知、备份、小组件) │ │
│ │ 4. 变更通知 (notifyListeners) │ │
│ └─────────────────────────────────────┘ │
│ │ │ │ │
│ HabitRepo EventRepo CategoryRepo │
│ │ │ │ │
│ NotificationService BackupService ... │
└─────────────────────────────────────────────┘
```
**职责清单**
- 维护 `allHabits` 内存列表(活跃 + 归档)
- 协调 Repository 的 CRUD 操作
- 调用通知服务更新提醒
- 调用小组件服务更新桌面小组件
- 调用 UI 反馈服务展示消息
- 处理 undo删除后可撤销
- 处理拖拽排序的位置更新
#### SettingsManager
**职责**
- 管理所有用户偏好设置(主题、音效、通知等)
- 使用 SharedPreferences 持久化
- 管理 SoLoud 音效引擎的初始化和播放
- 通知 UI 主题变化
#### Statistics
**为什么统计是纯函数而非 Manager**
统计计算是无状态的——每次从当前习惯数据实时计算,不需要维护额外状态。因此设计为纯静态方法 `Statistics.calculateStatistics()`
### 3.3 Service Layer
**为什么需要 Service 层?**
跨领域的关注点通知、备份、UI反馈、认证不属于任何单一 Manager 的职责,提取为独立服务便于复用和测试。
| 服务 | 依赖 | 被谁调用 |
|------|------|----------|
| NotificationService | awesome_notifications | HabitsManager |
| BackupService | RepositoryFactory, 文件系统 | HabitsManager, SettingsScreen |
| UIFeedbackService | ScaffoldMessenger | HabitsManager |
| BiometricAuthService | local_auth | BiometricAuthWrapper |
| HomeWidgetService | home_widget | HabitsManager |
| ServiceLocator | 所有服务 | main.dart (初始化) |
### 3.4 Repository Layer
**为什么用 Repository Pattern 而非直接用 HaboModel**
```
// 不好的方式 — UI 直接依赖数据层
HaboModel().insertHabit(habit);
// 好的方式 — 通过抽象接口解耦
HabitRepository habitRepo = RepositoryFactory.createHabitRepository(model);
habitRepo.createHabit(habit);
```
好处:
1. **可测试** — 可以用 MockRepository 替换真实数据库
2. **可替换** — 未来换数据库引擎只需改 Repository 实现
3. **关注点分离** — UI/Manager 不知道也不关心数据如何存储
### 3.5 Data Layer (HaboModel)
**定位**:直接操作 SQLite 的底层类,是整个数据层的基石。
**特殊处理**
- 移动端使用 `sqflite`
- 桌面端Linux/macOS使用 `sqflite_common_ffi`
- 启用 `PRAGMA foreign_keys = ON` 确保级联删除
- 管理数据库版本升级迁移(版本 1→9
---
## 4. 数据流设计
### 4.1 用户打卡事件流
```
用户点击日历日期
├─── 一键打卡模式 ON
│ └── 直接切换 check/clear
└─── 一键打卡模式 OFF
└── 弹出选择菜单
├── Check (完成)
├── Progress (进度,数值型)
├── Fail (失败)
├── Skip (跳过)
├── Note (备注)
└── Date (修改日期)
HabitsManager.addEvent(id, date, eventData)
┌─────────┴─────────┐
│ │
内存状态更新 Repository 写入
habitData.events EventRepository.add()
│ │
│ SQLite INSERT/REPLACE
│ │
▼ ▼
notifyListeners() NotificationService
│ (奖励/惩罚通知)
Provider → Widget 重建
(日历标记更新、连续天数更新)
```
### 4.2 应用初始化流
```
main()
├── 1. WidgetsFlutterBinding.ensureInitialized()
├── 2. SettingsManager.loadData() ← SharedPreferences
├── 3. HaboModel.initDatabase() ← SQLite 初始化
├── 4. ServiceLocator.init(model) ← 注册所有服务
├── 5. HabitsManager(repos, services) ← 注入依赖
├── 6. HabitsManager.loadHabits() ← 从数据库加载所有习惯
├── 7. NotificationService 初始化
├── 8. AppRouter(stateManagers) ← 创建路由
├── 9. 日变化定时器启动 ← 检测跨日刷新
└── runApp(MultiProvider → MaterialApp.router)
├── ChangeNotifierProvider<SettingsManager>
├── ChangeNotifierProvider<HabitsManager>
└── ChangeNotifierProvider<AppStateManager>
```
### 4.3 跨日刷新流
```
DayChangeTimer (每小时检查)
├── 检测到日期变化 (now.day != lastDay.day)
│ │
│ ├── HabitsManager.loadHabits() ← 重新加载习惯数据
│ ├── NotificationService.reset() ← 重置通知调度
│ └── HomeWidgetService.update() ← 更新桌面小组件
└── 未变化 → 等待下次检查
```
---
## 5. 导航架构
### 5.1 为什么选择 Navigation 2.0
| Navigation 1.0 (Navigator) | Navigation 2.0 (RouterDelegate) |
|---------------------------|----------------------------------|
| 命令式 push/pop | 声明式页面栈 |
| 不支持深度链接 | 原生支持 URL 映射 |
| 难以管理复杂页面栈 | 通过状态管理器统一控制 |
### 5.2 页面栈结构
```
AppRouter (RouterDelegate)
├── SplashScreen ← 初始页(条件判断后自动跳转)
│ │
│ ├── 首次使用 → OnboardingScreen
│ ├── 有新版本 → WhatsNewScreen
│ └── 正常使用 → HabitsScreen (主页面)
├── HabitsScreen ← 主页面(始终在栈底)
│ ├── → StatisticsScreen
│ ├── → SettingsScreen
│ ├── → CreateHabitScreen
│ └── → EditHabitScreen
└── 深度链接: habo://settings → 直接打开 SettingsScreen
```
### 5.3 状态驱动导航
```
AppStateManager (ChangeNotifier)
├── _statistics: bool → 控制 StatisticsScreen 显示
├── _settings: bool → 控制 SettingsScreen 显示
├── _onboarding: bool → 控制 OnboardingScreen 显示
├── _whatsNew: bool → 控制 WhatsNewScreen 显示
├── _createHabit: bool → 控制 CreateHabitScreen 显示
└── _editHabit: bool → 控制 EditHabitScreen 显示
AppRouter 监听 AppStateManager 变更
└── 根据 bool 标志位组合构建 pages 列表
```
**关键设计决策**`AppRouter` 不监听 `HabitsManager`,因为习惯数据变化不应触发导航跳转,只应刷新当前页面内容。
---
## 6. 状态管理策略
### 6.1 Provider 分布
```
MultiProvider(
providers: [
ChangeNotifierProvider<SettingsManager> ← 主题、音效、设置
ChangeNotifierProvider<HabitsManager> ← 习惯数据、分类
ChangeNotifierProvider<AppStateManager> ← 导航状态
]
)
```
### 6.2 读写模式
```dart
// 读取 — context.watch<T>() 或 Provider.of<T>(context)
// UI 组件监听变化并自动重建
final habits = context.watch<HabitsManager>().activeHabits;
// 写入 — context.read<T>() 或 Provider.of<T>(context, listen: false)
// 事件处理中调用方法,不触发当前 widget 重建
context.read<HabitsManager>().addEvent(id, date, event);
```
### 6.3 状态生命周期
| 状态 | 作用域 | 生命周期 |
|------|--------|----------|
| HabitsManager.allHabits | 全局 | 应用启动到关闭 |
| SettingsManager.* | 全局 | 应用启动到关闭 |
| AppStateManager.* | 全局 | 应用启动到关闭 |
| HabitData.events | 每个习惯 | 随 HabitsManager 加载 |
| 习惯卡片 UI 状态 (streak, calendar) | 单个 Widget | Widget 生命周期 |
---
## 7. 主题系统设计
### 7.1 为什么支持 5 种主题模式
| 主题 | 目标用户 | 技术实现 |
|------|----------|----------|
| Device | 大多数用户 | 跟随系统 MediaQuery |
| Light | 强制浅色偏好 | 固定浅色 ColorScheme |
| Dark | 强制深色偏好 | 固定深色 ColorScheme |
| OLED | OLED 屏幕用户 | 纯黑 (#000000) 背景 |
| Material You | Android 12+ 用户 | dynamic_color 提取壁纸颜色 |
### 7.2 颜色体系
```
核心颜色常量:
primary: #09BF30 (完成/主色)
red: #F44336 (失败)
skip: #FBC02D (跳过)
orange: #FF9800 (两天法则警告)
progress: #2196F3 (进度)
progressBg: #E3F2FD (进度背景)
用户可自定义:
checkColor: 默认 primary
failColor: 默认 red
skipColor: 默认 skip
progressColor: 默认 progress
```
---
## 8. 通知系统设计
### 8.1 通知类型
| 类型 | 触发条件 | 内容 |
|------|----------|------|
| **每日提醒** | 用户设定时间 | "Do not forget to check your habits." |
| **奖励通知** | 习惯标记完成 + showReward 开启 | "Congratulations! Your reward: {reward}" |
| **惩罚通知** | 习惯标记失败 + showSanction 开启 | "Oh no! Your sanction: {sanction}" |
### 8.2 通知调度策略
```
创建/编辑习惯
└── NotificationService.resetNotifications()
├── 取消所有现有通知
└── 为每个启用通知的习惯创建定时通知
└── awesome_notifications.createNotification()
├── channel: "habit_notifications"
├── schedule: 每日重复 at notTime
└── payload: habitId
删除习惯
└── NotificationService.removeNotifications(id)
```
---
## 9. 备份系统设计
### 9.1 为什么选择 JSON 文件而非二进制
- **用户可读** — 用户可以打开 JSON 查看自己的数据
- **调试友好** — 开发时可直接检查备份内容
- **版本控制** — 可以 git diff 对比变化
- **跨平台** — JSON 在所有平台上通用
### 9.2 备份文件格式
```json
{
"version": 3,
"habits": [...],
"events": { "habitId": { "date": [dayType, ...] } },
"categories": [...],
"habit_categories": [{ "habit_id": 1, "category_id": 1 }],
"metadata": {
"imported_from": "legacy_list",
"import_timestamp": "ISO8601"
}
}
```
### 9.3 兼容性策略
- 新格式包含 `version` 字段用于版本识别
- 支持读取旧版数组格式(无 version 字段 = 旧版)
- 导入时自动转换为当前格式
- 文件大小限制 10MB
---
## 10. 依赖注入设计
### 10.1 ServiceLocator 模式
```
ServiceLocator (单例)
├── 提供:
│ ├── RepositoryFactory → 创建各 Repository
│ ├── NotificationService
│ ├── BackupService
│ ├── UIFeedbackService
│ ├── BiometricAuthService
│ └── HomeWidgetService
├── 初始化时接收:
│ └── HaboModel (共享数据库连接)
└── 使用方:
└── main.dart 中创建并注入到 HabitsManager
```
### 10.2 HabitsManager 的依赖注入
```dart
HabitsManager(
habitRepository: repoFactory.habitRepository,
eventRepository: repoFactory.eventRepository,
categoryRepository: repoFactory.categoryRepository,
backupService: serviceLocator.backupService, // 可选
notificationService: serviceLocator.notificationService, // 可选
uiFeedbackService: serviceLocator.uiFeedbackService, // 可选
)
```
可选服务的设计使得在测试时可以传入 null方便隔离测试业务逻辑。
---
## 11. 错误处理策略
| 层级 | 策略 | 用户体验 |
|------|------|----------|
| Data Layer (HaboModel) | try-catch + debugPrint | 静默失败,不中断应用 |
| Repository Layer | 抛出异常 | 向上传播 |
| Service Layer | 返回结果对象 (BackupResult) | UI 反馈服务展示错误消息 |
| Manager Layer | catch + UIFeedbackService.showError() | 用户看到错误提示 |
| Presentation Layer | FutureBuilder 处理 loading/error | 加载指示器 + 错误状态 |

807
docs/03-SPECIFICATION.md Normal file
View File

@@ -0,0 +1,807 @@
# Habo 实现规格文档
> 基于 REQUIREMENTS.md 和 ARCHITECTURE.md定义每个功能的具体实现逻辑、算法和交互流程
---
## 1. 连续天数 (Streak) 算法
### 1.1 普通模式 (`_updateLastStreakNormal`)
```
输入: habitData.events (SplayTreeMap<DateTime, List>, 按日期升序排列)
输出: streak 值, streakVisible (bool), orangeStreak (bool)
算法:
1. 从最后一天开始,向前遍历 events
2. 初始化 inStreak = 0
3. 对每个事件(从后往前):
a. 如果 DayType == clear → 跳过
b. 如果日期间隔 > 1 天 → 断开,结束循环
c. 如果 DayType == check → inStreak++
d. 如果 DayType == progress (数值型):
- 如果 progressValue >= targetValue → inStreak++
- 否则 → 跳过
e. 如果 DayType == fail 或 skip → 断开,结束循环
4. streak = inStreak
5. streakVisible = (streak >= 2)
6. orangeStreak = false
```
### 1.2 两天法则模式 (`_updateLastStreakTwoDay`)
```
输入: habitData.events, habitData.twoDayRule == true
输出: streak 值, streakVisible, orangeStreak
算法:
1. 从最后一天开始,向前遍历 events
2. 变量: inStreak = 0, usingTwoDayRule = false
3. 对每个事件(从后往前):
a. 如果 DayType == clear → 跳过
b. 如果日期间隔 > 1 天 → 断开,结束循环
c. 如果 DayType == check → inStreak++, usingTwoDayRule = false
d. 如果 DayType == progress (数值型) 且 progress >= target → inStreak++, usingTwoDayRule = false
e. 如果 DayType == fail:
- 如果 usingTwoDayRule == true → 断开,结束循环(连续两天失败)
- 如果 usingTwoDayRule == false → usingTwoDayRule = true, 不增加 inStreak
f. 如果 DayType == skip:
- 如果 usingTwoDayRule == true → 断开
- 否则 → 跳过(不影响连续)
4. streak = inStreak
5. streakVisible = (streak >= 2)
6. orangeStreak = usingTwoDayRule ← 橙色表示"处于危险中"
```
**两天法则图解**
```
情况 1: ✅ ✅ ❌ ✅ ✅ → streak = 5 (✅ 失败一天后立即恢复)
情况 2: ✅ ✅ ❌ ❌ ✅ → streak = 0 (✅ 连续两天失败,归零)
情况 3: ✅ ✅ ❌ ⏭ ✅ → streak = 0 (⏭ 在两天法则期间跳过,归零)
情况 4: ✅ ✅ ⏭ ✅ ✅ → streak = 5 (⏭ 跳过不影响连续)
```
---
## 2. 日历事件交互流程
### 2.1 日历日期点击
```
用户点击日历日期
├── 检查 oneTapCheck 设置
│ │
│ ├── oneTapCheck == true (一键打卡模式):
│ │ │
│ │ ├── 布尔型习惯:
│ │ │ ├── 当前无事件 → 创建 [DayType.check, ""]
│ │ │ └── 当前有事件 → 删除事件 (设为 clear)
│ │ │
│ │ └── 数值型习惯:
│ │ ├── 当前无事件 → 创建 [DayType.check, "", targetValue, targetValue]
│ │ └── 当前有事件 → 删除事件
│ │
│ └── oneTapCheck == false (菜单模式):
│ │
│ └── 弹出选择菜单6 个选项:
│ │
│ ├── 📅 Date → 修改日期选择器
│ │
│ ├── ✅ Check → 标记完成
│ │ ├── 布尔型: 事件 = [DayType.check, ""]
│ │ └── 数值型: 事件 = [DayType.check, "", targetValue, targetValue]
│ │ └── 播放 check 音效
│ │ └── 如果 showReward → 显示奖励通知
│ │
│ ├── Plus/Progress → 数值型专用
│ │ └── 弹出 ProgressInputModal
│ │ ├── 圆形进度指示器 (120px)
│ │ ├── 当前值 / 目标值 显示
│ │ ├── 快捷按钮: +partialValue, -partialValue
│ │ ├── 直接输入文本框
│ │ └── "Complete" 按钮直接设为目标值
│ │
│ ├── ❌ Fail → 标记失败
│ │ └── 事件 = [DayType.fail, ""]
│ │ └── 播放 click 音效
│ │ └── 如果 showSanction → 显示惩罚通知
│ │
│ ├── ⏭ Skip → 标记跳过
│ │ └── 事件 = [DayType.skip, ""]
│ │ └── 播放 click 音效
│ │
│ └── 💬 Note → 添加备注
│ └── 弹出文本输入对话框
│ └── 保留原有事件类型,更新 comment
HabitsManager.addEvent(habitId, date, eventData)
├── 更新内存: habitData.events[date] = eventData
├── 写入数据库: EventRepository → SQLite REPLACE
├── 更新连续天数: _updateLastStreak()
├── 更新桌面小组件: HomeWidgetService.update()
└── notifyListeners() → UI 重建
```
### 2.2 事件数据结构
```dart
// 布尔型习惯
[DayType.check, ""] // 完成
[DayType.fail, ""] // 失败
[DayType.skip, ""] // 跳过
[DayType.check, "好的开始"] // 完成 + 备注
// 数值型习惯
[DayType.check, "", 5.0, 5.0] // 完成 (5/5 km)
[DayType.progress, "", 3.5, 5.0] // 部分进度 (3.5/5 km)
[DayType.fail, ""] // 失败
// 数组索引:
// [0] = DayType (枚举)
// [1] = comment (String)
// [2] = progressValue (double, 数值型)
// [3] = targetValue (double, 数值型)
```
---
## 3. 统计计算算法
### 3.1 数据结构
```dart
class StatisticsData {
String title; // 习惯标题
int topStreak = 0; // 最高连续天数
int actualStreak = 0; // 当前连续(遍历中)
int checks = 0; // 完成次数
int fails = 0; // 失败次数
int skips = 0; // 跳过次数
int progress = 0; // 进度次数
SplayTreeMap<int, Map<DayType, List<int>>> monthlyTracking;
// key = year * 100 + month (如 202604)
// value = { DayType: [day1, day2, ...] }
}
class OverallStatisticsData {
int totalChecks;
int totalFails;
int totalSkips;
int totalProgress;
}
```
### 3.2 计算流程
```
Statistics.calculateStatistics(habits):
├── 1. 创建 AllStatistics 容器
├── 2. 遍历每个 habit:
│ │
│ ├── 创建 StatisticsData
│ │
│ ├── 3. 遍历 events (按日期升序):
│ │ │
│ │ ├── 计算日期间隔:
│ │ │ └── 如果间隔 > 1 天 → actualStreak 归零
│ │ │
│ │ ├── DayType.check:
│ │ │ ├── checks++
│ │ │ ├── actualStreak++
│ │ │ └── if actualStreak > topStreak → topStreak = actualStreak
│ │ │
│ │ ├── DayType.progress:
│ │ │ ├── progress++
│ │ │ └── if 数值型 && progressValue >= targetValue:
│ │ │ ├── actualStreak++
│ │ │ └── update topStreak
│ │ │
│ │ ├── DayType.fail:
│ │ │ ├── fails++
│ │ │ └── if twoDayRule:
│ │ │ ├── if usingTwoDayRule → actualStreak = 0
│ │ │ └── else → usingTwoDayRule = true
│ │ │ └── else → actualStreak = 0
│ │ │
│ │ ├── DayType.skip:
│ │ │ ├── skips++
│ │ │ └── if usingTwoDayRule → actualStreak = 0
│ │ │
│ │ └── 记录到 monthlyTracking:
│ │ └── key = year * 100 + month
│ │ └── monthlyTracking[key][dayType].add(day)
│ │
│ └── 添加到 allStatistics
└── 4. 汇总 OverallStatisticsData:
├── totalChecks = sum(各习惯 checks)
├── totalFails = sum(各习惯 fails)
├── totalSkips = sum(各习惯 skips)
└── totalProgress = sum(各习惯 progress)
```
---
## 4. 习惯 CRUD 交互流程
### 4.1 创建习惯
```
用户点击 FAB (+)
└── AppStateManager.goCreateHabit(true)
└── EditHabitScreen(isNew: true)
├── 表单字段:
│ ├── title (必填, 不能为空)
│ ├── habitType (下拉: Checkable / Progressive)
│ │ └── 如果 Progressive:
│ │ ├── targetValue (NumberFormat('#.##'))
│ │ ├── partialValue
│ │ └── unit
│ ├── twoDayRule (Checkbox)
│ ├── categories (多选)
│ ├── notification (Checkbox)
│ │ └── notTime (TimePicker)
│ └── [展开] Advanced:
│ ├── cue (提示触发器)
│ ├── routine (例行动作)
│ ├── reward (奖励)
│ ├── showReward (显示奖励通知)
│ ├── sanction (惩罚)
│ ├── showSanction (显示惩罚通知)
│ └── accountant (问责伙伴)
└── 点击保存 (FAB ✓):
├── 验证 title 非空
├── 创建 HabitData:
│ ├── id: null (数据库自增)
│ ├── position: activeHabits.length (追加到末尾)
│ └── ... 各字段
├── HabitsManager.addHabit(habit)
│ ├── HabitRepository.createHabit() → INSERT
│ ├── CategoryRepository.updateHabitCategories()
│ ├── 如果 notification → NotificationService.reset()
│ └── notifyListeners()
└── 导航回主页面
```
### 4.2 编辑习惯
```
用户点击习惯卡片标题区域
└── AppStateManager.goEditHabit(true)
└── EditHabitScreen(isNew: false, habit: currentHabit)
├── 表单预填充现有数据
├── 额外按钮:
│ ├── 归档/取消归档 (FAB 左侧)
│ └── 删除 (AppBar)
│ └── 直接删除,无确认对话框
└── 点击保存:
├── 更新 HabitData 字段
├── HabitsManager.editHabit(habit)
│ ├── HabitRepository.updateHabit() → UPDATE
│ ├── CategoryRepository.updateHabitCategories()
│ ├── NotificationService.reset()
│ └── notifyListeners()
└── 导航回主页面
```
### 4.3 删除习惯
```
用户在编辑页点击删除按钮
└── HabitsManager.deleteHabit(id)
├── 从内存列表中移除
├── HabitRepository.deleteHabit() → DELETE FROM habits WHERE id = ?
│ └── CASCADE DELETE events, habit_categories
├── NotificationService.removeNotifications(id)
├── HomeWidgetService.update()
├── UIFeedbackService.showMessageWithAction(
│ "Habit deleted.",
│ "Undo",
│ () => undoDelete()
│ )
└── notifyListeners()
```
### 4.4 归档/取消归档
```
归档:
HabitsManager.archiveHabit(id)
├── 更新 habit.archived = true
├── HabitRepository.updateHabit()
├── NotificationService.removeNotifications(id)
├── UIFeedbackService.showSuccess("Habit archived")
└── notifyListeners()
取消归档:
HabitsManager.unarchiveHabit(id)
├── 更新 habit.archived = false
├── HabitRepository.updateHabit()
├── 如果 habit.notification → NotificationService.reset()
├── UIFeedbackService.showSuccess("Habit unarchived")
└── notifyListeners()
```
### 4.5 拖拽排序
```
用户长按拖动习惯卡片到新位置
└── onReorder(oldIndex, newIndex)
├── 调整 newIndex (如果 oldIndex < newIndex → newIndex--)
├── 列表操作: list.removeAt(oldIndex), list.insert(newIndex, item)
├── 更新所有习惯的 position 字段
├── HabitsManager.reorderList(oldIndex, newIndex)
│ ├── 更新内存列表
│ ├── HabitRepository.updateHabitsOrder() → UPDATE position
│ └── notifyListeners()
└── UI 自动刷新
```
---
## 5. 数值型习惯进度输入
### 5.1 ProgressInputModal 交互
```
弹出 ProgressInputModal
├── 显示:
│ ├── 标题: "Save Progress"
│ ├── 圆形进度指示器 (120px 直径)
│ │ ├── 未完成: 显示百分比 (如 "70%")
│ │ ├── 已完成: 显示 ✓ 图标
│ │ └── 超出: 显示百分比 (> 100%)
│ ├── 当前值 / 目标值 显示
│ └── 控制按钮行:
│ ├── [-] 减少 partialValue
│ ├── [+] 增加 partialValue
│ └── [Complete] 直接设为 targetValue
├── 用户操作:
│ ├── 点击 [+]:
│ │ ├── currentValue += partialValue
│ │ └── clamp(0, targetValue * 2)
│ ├── 点击 [-]:
│ │ ├── currentValue -= partialValue
│ │ └── clamp(0, targetValue * 2)
│ ├── 点击 Complete:
│ │ └── currentValue = targetValue
│ ├── 点击当前值:
│ │ └── 弹出文本输入框直接编辑
│ └── 点击 Save:
│ ├── 确定 DayType:
│ │ ├── currentValue >= targetValue → DayType.check
│ │ └── currentValue < targetValue → DayType.progress
│ ├── 创建事件: [DayType, "", currentValue, targetValue]
│ └── 返回事件数据给调用方
└── 点击 Cancel → 返回 null
```
---
## 6. 日历组件行为
### 6.1 日历格式
```
月视图 → 显示整月
周视图 → 显示一周
切换触发:
├── 用户点击日历标题区域 → 切换格式
└── 月份切换时重置为月视图
```
### 6.2 日历日期标记
```
每个日期根据当天的 event[0] 显示不同颜色的圆点:
DayType.check → checkColor (默认 #09BF30 绿色)
DayType.fail → failColor (默认 #F44336 红色)
DayType.skip → skipColor (默认 #FBC02D 黄色)
DayType.progress → progressColor (默认 #2196F3 蓝色)
DayType.clear → 无标记
今天特殊显示:
→ 外圈高亮环
```
### 6.3 月份名称显示
```
如果 showMonthName 设置为 true:
→ 在日历上方显示当前月份名称文本
如果 false:
→ 不显示
```
---
## 7. 设置项完整规格
### 7.1 所有设置项及默认值
| 设置项 | 类型 | 默认值 | 持久化 Key |
|--------|------|--------|------------|
| theme | Themes 枚举 | Themes.device | theme |
| weekStart | StartingDayOfWeek | monday | weekStart |
| showDailyNot | bool | true | showDailyNot |
| dailyNotTime | TimeOfDay | 20:00 | dailyNotTime |
| soundEffects | bool | true | soundEffects |
| soundVolume | double | 3.0 (范围 0-5) | soundVolume |
| biometricLock | bool | false | biometricLock |
| oneTapCheck | bool | false | oneTapCheck |
| showMonthName | bool | true | showMonthName |
| showCategories | bool | true | showCategories |
| seenOnboarding | bool | false | seenOnboarding |
| lastWhatsNewVersion | String | '' | lastWhatsNewVersion |
| checkColor | Color | #09BF30 | checkColor |
| failColor | Color | #F44336 | failColor |
| skipColor | Color | #FBC02D | skipColor |
| progressColor | Color | #2196F3 | progressColor |
### 7.2 设置项分组 (UI 展示顺序)
```
外观 (Appearance):
├── Theme (下拉: Device / Light / Dark / OLED / Material You)
├── First day of the week (下拉: Su Mo Tu We Th Fr Sa)
├── Show month name (开关)
├── Show categories (开关)
└── Set colors (点击打开颜色选择器 × 4)
通知 (Notifications):
├── App notifications (开关) → 控制每日提醒
└── Notification time (时间选择器, 仅通知开启时可用)
音效 (Sound):
└── Sound effects (滑块 0-5, 左侧图标, 右侧数字)
安全 (Security):
├── Biometric Lock (开关, 需设备支持)
└── Single tap to check (开关)
数据管理:
├── Backup → Create (导出 JSON 文件)
├── Backup → Restore (导入 JSON 文件)
├── Onboarding (重播引导)
└── What's New (查看更新日志)
关于:
├── App 名称 + 版本号
├── Terms and Conditions (URL)
├── Privacy Policy (URL)
├── Source code (GitHub URL)
└── Support (捐赠 URL)
```
---
## 8. 备份/恢复流程
### 8.1 创建备份
```
用户点击 "Create" 备份按钮
└── BackupService.createDatabaseBackup()
├── 1. 从 HabitsManager 获取所有习惯
├── 2. 序列化每个习惯:
│ ├── id, position, title, twoDayRule, cue, routine, reward
│ ├── showReward, advanced, notification, notTime
│ ├── sanction, showSanction, accountant
│ ├── habitType, targetValue, partialValue, unit
│ └── events: { "YYYY-MM-DD": [dayTypeIndex, ...] }
├── 3. 序列化分类:
│ └── categories: [{ id, title, iconCodePoint, fontFamily }]
├── 4. 序列化关联:
│ └── habit_categories: [{ habit_id, category_id }]
├── 5. 添加元数据:
│ └── metadata: { import_timestamp, version }
├── 6. JSON.encode → 字符串
├── 7. 弹出文件保存对话框 (flutter_file_dialog / file_picker)
└── 8. 写入文件
├── 成功 → UIFeedbackService.showSuccess("Backup created successfully!")
└── 失败 → UIFeedbackService.showError("Backup failed!")
```
### 8.2 恢复备份
```
用户点击 "Restore" 备份按钮
└── 弹出确认对话框: "All habits will be replaced with habits from backup."
├── Cancel → 取消
└── Restore →
├── BackupService.loadBackup()
│ │
│ ├── 1. 弹出文件选择对话框
│ ├── 2. 读取文件内容
│ ├── 3. 验证:
│ │ ├── 文件是否存在
│ │ ├── 文件大小 <= 10MB
│ │ └── JSON 格式是否有效
│ ├── 4. 解析 JSON:
│ │ ├── 如果是数组 → 旧版格式,转换为新格式
│ │ └── 如果是对象 → 新版格式
│ ├── 5. 清空数据库:
│ │ ├── DELETE FROM habits
│ │ └── DELETE FROM events
│ ├── 6. 逐个插入习惯和事件
│ └── 7. 插入分类和关联
└── 成功后:
├── HabitsManager.loadHabits() ← 重新加载
├── NotificationService.reset() ← 重置通知
├── HomeWidgetService.update() ← 更新小组件
└── UIFeedbackService.showSuccess("Restore completed successfully!")
```
---
## 9. 引导页流程
```
首次启动应用
└── SettingsManager.seenOnboarding == false
└── 显示 OnboardingScreen
├── Step 1: "Define your habits"
│ ├── 插图: empty_list.svg
│ ├── 描述: "To better stick to your habits, you can define:"
│ ├── 概念 1: Cue (提示触发器)
│ ├── 概念 2: Routine (例行动作)
│ └── 概念 3: Reward (奖励)
├── Step 2: "Log your days"
│ ├── 插图: habit_tracking.svg
│ └── 每日操作:
│ ├── ✓ Successful (完成)
│ ├── + Progressive (进度)
│ ├── ✗ Not so successful (失败)
│ ├── ⏭ Skip (跳过)
│ └── 💬 Note (备注)
├── Step 3: "Observe your progress"
│ ├── 插图: progress.svg
│ └── 描述: "You can track your progress through the calendar
│ view in every habit or on the statistics page."
├── 导航: Skip (跳过) / Next (下一步) / Done (完成)
└── 完成:
├── SettingsManager.seenOnboarding = true
├── SettingsManager.saveData()
└── 导航到 HabitsScreen
```
---
## 10. 生物识别认证流程
```
应用启动 或 从后台恢复 (AppLifecycleState.resumed)
└── BiometricAuthWrapper
├── 检查 biometricLock 设置
│ ├── false → 直接显示内容
│ └── true →
│ │
│ ├── BiometricAuthService.authenticate()
│ │ ├── 获取可用生物识别类型
│ │ │ ├── 指纹 → "Fingerprint"
│ │ │ ├── 面容 → "Face ID"
│ │ │ ├── 虹膜 → "Iris"
│ │ │ └── 设备凭据 → "Device PIN, Pattern, or Password"
│ │ │
│ │ └── local_auth.authenticate(
│ │ localizedReason: "Please authenticate to access Habo",
│ │ biometricOnly: false
│ │ )
│ │
│ ├── 成功 → 显示内容
│ │
│ └── 失败 → 显示认证错误界面:
│ ├── 标题: "Authentication Required"
│ ├── 描述: "Please authenticate to access Habo"
│ ├── 图标: 指纹图标
│ └── "Try Again" 按钮 → 重新认证
└── 设备不支持生物识别:
├── showToast: "Please set up fingerprint/face unlock"
└── 自动关闭 biometricLock 设置
```
---
## 11. 桌面小组件数据更新
```
习惯事件变化时
└── HomeWidgetService.update()
├── 1. 计算今日习惯进度:
│ ├── 获取所有活跃习惯
│ ├── 计算今日已完成数 (DayType.check 或 progress >= target)
│ └── 计算总习惯数
├── 2. 写入小组件数据:
│ ├── HomeWidget.saveWidgetData<int>("habitsCompleted", count)
│ ├── HomeWidget.saveWidgetData<int>("habitsTotal", total)
│ └── HomeWidget.updateWidget()
└── 3. 小组件渲染:
├── CircularProgressPainter
│ └── 弧度 = (completed / total) * 2π
├── 中心显示: "completed / total"
└── 尺寸: 170 × 170
```
---
## 12. 通知调度逻辑
```
NotificationService.resetNotifications():
├── 1. 取消所有现有通知:
│ └── AwesomeNotifications().cancelAll()
├── 2. 检查全局通知开关:
│ └── if !showDailyNot → return
└── 3. 遍历所有活跃习惯:
└── if habit.notification:
├── 创建每日重复通知:
│ ├── id: habit.id
│ ├── channel: "habit_notifications"
│ ├── title: "Habo"
│ ├── body: "Do not forget to check your habits."
│ ├── schedule: 每日 at habit.notTime
│ └── payload: { habitId: habit.id }
└── AwesomeNotifications().createNotification()
```
---
## 13. 完整数据库 Schema
### 13.1 habits 表
```sql
CREATE TABLE habits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
position INTEGER, -- 排序权重
title TEXT NOT NULL, -- 习惯标题 (必填)
twoDayRule INTEGER DEFAULT 0, -- 两天法则 0=关 1=开
cue TEXT DEFAULT '', -- 提示触发器
routine TEXT DEFAULT '', -- 例行动作
reward TEXT DEFAULT '', -- 奖励
showReward INTEGER DEFAULT 0, -- 显示奖励 0=关 1=开
advanced INTEGER DEFAULT 0, -- 高级模式 0=关 1=开
notification INTEGER DEFAULT 0, -- 通知开关 0=关 1=开
notTime TEXT DEFAULT '', -- 通知时间 "HH:MM"
sanction TEXT DEFAULT '', -- 惩罚描述
showSanction INTEGER DEFAULT 0, -- 显示惩罚 0=关 1=开
accountant TEXT DEFAULT '', -- 问责伙伴
habitType INTEGER DEFAULT 0, -- 0=布尔 1=数值
targetValue REAL DEFAULT 1.0, -- 目标值
partialValue REAL DEFAULT 1.0, -- 部分增量
unit TEXT DEFAULT '', -- 单位
archived INTEGER DEFAULT 0 -- 0=活跃 1=归档
);
```
### 13.2 events 表
```sql
CREATE TABLE events (
id INTEGER NOT NULL, -- FK → habits.id
dateTime TEXT NOT NULL, -- ISO8601 日期字符串
dayType INTEGER NOT NULL, -- 0=clear 1=check 2=fail 3=skip 4=progress
comment TEXT DEFAULT '', -- 备注
progressValue REAL DEFAULT 0.0, -- 进度值 (数值型)
targetValue REAL DEFAULT 0.0, -- 目标值快照 (数值型)
PRIMARY KEY (id, dateTime),
FOREIGN KEY (id) REFERENCES habits(id) ON DELETE CASCADE
);
```
### 13.3 categories 表
```sql
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL, -- 分类名称
iconCodePoint INTEGER NOT NULL, -- IconData.codePoint
fontFamily TEXT -- 字体族 (如 fontAwesomeFlutter)
);
```
### 13.4 habit_categories 关联表
```sql
CREATE TABLE habit_categories (
habit_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
PRIMARY KEY (habit_id, category_id),
FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);
```
---
## 14. 跨日自动刷新
```
应用启动时:
└── _startDayChangeTimer()
└── Timer.periodic(Duration(hours: 1), callback)
├── 记录当前日期: lastDate = DateTime.now().day
└── 每小时检查:
├── if DateTime.now().day != lastDate:
│ ├── lastDate = DateTime.now().day
│ ├── HabitsManager.loadHabits() ← 重载数据
│ ├── NotificationService.reset() ← 重置通知
│ └── HomeWidgetService.update() ← 更新小组件
└── else → 无操作
应用暂停时:
└── _stopDayChangeTimer() ← 暂停定时器
应用恢复时:
└── 检查日期变化 → 刷新 → _startDayChangeTimer() ← 恢复定时器
```
---
## 15. 颜色选择器交互
```
用户点击颜色设置项
└── 弹出 ColorIcon 对话框
├── 显示 HueRingPicker:
│ ├── 色相环 (360°)
│ └── 饱和度/亮度选择区域
├── 当前选中颜色的实时预览
├── 重置按钮 → 恢复默认颜色
└── 确认 → SettingsManager 更新颜色值
├── SharedPreferences 保存 (ARGB int)
└── notifyListeners() → 全局主题刷新
```

547
docs/04-ASSETS.md Normal file
View File

@@ -0,0 +1,547 @@
# Habo 可复用素材清单
> 从原项目提取的静态资源、国际化文本、测试用例,作为 AI 复刻项目的起点素材
---
## 1. 静态资源清单
### 1.1 图片资源 (`assets/images/`)
| 文件 | 用途 | 格式 |
|------|------|------|
| `icon.png` | Android 应用图标 | PNG |
| `ios_icon.jpg` | iOS 应用图标 | JPG |
| `macos_icon.png` | macOS 应用图标 | PNG |
| `app_icon.png` | 通用应用图标 | PNG |
| `splash_icon.png` | 启动画面图标 | PNG |
| `splash_icon2.png` | 备用启动图标 | PNG |
| `android_foreground.png` | Android 自适应图标前景 | PNG |
| `android_background.png` | Android 自适应图标背景 | PNG |
| `android_monochrome.svg` | Android 单色图标 | SVG |
| `emptyList.svg` | 空列表占位图 | SVG |
| `noDataStatistics.svg` | 统计页空数据占位图 | SVG |
### 1.2 引导页图片 (`assets/images/onboard/`)
| 文件 | 用途 |
|------|------|
| `1.svg` | 第 1 步: 定义习惯 (空列表插图) |
| `2.svg` | 第 2 步: 记录天数 (习惯追踪插图) |
| `3.svg` | 第 3 步: 观察进步 (进度追踪插图) |
### 1.3 音效资源 (`assets/sounds/`)
| 文件 | 用途 | 说明 |
|------|------|------|
| `check.wav` | 打卡完成音效 | 成功完成的正向音效 |
| `click.wav` | 通用点击音效 | 失败/跳过等操作的反馈音效 |
| `sound_sources.txt` | 音效来源说明 | 开源协议信息 |
### 1.4 字体资源 (`assets/google_fonts/`)
**字体族**: Nunito18 个字重/样式变体)
| 文件 | 字重 |
|------|------|
| Nunito-ExtraLight.ttf | 200 |
| Nunito-Light.ttf | 300 |
| Nunito-Regular.ttf | 400 |
| Nunito-Medium.ttf | 500 |
| Nunito-SemiBold.ttf | 600 |
| Nunito-Bold.ttf | 700 |
| Nunito-ExtraBold.ttf | 800 |
| Nunito-Black.ttf | 900 |
| 以及对应的 Italic 变体 | |
许可证: OFL (SIL Open Font License)
---
## 2. 国际化文本 (英文基准 ARB)
> 完整的 `intl_en.arb`,包含 **354 个键**,可直接复制为项目的国际化基准文件
```json
{
"@@locale": "en",
"habits": "Habits:",
"statistics": "Statistics",
"emptyList": "Empty list",
"noDataAboutHabits": "There is no data about habits.",
"topStreak": "Top streak",
"currentStreak": "Current streak",
"total": "Total",
"unknown": "Unknown",
"warning": "Warning",
"allHabitsWillBeReplaced": "All habits will be replaced with habits from backup.",
"restore": "Restore",
"cancel": "Cancel",
"settings": "Settings",
"theme": "Theme",
"firstDayOfWeek": "First day of the week",
"notifications": "Notifications",
"notificationTime": "Notification time",
"soundEffects": "Sound effects",
"showMonthName": "Show month name",
"setColors": "Set colors",
"backup": "Backup",
"create": "Create",
"onboarding": "Onboarding",
"about": "About",
"habo": "Habo",
"copyright": "©2023 Habo",
"termsAndConditions": "Terms and Conditions",
"privacyPolicy": "Privacy Policy",
"disclaimer": "Disclaimer",
"sourceCode": "Source code (GitHub)",
"ifYouWantToSupport": "If you want to support Habo you can:",
"buyMeACoffee": "Buy me a coffee",
"reset": "Reset",
"done": "Done",
"congratulationsReward": "Congratulations! Your reward:",
"ohNoSanction": "Oh no! Your sanction:",
"month": "Month",
"week": "Week",
"habitLoop": "Habit loop",
"habitLoopDescription": "Habit Loop is a psychological model describing the process of habit formation. It consists of three components: Cue, Routine, and Reward. The Cue triggers the Routine (habitual action), which is then reinforced by the Reward, creating a loop that makes the habit more ingrained and likely to be repeated.",
"cue": "Cue",
"cueDescription": "is the trigger that initiates your habit. It could be a specific time, location, feeling, or an event.",
"routine": "Routine",
"routineDescription": "is the action you take in response to the cue. This is the habit itself.",
"reward": "Reward",
"rewardDescription": "is the benefit or positive feeling you experience after performing the routine. It reinforces the habit.",
"editHabit": "Edit Habit",
"createHabit": "Create Habit",
"delete": "Delete",
"habitTitleEmptyError": "The habit title can not be empty.",
"save": "Save",
"exercise": "Exercise",
"habit": "Habit",
"useTwoDayRule": "Use Two day rule",
"twoDayRule": "Two day rule",
"twoDayRuleDescription": "With two day rule, you can miss one day and do not lose a streak if the next day is successful.",
"advancedHabitBuilding": "Advanced habit building",
"advancedHabitBuildingDescription": "This section helps you better define your habits utilizing the Habit loop. You should define cues, routines, and rewards for every habit.",
"at7AM": "At 7:00AM",
"do50PushUps": "Do 50 push ups",
"fifteenMinOfVideoGames": "15 min. of video games",
"showReward": "Show reward",
"remainderOfReward": "The reminder of the reward after a successful routine.",
"habitContract": "Habit contract",
"habitContractDescription": "While positive reinforcement is recommended, some people may opt for a habit contract. A habit contract allows you to specify a sanction that will be imposed if you miss your habit, and may involve an accountability partner who helps supervise your goals.",
"donateToCharity": "Donate 10$ to charity",
"sanction": "Sanction",
"showSanction": "Show sanction",
"remainderOfSanction": "The reminder of the sanction after a unsuccessful routine.",
"dan": "Dan",
"accountabilityPartner": "Accountability partner",
"add": "Add",
"haboNeedsPermission": "Habo needs permission to send notifications to work properly.",
"allow": "Allow",
"date": "Date",
"check": "Check",
"fail": "Fail",
"skip": "Skip",
"note": "Note",
"yourCommentHere": "Your note here",
"close": "Close",
"createYourFirstHabit": "Create your first habit.",
"modify": "Modify",
"backupFailedError": "ERROR: Creating backup failed.",
"restoreFailedError": "ERROR: Restoring backup failed.",
"habitDeleted": "Habit deleted.",
"undo": "Undo",
"appNotifications": "App notifications",
"appNotificationsChannel": "Notification channel for application notifications",
"habitNotifications": "Habit notifications",
"habitNotificationsChannel": "Notification channel for habit notifications",
"doNotForgetToCheckYourHabits": "Do not forget to check your habits.",
"themeSelect": "{theme, select, device {Device} light {Light} dark {Dark} oled {OLED black} materialYou {Material You} other{Device}}",
"@themeSelect": {
"placeholders": {
"theme": {
"type": "String"
}
}
},
"defineYourHabits": "Define your habits",
"defineYourHabitsDescription": "To better stick to your habits, you can define:",
"cueNumbered": "1. Cue",
"routineNumbered": "2. Routine",
"rewardNumbered": "3. Reward",
"logYourDays": "Log your days",
"successful": "Successful",
"notSoSuccessful": "Not so successful",
"skipDoesNotAffectStreaks": "Skip (does not affect streaks)",
"observeYourProgress": "Observe your progress",
"trackYourProgress": "You can track your progress through the calendar view in every habit or on the statistics page.",
"backupCreatedSuccessfully": "Backup created successfully!",
"backupFailed": "Backup failed!",
"restoreCompletedSuccessfully": "Restore completed successfully!",
"restoreFailed": "Restore failed!",
"fileNotFound": "File not found",
"fileTooLarge": "File too large (max 10MB)",
"invalidBackupFile": "Invalid backup file",
"progress": "Progress",
"enterAmount": "Enter amount",
"complete": "Complete",
"saveProgress": "Save Progress",
"currentProgress": "Current: {current} {unit}",
"@currentProgress": {
"placeholders": {
"current": { "type": "String" },
"unit": { "type": "String" }
}
},
"targetProgress": "Target: {target} {unit}",
"@targetProgress": {
"placeholders": {
"target": { "type": "String" },
"unit": { "type": "String" }
}
},
"progressOf": "{current} / {target} {unit}",
"@progressOf": {
"placeholders": {
"current": { "type": "String" },
"target": { "type": "String" },
"unit": { "type": "String" }
}
},
"numericHabit": "Progressive",
"targetValue": "Target value",
"partialValue": "Partial value",
"unit": "Unit",
"habitType": "Habit type",
"booleanHabit": "Checkable (Yes/No)",
"slider": "Slider",
"input": "Input",
"numericHabitDescription": "Numeric habits let you track progress in increments throughout the day.",
"partialValueDescription": "To track progress in smaller increments",
"categories": "Categories",
"addCategory": "Add Category",
"editCategory": "Edit Category",
"category": "Category",
"noCategoriesYet": "No categories yet",
"createFirstCategory": "Create your first category to organize your habits",
"pleaseEnterCategoryTitle": "Please enter a category title",
"categoryAlreadyExists": "Category \"{title}\" already exists",
"@categoryAlreadyExists": { "placeholders": { "title": { "type": "String" } } },
"categoryCreatedSuccessfully": "Category \"{title}\" created successfully",
"@categoryCreatedSuccessfully": { "placeholders": { "title": { "type": "String" } } },
"categoryUpdatedSuccessfully": "Category \"{title}\" updated successfully",
"@categoryUpdatedSuccessfully": { "placeholders": { "title": { "type": "String" } } },
"categoryDeletedSuccessfully": "Category \"{title}\" deleted successfully",
"@categoryDeletedSuccessfully": { "placeholders": { "title": { "type": "String" } } },
"failedToSaveCategory": "Failed to save category: {error}",
"@failedToSaveCategory": { "placeholders": { "error": { "type": "String" } } },
"failedToDeleteCategory": "Failed to delete category: {error}",
"@failedToDeleteCategory": { "placeholders": { "error": { "type": "String" } } },
"selectCategories": "Select Categories",
"selectedCategories": "Selected Categories ({count})",
"@selectedCategories": { "placeholders": { "count": { "type": "int" } } },
"allCategories": "All Categories",
"deleteCategory": "Delete Category",
"deleteCategoryConfirmation": "Are you sure you want to delete \"{title}\"?\n\nThis will remove the category from all habits that use it.",
"@deleteCategoryConfirmation": { "placeholders": { "title": { "type": "String" } } },
"noHabitsInCategory": "No habits in \"{title}\"",
"@noHabitsInCategory": { "placeholders": { "title": { "type": "String" } } },
"createHabitForCategory": "Create a habit and assign it to this category",
"showCategories": "Show Categories",
"archive": "Archive",
"unarchive": "Unarchive",
"archiveHabit": "Archive habit",
"unarchiveHabit": "Unarchive habit",
"archivedHabits": "Archived Habits",
"noArchivedHabits": "No archived habits",
"viewArchivedHabits": "View archived habits",
"habitArchived": "Habit archived",
"habitUnarchived": "Habit unarchived",
"biometric": "Biometric",
"biometricLockEnabled": "Biometric lock enabled",
"biometricLockDisabled": "Biometric lock disabled",
"authenticationError": "Authentication error",
"biometricAuthenticationRequired": "Biometric authentication required",
"setupFingerprintFaceUnlock": "Please set up your fingerprint or face unlock in device settings",
"touchSensor": "Touch sensor",
"biometricNotRecognized": "Biometric not recognized, try again",
"biometricRequired": "Biometric required",
"biometricAuthenticationSucceeded": "Biometric authentication succeeded",
"deviceCredentialsRequired": "Device credentials required",
"setupDeviceCredentials": "Please set up device credentials in settings",
"setupTouchIdFaceId": "Please set up your Touch ID or Face ID in device settings",
"reenableTouchIdFaceId": "Please reenable your Touch ID or Face ID",
"biometricLock": "Biometric Lock",
"biometricLockDescription": "Secure app with {authMethod}",
"@biometricLockDescription": { "placeholders": { "authMethod": { "type": "String" } } },
"authenticateToEnable": "Authenticate to enable biometric lock",
"authenticateToAccess": "Please authenticate to access Habo",
"authenticationRequired": "Authentication Required",
"authenticationFailedMessage": "Please authenticate to access Habo using {authMethod}",
"@authenticationFailedMessage": { "placeholders": { "authMethod": { "type": "String" } } },
"tryAgain": "Try Again",
"authenticating": "Authenticating…",
"authenticate": "Authenticate",
"buildingBetterHabits": "Building Better Habits",
"authenticationPrompt": "Please authenticate using {authMethod} to access your habits",
"@authenticationPrompt": { "placeholders": { "authMethod": { "type": "String" } } },
"devicePinPatternPassword": "Device PIN, Pattern, or Password",
"fingerprint": "Fingerprint",
"iris": "Iris",
"whatsNewTitle": "What's New",
"whatsNewVersion": "Version {version}",
"@whatsNewVersion": { "placeholders": { "version": { "type": "String" } } },
"featureNumericTitle": "Numeric values in habits",
"featureNumericDesc": "Track counts like glasses of water or pages read",
"featureDeepLinksTitle": "URL scheme (deep links)",
"featureDeepLinksDesc": "Open Habo directly to screens like settings or create",
"featureCategoriesTitle": "Categories",
"featureCategoriesDesc": "Organize habits with category filters",
"featureArchiveTitle": "Archive",
"featureArchiveDesc": "Hide habits you no longer track without deleting",
"featureMaterialYouTitle": "Material You theme (Android)",
"featureMaterialYouDesc": "Dynamic colors that match your wallpaper",
"featureSoundTitle": "New sound engine",
"featureSoundDesc": "Adjustable volume",
"featureLockTitle": "Lock feature",
"featureLockDesc": "Secure the app with Face ID / Touch ID / biometrics",
"featureIosSoundMixingTitle": "Fixed sound mixing",
"featureIosSoundMixingDesc": "Habo sounds no longer interrupt your music or podcasts",
"featureHomescreenWidgetTitle": "Homescreen widget",
"featureHomescreenWidgetDesc": "View your habit progress at a glance from your home screen (experimental)",
"featureLongpressCheckTitle": "Longpress check",
"featureLongpressCheckDesc": "Longpress on habit buttons to quickly change status",
"haboSyncComingSoon": "Coming Soon",
"haboSyncDescription": "Sync your habits across all your devices with Habo's end-to-end encrypted cloud service.",
"haboSyncLearnMore": "Learn more at habo.space/sync",
"habitsToday": "Habits today",
"or": "or",
"oneTapCheck": "Single tap to check",
"tapCheckLongPressMenu": "Tap to check, long press for menu",
"categoryName": "Category name",
"createCategory": "Create category",
"all": "All",
"selectIcon": "Pick an icon",
"searchIcons": "Search"
}
```
---
## 3. 支持的语言 (27 种)
| 代码 | 语言 |
|------|------|
| `en` | English (基准) |
| `zh_Hans` | 中文简体 |
| `zh_Hant` | 中文繁體 |
| `es` | Español |
| `fr` | Français |
| `de` | Deutsch |
| `it` | Italiano |
| `pt` | Português |
| `pt_BR` | Português (Brasil) |
| `ru` | Русский |
| `ja` | (可能缺失, 需确认) |
| `ar` | العربية |
| `he` | עברית |
| `pl` | Polski |
| `nl` | Nederlands |
| `sv` | Svenska |
| `cs` | Čeština |
| `sk` | Slovenčina |
| `uk` | Українська |
| `vi` | Tiếng Việt |
| `id` | Bahasa Indonesia |
| `tr` | Türkçe |
| `ca` | Català |
| `ta` | தமிழ் |
| `nb_NO` | Norsk bokmål |
| `eo` | Esperanto |
| `ia` | Interlingua |
| `ckb` | کوردی |
---
## 4. 测试用例清单
> 13 个测试文件,覆盖核心业务逻辑
### 4.1 单元测试
| 文件 | 测试目标 | 测试用例数 |
|------|----------|-----------|
| `test/habits/habits_manager_test.dart` | HabitsManager CRUD | 13 |
| `test/habits/habits_manager_updated_test.dart` | Repository 模式集成 | 8 |
| `test/habits/habits_manager_fixed_test.dart` | 归档功能 | 8 |
| `test/habits/habits_manager_notifications_test.dart` | 通知调度 | 3 |
| `test/habits/backup_enhancement_test.dart` | 备份格式 | 4 |
| `test/services/backup_service_test.dart` | 备份服务 | 7 |
| `test/services/backup_feature_comprehensive_test.dart` | 备份完整性 | 18 |
| `test/services/notification_service_test.dart` | 通知服务 | 11 |
| `test/app_test.dart` | 应用初始化 | 4 |
| `test/repositories/repository_test.dart` | Repository 模式 | 6 |
### 4.2 Widget 测试
| 文件 | 测试目标 | 测试用例数 |
|------|----------|-----------|
| `test/widgets/habit_details_widget_test.dart` | 习惯详情组件 | 3 |
| `test/widgets/habit_list_widget_test.dart` | 习惯列表组件 | 2 |
### 4.3 集成测试
| 文件 | 测试目标 | 测试用例数 |
|------|----------|-----------|
| `test/integration/habit_crud_integration_test.dart` | 完整 CRUD 流程 | 2 |
### 4.4 测试 Mock 基础设施
| 文件 | 内容 |
|------|------|
| `test/mocks/mock_repositories.dart` | MockHabitRepository, MockEventRepository, MockCategoryRepository, MockBackupRepository + InMemory 实现用于集成测试 |
### 4.5 关键测试场景摘要
**HabitsManager 测试**:
- 初始化时从 Repository 加载习惯
- 空列表正确处理
- 创建习惯并分配正确位置
- 编辑更新已有习惯
- 删除习惯并支持 Undo
- 归档/取消归档切换
- 活跃/归档习惯正确过滤
- 位置排序更新
- 通知调度触发
**备份测试**:
- 正确的 JSON 结构验证
- 时间戳格式验证
- 空数据备份处理
- 事件类型保留
- 分类关联保留
- 大数据集处理
- 损坏数据检测
- 10MB 文件大小限制
- 并发操作处理
**通知测试**:
- 空习惯列表不崩溃
- 习惯通知调度
- 事件添加/删除触发通知
- 多习惯批量通知
- 当日/非当日事件区分
---
## 5. 备份文件格式规格
### 5.1 当前格式 (Version 3)
```json
{
"version": 3,
"habits": [
{
"id": 1,
"position": 0,
"title": "Exercise",
"twoDayRule": false,
"cue": "At 7:00AM",
"routine": "Do 50 push ups",
"reward": "15 min. of video games",
"showReward": true,
"advanced": true,
"notification": true,
"notTime": {"hour": 7, "minute": 0},
"events": {
"2024-01-01 00:00:00.000": [1, ""],
"2024-01-02 00:00:00.000": [2, "Tired"]
},
"sanction": "Donate 10$ to charity",
"showSanction": true,
"accountant": "Dan",
"habitType": 0,
"targetValue": 1.0,
"partialValue": 1.0,
"unit": ""
}
],
"categories": [
{ "id": 1, "title": "Health", "iconCodePoint": 58718, "fontFamily": "fontAwesomeFlutter" }
],
"habit_categories": [
{ "habit_id": 1, "category_id": 1 }
],
"metadata": {
"import_timestamp": "2024-01-15T10:30:00.000Z"
}
}
```
### 5.2 旧版兼容格式 (数组)
```json
[
{
"id": 1,
"title": "Exercise",
"position": 0,
"events": {},
...
}
]
```
### 5.3 事件类型编码
| 值 | DayType | 含义 |
|----|---------|------|
| 0 | clear | 清除/无事件 |
| 1 | check | 完成 |
| 2 | fail | 失败 |
| 3 | skip | 跳过 |
| 4 | progress | 进度(部分完成) |
---
## 6. pubspec.yaml 关键配置
```yaml
name: habo
version: 3.1.2+5115
environment:
sdk: ">=3.11.0 <4.0.0"
flutter: ">=3.41.1"
flutter:
uses-material-design: true
generate: true
assets:
- assets/
- assets/images/
- assets/images/onboard/
- assets/sounds/
- assets/google_fonts/
flutter_intl:
enabled: true
flutter_native_splash:
color: "#FAFAFA"
color_dark: "#000000"
image: assets/images/splash_icon.png
image_dark: assets/images/splash_icon.png
ios_content_mode: center
android_gravity: center
fullscreen: false
android_12:
color: "#FAFAFA"
color_dark: "#000000"
image: assets/images/splash_icon.png
android: true
ios: true
web: false
```

687
docs/05-DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,687 @@
# Habo 开发文档
> 版本: 3.1.2+5115 | Flutter 3.41.1+ | Dart 3.11.0+
Habo 是一款极简风格的习惯追踪应用,支持 Android、iOS、Linux、macOS 多平台。所有数据存储在本地 SQLite 数据库中,无需服务端。
---
## 目录
1. [项目概览](#1-项目概览)
2. [技术栈与依赖](#2-技术栈与依赖)
3. [目录结构](#3-目录结构)
4. [架构设计](#4-架构设计)
5. [数据模型](#5-数据模型)
6. [数据库 Schema](#6-数据库-schema)
7. [核心模块详解](#7-核心模块详解)
8. [导航系统](#8-导航系统)
9. [状态管理](#9-状态管理)
10. [国际化 (i18n)](#10-国际化-i18n)
11. [主题系统](#11-主题系统)
12. [通知系统](#12-通知系统)
13. [备份与恢复](#13-备份与恢复)
14. [桌面端小组件](#14-桌面端小组件)
15. [生物识别认证](#15-生物识别认证)
16. [CI/CD 与构建](#16-cicd-与构建)
17. [开发指南](#17-开发指南)
---
## 1. 项目概览
Habo 是一个功能完整的习惯追踪应用,核心功能包括:
- **习惯管理** — 创建、编辑、归档、删除习惯,支持拖拽排序
- **两种习惯类型** — 布尔型(打卡/未打卡)和数值型(进度追踪,如跑步 5km
- **日历视图** — 基于 `table_calendar` 的月度视图,标记每日状态
- **连续天数 (Streak)** — 支持普通模式和"两天法则"(允许间隔一天)
- **分类系统** — 习惯可归属多个分类,支持按分类筛选
- **统计分析** — 饼图总览、月度柱状图、个人习惯统计卡片
- **通知提醒** — 每日提醒和成就/惩罚通知
- **备份恢复** — JSON 文件导入/导出,支持跨设备迁移
- **桌面小组件** — iOS/Android 主屏幕小组件显示今日进度
- **生物识别锁** — 支持指纹/面容锁定应用
- **Material You** — 支持动态取色主题
---
## 2. 技术栈与依赖
| 类别 | 技术 | 说明 |
|------|------|------|
| 框架 | Flutter 3.41.1+ | 跨平台 UI 框架 |
| 语言 | Dart 3.11.0+ | 支持 null safety |
| 数据库 | sqflite / sqflite_common_ffi | SQLite移动端/桌面端) |
| 状态管理 | provider + ChangeNotifier | 响应式状态管理 |
| 图表 | fl_chart | 统计图表渲染 |
| 日历 | table_calendar | 日历视图组件 |
| 通知 | awesome_notifications | 本地通知调度 |
| 国际化 | flutter_localizations + intl | ARB 文件管理多语言 |
| 字体 | google_fonts + 动态取色 | dynamic_color (Material You) |
| 音效 | flutter_soloud + audio_session | 习惯完成音效反馈 |
| 认证 | local_auth | 指纹/面容生物识别 |
| 小组件 | home_widget | iOS/Android 桌面小组件 |
| 桌面窗口 | window_manager | Linux/macOS 窗口管理 |
| 测试 | flutter_test + mocktail | 单元测试与 mock |
| CI | GitHub Actions | 自动测试与 APK 构建 |
| 发布 | fastlane | 多平台商店发布自动化 |
---
## 3. 目录结构
```
Habo-master/
├── lib/ # 应用主源码
│ ├── main.dart # 应用入口,初始化流程
│ ├── constants.dart # 枚举类型和颜色常量
│ ├── themes.dart # 主题定义(亮色/暗色/OLED
│ ├── helpers.dart # 工具函数(日期解析等)
│ │
│ ├── model/ # 数据模型层
│ │ ├── habit_data.dart # HabitData 习惯数据模型
│ │ ├── habo_model.dart # HaboModel 数据库操作层
│ │ ├── category.dart # Category 分类模型
│ │ ├── settings_data.dart # 设置数据模型
│ │ └── backup.dart # 备份数据模型
│ │
│ ├── habits/ # 习惯管理模块
│ │ ├── habit.dart # Habit StatefulWidget日历卡片
│ │ ├── habits_manager.dart # HabitsManager 业务逻辑中心
│ │ ├── habits_screen.dart # 习惯列表主屏幕
│ │ ├── create_habit.dart # 创建习惯页面
│ │ └── edit_habit.dart # 编辑习惯页面
│ │
│ ├── statistics/ # 统计分析模块
│ │ ├── statistics.dart # 统计数据计算逻辑
│ │ ├── statistics_screen.dart # 统计主屏幕
│ │ ├── statistics_card.dart # 单个习惯统计卡片
│ │ ├── overall_statistics_card.dart # 总览饼图卡片
│ │ └── monthly_graph.dart # 月度柱状图
│ │
│ ├── settings/ # 设置模块
│ │ ├── settings_manager.dart # 设置管理器(持久化)
│ │ ├── settings_screen.dart # 设置页面 UI
│ │ └── color_icon.dart # 颜色选择器组件
│ │
│ ├── navigation/ # 导航系统
│ │ ├── routes.dart # 路由常量
│ │ ├── app_router.dart # RouterDelegate 实现
│ │ ├── app_state_manager.dart # 导航状态管理
│ │ ├── route_information_parser.dart # 深度链接解析
│ │ └── navigation.dart # 导出文件
│ │
│ ├── repositories/ # 数据仓库层Repository Pattern
│ │ ├── habit_repository.dart # 习惯仓库接口
│ │ ├── sqlite_habit_repository.dart # SQLite 实现
│ │ ├── event_repository.dart # 事件仓库接口
│ │ ├── category_repository.dart # 分类仓库接口
│ │ └── repository_factory.dart # 仓库工厂DI
│ │
│ ├── services/ # 服务层
│ │ ├── service_locator.dart # 服务定位器DI 容器)
│ │ ├── notification_service.dart # 通知服务
│ │ ├── backup_service.dart # 备份/恢复服务
│ │ ├── ui_feedback_service.dart # UI 反馈服务Snackbar
│ │ ├── biometric_auth_service.dart # 生物识别服务
│ │ └── home_widget_service.dart # 桌面小组件服务
│ │
│ ├── widgets/ # 可复用 UI 组件
│ │ ├── habit_progress_indicator.dart # 进度指示器
│ │ ├── biometric_auth_wrapper.dart # 生物识别包裹组件
│ │ ├── category_filter_row.dart # 分类筛选行
│ │ ├── progress_input_modal.dart # 数值进度输入弹窗
│ │ ├── home_widget_data.dart # 小组件数据模型
│ │ ├── habo_home_widget.dart # 小组件渲染
│ │ └── text_container.dart # 文本输入组件
│ │
│ ├── onboarding/ # 引导页
│ │ ├── onboarding_screen.dart
│ │ └── onboarding.dart
│ │
│ ├── l10n/ # 国际化 ARB 文件27 种语言)
│ └── generated/ # 自动生成的代码intl 等)
├── test/ # 测试目录
├── assets/ # 静态资源
│ ├── images/ # 图片(含 onboard/ 引导图)
│ ├── sounds/ # 音效文件
│ └── google_fonts/ # 本地字体文件
├── android/ # Android 平台代码
├── ios/ # iOS 平台代码
├── linux/ # Linux 平台代码
├── macos/ # macOS 平台代码
├── fastlane/ # 发布自动化配置
├── .github/workflows/ci.yml # CI/CD 流水线
└── pubspec.yaml # 项目配置和依赖声明
```
---
## 4. 架构设计
### 4.1 整体架构
```
┌─────────────────────────────────────────────────────┐
│ Presentation Layer │
│ (Screens, Widgets) │
│ habits_screen statistics_screen settings_screen │
├─────────────────────────────────────────────────────┤
│ Business Logic Layer │
│ HabitsManager (ChangeNotifier) │
│ SettingsManager │
├─────────────────────────────────────────────────────┤
│ Service Layer │
│ NotificationService BackupService UIFeedbackService│
│ BiometricAuthService HomeWidgetService │
├─────────────────────────────────────────────────────┤
│ Repository Layer │
│ HabitRepository EventRepository CategoryRepository│
│ RepositoryFactory │
├─────────────────────────────────────────────────────┤
│ Data Layer │
│ HaboModel → SQLite (sqflite) │
└─────────────────────────────────────────────────────┘
```
### 4.2 设计模式
| 模式 | 应用场景 |
|------|----------|
| **Repository Pattern** | `HabitRepository``EventRepository``CategoryRepository` 抽象数据访问 |
| **Service Locator** | `ServiceLocator` 单例管理全局服务实例 |
| **Factory Pattern** | `RepositoryFactory` 创建各仓库实例 |
| **Observer Pattern** | `ChangeNotifier` + `Provider` 实现响应式 UI 更新 |
| **Singleton Pattern** | `HaboModel``ServiceLocator` 确保单实例 |
| **Strategy Pattern** | `HabitType` 枚举区分布尔/数值习惯的不同处理逻辑 |
### 4.3 数据流
```
用户操作 → Widget
→ HabitsManager (ChangeNotifier)
→ Repository (数据访问抽象)
→ HaboModel (SQLite 操作)
→ Database
HabitsManager.notifyListeners()
→ Provider 更新
→ Widget 重建
```
### 4.4 初始化流程 (`main.dart`)
```
1. SettingsManager 初始化(读取持久化设置)
2. 创建 HaboModel 实例(共享数据库连接)
3. 调用 HaboModel.initDatabase() 初始化 SQLite
4. 初始化 ServiceLocator注册所有服务
5. 创建 HabitsManager注入仓库和服务依赖
6. 调用 HabitsManager.loadHabits() 加载数据
7. 初始化通知服务
8. 创建 AppRouter注入状态管理器
9. 启动日变化定时器(检测跨日自动刷新)
10. 渲染 MaterialApp.router
```
---
## 5. 数据模型
### 5.1 核心枚举 (`constants.dart`)
```dart
// 习惯类型
enum HabitType { boolean, numeric }
// 日状态类型
enum DayType { clear, check, fail, skip, progress }
// 主题模式
enum Themes { device, light, dark, oled, materialYou }
```
### 5.2 HabitData (`model/habit_data.dart`)
习惯的完整数据模型:
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | `int?` | 数据库自增主键 |
| `position` | `int` | 排序位置 |
| `title` | `String` | 习惯标题 |
| `twoDayRule` | `bool` | 是否启用两天法则 |
| `cue` | `String` | 提示(触发器) |
| `routine` | `String` | 例行动作描述 |
| `reward` | `String` | 奖励描述 |
| `showReward` | `bool` | 是否显示奖励通知 |
| `sanction` | `String` | 惩罚描述 |
| `showSanction` | `bool` | 是否显示惩罚通知 |
| `accountant` | `String` | 问责伙伴 |
| `advanced` | `bool` | 是否显示高级选项cue/routine/reward |
| `notification` | `bool` | 是否启用通知提醒 |
| `notTime` | `TimeOfDay` | 通知时间 |
| `events` | `SplayTreeMap<DateTime, List>` | 事件记录(日期→事件列表) |
| `habitType` | `HabitType` | 布尔型或数值型 |
| `targetValue` | `double` | 数值型目标值(默认 100 |
| `partialValue` | `double` | 数值型部分进度增量(默认 10 |
| `unit` | `String` | 数值型单位 |
| `categories` | `List<Category>` | 所属分类列表 |
| `archived` | `bool` | 是否已归档 |
| `streak` | `int` | 当前连续天数(运行时计算) |
**事件列表结构** (`events[date]`):
```
布尔型: [DayType, comment]
数值型: [DayType, comment, progressValue, targetValue]
```
**关键方法**:
- `isCompletedForDate(date)` — 判断某日是否完成
- `getProgressForDate(date)` — 获取某日进度值
- `getProgressPercentage(date)` — 获取进度百分比
### 5.3 Category (`model/category.dart`)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | `int?` | 自增主键 |
| `title` | `String` | 分类名称 |
| `iconCodePoint` | `int` | 图标 Unicode 码点 |
| `fontFamily` | `String?` | 图标字体族(如 FontAwesome |
---
## 6. 数据库 Schema
数据库版本: **9**,文件: `habo_db0.db`
### 6.1 habits 表
```sql
CREATE TABLE habits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
position INTEGER, -- 排序位置
title TEXT, -- 习惯标题
twoDayRule INTEGER, -- 两天法则开关 (0/1)
cue TEXT DEFAULT '', -- 提示触发器
routine TEXT DEFAULT '', -- 例行动作
reward TEXT DEFAULT '', -- 奖励
showReward INTEGER, -- 显示奖励 (0/1)
advanced INTEGER, -- 高级模式 (0/1)
notification INTEGER, -- 通知开关 (0/1)
notTime TEXT, -- 通知时间 (HH:MM)
sanction TEXT DEFAULT '', -- 惩罚
showSanction INTEGER DEFAULT 0, -- 显示惩罚 (0/1)
accountant TEXT DEFAULT '', -- 问责伙伴
habitType INTEGER DEFAULT 0, -- 习惯类型 (0=布尔, 1=数值)
targetValue REAL DEFAULT 1.0, -- 目标值
partialValue REAL DEFAULT 1.0, -- 部分增量
unit TEXT DEFAULT '', -- 单位
archived INTEGER DEFAULT 0 -- 归档状态 (0/1)
);
```
### 6.2 events 表
```sql
CREATE TABLE events (
id INTEGER, -- 外键 → habits.id
dateTime TEXT, -- 日期时间字符串
dayType INTEGER, -- DayType 枚举索引
comment TEXT DEFAULT '', -- 备注
progressValue REAL DEFAULT 0.0, -- 进度值(数值习惯)
targetValue REAL DEFAULT 0.0, -- 目标值快照
PRIMARY KEY(id, dateTime),
FOREIGN KEY (id) REFERENCES habits(id) ON DELETE CASCADE
);
```
**DayType 枚举值**:
| 值 | 含义 |
|----|------|
| 0 | clear — 清除 |
| 1 | check — 完成 |
| 2 | fail — 失败 |
| 3 | skip — 跳过 |
| 4 | progress — 进行中(数值型部分完成) |
### 6.3 categories 表
```sql
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL, -- 分类名称
iconCodePoint INTEGER NOT NULL, -- 图标码点
fontFamily TEXT -- 图标字体族
);
```
### 6.4 habit_categories 关联表
```sql
CREATE TABLE habit_categories (
habit_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
PRIMARY KEY (habit_id, category_id),
FOREIGN KEY (habit_id) REFERENCES habits(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);
```
### 6.5 数据库迁移历史
| 版本 | 变更 |
|------|------|
| V1→V2 | events 表增加 `comment` 字段 |
| V2→V3 | habits 表增加 `sanction``showSanction``accountant` 字段 |
| V3→V4 | habits 表增加 `habitType``targetValue``partialValue``unit`events 增加 `progressValue` |
| V4→V5 | events 表增加 `targetValue`;新建 `categories``habit_categories` 表 |
| V5→V6 | habits 表增加 `archived` 字段 |
| V6→V7 | events 表确保有 `targetValue`categories 增加 `fontFamily` |
| V7→V8 | events 表确保有 `targetValue` |
| V8→V9 | events 表确保有 `targetValue` |
---
## 7. 核心模块详解
### 7.1 HabitsManager (`habits/habits_manager.dart`)
**中心业务逻辑管理器**,继承 `ChangeNotifier`,是整个应用的核心。
**职责**:
- 管理 habits 和 categories 的内存状态
- 协调 Repository 层的数据操作
- 集成通知、备份、UI 反馈等服务
- 处理拖拽排序、归档、undo 等交互逻辑
**核心 API**:
```dart
// 习惯 CRUD
Future<void> loadHabits()
Future<void> addHabit(Habit habit)
Future<void> editHabit(Habit habit)
Future<void> deleteHabit(int id)
Future<void> archiveHabit(int id)
void reorderList(int oldIndex, int newIndex)
// 事件操作
Future<void> addEvent(int id, DateTime date, List event)
Future<void> deleteEvent(int id, DateTime date)
// 分类操作
Future<void> loadCategories()
Future<void> addCategory(Category category)
Future<void> updateCategory(Category category)
Future<void> deleteCategory(int id)
Future<void> updateHabitCategories(int habitId, List<Category> categories)
// 服务调用
Future<void> createBackup()
Future<void> loadBackup(String path)
Future<void> resetNotifications()
void updateHomeWidget()
// 数据访问
List<Habit> get activeHabits // 未归档习惯
List<Habit> get archivedHabits // 已归档习惯
Habit? findHabitById(int id)
```
### 7.2 HaboModel (`model/habo_model.dart`)
**直接操作 SQLite 数据库的底层类**
- 使用 `sqflite`(移动端)或 `sqflite_common_ffi`Linux/macOS
- 管理数据库创建、迁移、CRUD
- 处理 `PRAGMA foreign_keys = ON` 级联删除
### 7.3 Repository 层 (`repositories/`)
`HaboModel` 提供**抽象接口**,实现关注点分离:
- `HabitRepository` — 习惯 CRUD、排序、批量操作
- `EventRepository` — 事件增删查
- `CategoryRepository` — 分类 CRUD 及关联管理
- `RepositoryFactory` — 创建各 Repository 实例,注入 `HaboModel`
### 7.4 Service 层 (`services/`)
| 服务 | 职责 |
|------|------|
| `ServiceLocator` | 单例 DI 容器,初始化并持有所有服务实例 |
| `NotificationService` | 管理本地通知调度(每日提醒、奖励/惩罚) |
| `BackupService` | 数据库备份为 JSON 文件、从 JSON 恢复 |
| `UIFeedbackService` | 统一的 Snackbar 消息展示(成功/失败/警告) |
| `BiometricAuthService` | 封装 `local_auth` 生物识别认证 |
| `HomeWidgetService` | 更新 iOS/Android 主屏幕小组件数据 |
---
## 8. 导航系统
采用 **Flutter Navigation 2.0**,基于 `RouterDelegate`
### 路由定义
| 常量 | 路径 | 页面 |
|------|------|------|
| `splashPath` | `/` | 启动页 |
| `habitsPath` | `/habits` | 习惯列表主页 |
| `statisticsPath` | `/statistics` | 统计页 |
| `settingsPath` | `/settings` | 设置页 |
| `onboardingPath` | `/onboarding` | 引导页 |
| `createHabitPath` | `/create` | 创建习惯 |
| `editHabitPath` | `/edit` | 编辑习惯 |
| `whatsNewPath` | `/whatsnew` | 更新日志 |
### 深度链接
支持 `habo://` scheme例如 `habo://settings` 直接跳转设置页。
### 关键类
- **`AppRouter`** — `RouterDelegate` 实现,管理页面栈
- **`AppStateManager`** — 管理各页面的显示状态bool 标志位)
- **`HaboRouteInformationParser`** — 解析 URL 到 `HaboRouteConfiguration`
---
## 9. 状态管理
使用 **Provider + ChangeNotifier** 模式:
```
MultiProvider(
providers:
- ChangeNotifierProvider<SettingsManager>
- ChangeNotifierProvider<HabitsManager>
- ChangeNotifierProvider<AppStateManager>
)
```
- `HabitsManager` — 习惯数据变化时调用 `notifyListeners()`,驱动 UI 重建
- `SettingsManager` — 设置变更时通知(主题、音效等)
- `AppStateManager` — 导航状态变更通知
- **注意**: `AppRouter` 不监听 `HabitsManager`,避免数据变化导致非预期的导航跳转
---
## 10. 国际化 (i18n)
- 使用 `flutter_intl` + ARB 文件管理
- 文件位于 `lib/l10n/intl_*.arb`
- 支持 **27 种语言**
- 中文(简体/繁体)、英语、西班牙语、法语、德语、意大利语、葡萄牙语、俄语、日语(未列出但可能有)、阿拉伯语、希伯来语、波兰语、荷兰语、瑞典语、捷克语、越南语、印尼语、土耳其语、乌克兰语、加泰罗尼亚语、斯洛伐克语、巴斯克语、世界语、挪威语等
- 生成代码在 `lib/generated/` 目录
**添加新语言**:
1.`lib/l10n/` 下创建 `intl_<locale>.arb`
2. 运行 `flutter gen-l10n` 生成代码
---
## 11. 主题系统
`HaboTheme` 类提供三种主题:
| 主题 | 说明 |
|------|------|
| `lightTheme` | 浅色主题,浅灰背景 (#FAFAFA) |
| `darkTheme` | 深色主题,纯黑背景 (#000000) |
| `oledTheme` | OLED 深色主题,纯黑背景 |
**主题模式** (`Themes` 枚举):
- `device` — 跟随系统
- `light` — 强制浅色
- `dark` — 强制深色
- `oled` — OLED 黑色
- `materialYou` — Material You 动态取色
**特性**:
- 使用 Google Fonts 自定义字体
- 主色调: `#09BF30`(绿色)
- 支持平台差异iOS/Android 不同组件样式)
---
## 12. 通知系统
使用 `awesome_notifications` 实现本地通知:
- **每日提醒** — 用户设定时间推送提醒
- **奖励通知** — 完成习惯时触发(可配置音效)
- **惩罚通知** — 习惯失败时触发
`NotificationService` 通过 `HabitsManager` 调用:
- `resetNotifications()` — 重置所有习惯通知
- `removeNotifications(id)` — 删除指定习惯的通知
- `handleHabitEventAdded()` — 事件添加后触发通知
---
## 13. 备份与恢复
`BackupService` 提供完整的数据导入/导出:
- **导出**: 将所有 habits、events、categories 序列化为 JSON 文件
- **导入**: 从 JSON 文件解析并恢复到数据库
- **兼容性**: 支持旧版格式迁移
- **文件选择**: 使用 `flutter_file_dialog`(移动端)或 `file_picker`(桌面端)
---
## 14. 桌面端小组件
使用 `home_widget` 包实现 iOS/Android 主屏幕小组件:
- **小组件类型**: 170x170 圆形进度指示器
- **数据传递**: 通过 `HomeWidgetService` 更新数据
- **显示内容**: 今日习惯完成数量 / 总数量
- **渲染**: `CircularProgressPainter` 自定义绘制多段圆弧
---
## 15. 生物识别认证
- `BiometricAuthService` 封装 `local_auth`
- `BiometricAuthWrapper` Widget 包裹主内容
- 支持指纹和面容识别
- 应用从后台恢复时重新验证
- 认证失败提供重试对话框
---
## 16. CI/CD 与构建
### GitHub Actions (`ci.yml`)
**触发条件**: push/PR 到 `main``develop` 分支
**流水线**:
1. **test**`flutter analyze` + `flutter test`
2. **build-android**(依赖 test 通过):
- `flutter build apk --release --split-per-abi --no-tree-shake-icons`
- 按 CPU 架构分拆 APKarm64-v8a, armeabi-v7a, x86_64
- 上传构建产物
### 本地构建
```bash
# 安装依赖
flutter pub get
# 生成图标包
dart run flutter_iconpicker:generate_packs --packs fontAwesomeIcons
# 生成国际化代码
flutter gen-l10n
# 运行测试
flutter test
# 构建 APK
flutter build apk --release
# 构建 iOS
flutter build ios --release
# 桌面端
flutter build linux --release
flutter build macos --release
```
### Fastlane
`fastlane/` 目录包含多平台商店发布的自动化配置。
---
## 17. 开发指南
### 环境要求
- Flutter SDK >= 3.41.1
- Dart SDK >= 3.11.0
- Android: Java 17, minSdk 21
- iOS: Xcode (最新版)
- Linux: 额外依赖 `sqflite_common_ffi`
### 项目约定
1. **状态管理** — 使用 `ChangeNotifier` + `Provider`,新功能应创建 Manager 类
2. **数据访问** — 通过 Repository 接口,不直接使用 `HaboModel`
3. **服务依赖** — 通过 `ServiceLocator` 获取,不手动创建实例
4. **国际化** — 所有用户可见文本必须使用 `AppLocalizations`,不硬编码字符串
5. **主题** — 使用 `HaboTheme` 定义的颜色和样式,不直接写色值
6. **数据库迁移** — 修改 Schema 必须新增迁移方法并更新 `_dbVersion`
7. **测试** — 使用 `mocktail` mock Repository 层,测试业务逻辑而非 UI 渲染
### 添加新功能的典型流程
1.`model/` 中定义或修改数据模型
2.`repositories/` 中添加/更新 Repository 接口和实现
3.`services/` 中添加服务(如需要)
4.`HabitsManager` 中添加业务逻辑方法
5. 在对应的 screen/widget 中实现 UI
6.`lib/l10n/intl_en.arb` 中添加国际化文本
7. 编写单元测试
### 关键文件速查
| 需求 | 文件 |
|------|------|
| 添加新习惯字段 | `model/habit_data.dart` + `model/habo_model.dart` + 数据库迁移 |
| 修改日历行为 | `habits/habit.dart` |
| 添加新统计图表 | `statistics/` 目录 |
| 修改通知逻辑 | `services/notification_service.dart` |
| 添加新设置项 | `settings/settings_manager.dart` + `settings_screen.dart` |
| 修改导航流程 | `navigation/app_router.dart` + `navigation/app_state_manager.dart` |
| 添加新语言 | `lib/l10n/intl_<locale>.arb` |
| 修改主题 | `themes.dart` |

34
ios/.gitignore vendored Normal file
View File

@@ -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

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
/* 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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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.habo.habo;
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.habo.habo.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.habo.habo.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.habo.habo.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.habo.habo;
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.habo.habo;
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 */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -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)
}
}

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -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.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Habo</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>habo</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -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.
}
}

7
l10n.yaml Normal file
View File

@@ -0,0 +1,7 @@
arb-dir: lib/l10n
template-arb-file: intl_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations
synthetic-package: false
output-dir: lib/generated
nullable-getter: false

Some files were not shown because too many files have changed in this diff Show More