release: seal Focus Seed 1.0.0
@@ -0,0 +1,18 @@
|
||||
* text=auto
|
||||
*.dart text eol=lf
|
||||
*.yaml text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.md text eol=lf
|
||||
*.json text eol=lf
|
||||
*.kt text eol=lf
|
||||
*.kts text eol=lf
|
||||
*.xml text eol=lf
|
||||
*.gradle text eol=lf
|
||||
*.properties text eol=lf
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.webp binary
|
||||
*.jks binary
|
||||
*.aab binary
|
||||
*.apk binary
|
||||
@@ -0,0 +1,54 @@
|
||||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
coverage/
|
||||
*.aab
|
||||
*.apk
|
||||
|
||||
# Tester email lists can contain personal data.
|
||||
/launch/play-console/tester-email-list*.csv
|
||||
/launch/play-console/testers.csv
|
||||
/launch/screenshots/device-smoke-*.png
|
||||
/launch/screenshots/device-05-settings.png
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
@@ -0,0 +1,33 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
- platform: android
|
||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
- platform: ios
|
||||
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
All notable product changes are recorded here.
|
||||
|
||||
## [1.0.0] - 2026-06-10
|
||||
|
||||
### Added
|
||||
|
||||
- Daily anchor task.
|
||||
- 15, 25, and 45 minute focus timer.
|
||||
- Habit creation, rename, deletion, and daily completion.
|
||||
- One-minute reflection with mood selection.
|
||||
- Seven-day progress pulse.
|
||||
- Local JSON export and import.
|
||||
- Local data deletion and in-app privacy explanation.
|
||||
- Light and dark Material 3 themes.
|
||||
- No-op analytics and monetization boundaries for future integrations.
|
||||
|
||||
### Verified
|
||||
|
||||
- Strict Dart analysis passed.
|
||||
- Widget tests passed.
|
||||
- Android App Bundle release build passed.
|
||||
- Release smoke test passed on MT2110 with Android 14.
|
||||
- Android manifest contains no sensitive user-data permissions.
|
||||
|
||||
### Release posture
|
||||
|
||||
- English-only V1.
|
||||
- No account, server, ads, billing, analytics, or cloud sync.
|
||||
- Released first through Google Play testing tracks.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Focus Seed
|
||||
|
||||
Focus Seed is a private, offline-first focus timer, habit tracker, and daily reflection app built with Flutter.
|
||||
|
||||
## V1 Product Loop
|
||||
|
||||
1. Choose one anchor task for today.
|
||||
2. Complete a focused session.
|
||||
3. Check off small habits.
|
||||
4. Save a one-minute reflection.
|
||||
5. Review the seven-day pulse.
|
||||
|
||||
## Architecture
|
||||
|
||||
The app uses a modular feature structure under `lib/src`:
|
||||
|
||||
- `app`: application shell and theme
|
||||
- `core`: date, JSON, and collection utilities
|
||||
- `data`: local persistence boundary
|
||||
- `models`: serializable domain models
|
||||
- `services`: analytics and monetization interfaces
|
||||
- `features`: user-facing product flows
|
||||
- `widgets`: reusable UI components
|
||||
|
||||
See `launch/architecture.md` for the complete responsibility map.
|
||||
|
||||
## Privacy
|
||||
|
||||
Version 1.0.0 has no account system, server, advertising SDK, analytics SDK, billing SDK, or sensitive Android permissions. User data remains on the device unless the user explicitly exports it.
|
||||
|
||||
## Development
|
||||
|
||||
```text
|
||||
flutter pub get
|
||||
flutter analyze
|
||||
flutter test
|
||||
flutter run
|
||||
```
|
||||
|
||||
Release build:
|
||||
|
||||
```text
|
||||
flutter build appbundle --release
|
||||
```
|
||||
|
||||
The upload keystore and `key.properties` are intentionally excluded from Git. Keep them backed up securely because future Play updates require the same upload key.
|
||||
@@ -0,0 +1,44 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
errors:
|
||||
dead_code: error
|
||||
invalid_use_of_protected_member: error
|
||||
invalid_use_of_visible_for_testing_member: error
|
||||
unused_import: error
|
||||
unused_local_variable: error
|
||||
language:
|
||||
strict-casts: true
|
||||
strict-inference: true
|
||||
strict-raw-types: true
|
||||
|
||||
linter:
|
||||
rules:
|
||||
always_declare_return_types: true
|
||||
always_put_required_named_parameters_first: true
|
||||
avoid_bool_literals_in_conditional_expressions: true
|
||||
avoid_catches_without_on_clauses: true
|
||||
avoid_empty_else: true
|
||||
avoid_print: true
|
||||
avoid_redundant_argument_values: true
|
||||
avoid_slow_async_io: true
|
||||
avoid_type_to_string: true
|
||||
cancel_subscriptions: true
|
||||
close_sinks: true
|
||||
directives_ordering: true
|
||||
no_adjacent_strings_in_list: true
|
||||
no_duplicate_case_values: true
|
||||
omit_local_variable_types: true
|
||||
prefer_asserts_with_message: true
|
||||
prefer_const_constructors: true
|
||||
prefer_const_declarations: true
|
||||
prefer_final_fields: true
|
||||
prefer_final_in_for_each: true
|
||||
prefer_final_locals: true
|
||||
prefer_single_quotes: true
|
||||
require_trailing_commas: true
|
||||
sort_child_properties_last: true
|
||||
unawaited_futures: true
|
||||
unnecessary_await_in_return: true
|
||||
unnecessary_lambdas: true
|
||||
use_build_context_synchronously: true
|
||||
@@ -0,0 +1,11 @@
|
||||
/.gradle
|
||||
/captures/
|
||||
/local.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
.cxx/
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
key.properties
|
||||
**/*.keystore
|
||||
**/*.jks
|
||||
@@ -0,0 +1,56 @@
|
||||
import java.util.Properties
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystorePropertiesFile.inputStream().use { keystoreProperties.load(it) }
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.dailyanchor.focusseed"
|
||||
compileSdk = 35
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.dailyanchor.focusseed"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["keyPassword"] as String
|
||||
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["storePassword"] as String
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
@@ -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>
|
||||
@@ -0,0 +1,45 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="Focus Seed"
|
||||
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>
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.dailyanchor.focusseed
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity : FlutterActivity()
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 14 KiB |
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,21 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
@@ -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.10.2-all.zip
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
@@ -0,0 +1,90 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,25 @@
|
||||
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.7.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
@@ -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
|
||||
@@ -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>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1 @@
|
||||
#include "Generated.xcconfig"
|
||||
@@ -0,0 +1 @@
|
||||
#include "Generated.xcconfig"
|
||||
@@ -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 = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
249021D4217E4FDB00AE95B9 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
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.dailyanchor.focusSeed;
|
||||
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.dailyanchor.focusSeed.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.dailyanchor.focusSeed.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.dailyanchor.focusSeed.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 = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
97C147041CF9000F007C117D /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SUPPORTED_PLATFORMS = iphoneos;
|
||||
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.dailyanchor.focusSeed;
|
||||
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.dailyanchor.focusSeed;
|
||||
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 */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,99 @@
|
||||
<?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"
|
||||
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"
|
||||
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>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
|
After Width: | Height: | Size: 68 B |
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>Focus Seed</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>focus_seed</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>
|
||||
@@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# Focus Seed Architecture
|
||||
|
||||
Status: refactored from single-file prototype into modular Flutter structure.
|
||||
|
||||
## Directory Layout
|
||||
|
||||
```text
|
||||
lib/
|
||||
main.dart
|
||||
src/
|
||||
app/
|
||||
focus_seed_app.dart
|
||||
app_theme.dart
|
||||
core/
|
||||
date_keys.dart
|
||||
iterable_extensions.dart
|
||||
json_utils.dart
|
||||
data/
|
||||
focus_seed_store.dart
|
||||
models/
|
||||
app_state.dart
|
||||
focus_session.dart
|
||||
habit.dart
|
||||
mood.dart
|
||||
reflection_entry.dart
|
||||
services/
|
||||
analytics_provider.dart
|
||||
monetization_provider.dart
|
||||
features/
|
||||
shell/
|
||||
focus_seed_home.dart
|
||||
home/
|
||||
home_screen.dart
|
||||
timer/
|
||||
timer_screen.dart
|
||||
habits/
|
||||
habits_screen.dart
|
||||
reflection/
|
||||
reflection_screen.dart
|
||||
settings/
|
||||
settings_screen.dart
|
||||
widgets/
|
||||
app_scaffold.dart
|
||||
dialogs.dart
|
||||
empty_state.dart
|
||||
habit_row.dart
|
||||
inline_editable_text.dart
|
||||
loading_view.dart
|
||||
metric_tile.dart
|
||||
section_label.dart
|
||||
seed_card.dart
|
||||
settings_tile.dart
|
||||
weekly_progress.dart
|
||||
```
|
||||
|
||||
## Responsibility Boundaries
|
||||
|
||||
- `main.dart`: process entry only.
|
||||
- `src/app`: app shell and theme.
|
||||
- `src/models`: serializable domain objects.
|
||||
- `src/data`: persistence boundary using `shared_preferences`.
|
||||
- `src/services`: no-op interfaces for analytics and monetization.
|
||||
- `src/features/shell`: state orchestration and feature routing.
|
||||
- `src/features/*`: screen-level UI and user flows.
|
||||
- `src/widgets`: reusable UI components and dialogs.
|
||||
- `src/core`: framework-light utilities.
|
||||
|
||||
## Current Design Decisions
|
||||
|
||||
- No real ad SDK in V1.
|
||||
- No real billing SDK in V1.
|
||||
- No analytics SDK in V1.
|
||||
- No network dependency.
|
||||
- JSON import/export uses copy/paste to avoid file permissions.
|
||||
- Shell owns state until a future state-management package is justified.
|
||||
|
||||
## Upgrade Path
|
||||
|
||||
Next modularization steps when needed:
|
||||
|
||||
- Move `src/app`, `src/core`, `src/models`, `src/services`, and `src/widgets` into shared packages.
|
||||
- Replace shell state with Riverpod once multiple apps/features share the same state patterns.
|
||||
- Add real `AnalyticsProvider`, `MonetizationProvider`, and remote config implementations only with privacy/Data safety updates.
|
||||
- Add golden tests for key screens before scaling the app factory.
|
||||
@@ -0,0 +1,206 @@
|
||||
# Closed Testing Plan
|
||||
|
||||
Status: Required for this personal developer account before production access.
|
||||
|
||||
Official baseline:
|
||||
|
||||
- Minimum 12 testers.
|
||||
- Testers must opt in to the closed test.
|
||||
- At least 12 testers must remain opted in for 14 continuous days before applying for production access.
|
||||
- Internal testing is optional and fast, but it does not replace the required closed test.
|
||||
|
||||
Official references:
|
||||
|
||||
- https://support.google.com/googleplay/android-developer/answer/14151465
|
||||
- https://support.google.com/googleplay/android-developer/answer/9845334
|
||||
|
||||
## Immediate Flow
|
||||
|
||||
1. Finish App setup.
|
||||
- Privacy policy URL
|
||||
- App access: no login required
|
||||
- Ads: no ads
|
||||
- Content rating
|
||||
- Target audience
|
||||
- Data safety
|
||||
- Government app: no
|
||||
- Financial products and services: no
|
||||
- Health: no
|
||||
- App category and contact details
|
||||
- Store listing
|
||||
|
||||
2. Publish an internal testing release.
|
||||
- Upload `build/app/outputs/bundle/release/app-release.aab`.
|
||||
- Use release name `1.0.0 internal`.
|
||||
- Release notes:
|
||||
```text
|
||||
First internal test release.
|
||||
Includes focus timer, habit tracking, daily reflection, local progress stats, JSON export/import, and local privacy controls.
|
||||
```
|
||||
- Add yourself and one or two trusted accounts if available.
|
||||
- Use this to confirm Play delivery and installation.
|
||||
|
||||
3. Create a closed testing track.
|
||||
- Play Console > Testing > Closed testing.
|
||||
- Create track name: `Focus Seed V1 closed test`.
|
||||
- Countries/regions: use English-first regions from `region-localization-strategy.md`.
|
||||
- Upload the same AAB.
|
||||
- Release name: `1.0.0 closed test`.
|
||||
- Release notes:
|
||||
```text
|
||||
First closed test release for Focus Seed.
|
||||
Please test the Today, Timer, Habits, Reflect, Settings, Export JSON, Import JSON, and Clear local data flows.
|
||||
```
|
||||
|
||||
4. Add testers.
|
||||
- Use email list or Google Group.
|
||||
- Recommended target: 15-20 invited testers, not exactly 12.
|
||||
- Minimum passing target: 12 opted-in testers.
|
||||
- Tester emails must be Google accounts that can access Google Play.
|
||||
- Adding emails is not enough. Each tester must open the opt-in link and join the test.
|
||||
- If uploading CSV, use one email address per line with no comma and no header row. Use `tester-email-list-no-header.csv` as the safer template.
|
||||
|
||||
5. Send tester instructions.
|
||||
- Send the opt-in link from Play Console.
|
||||
- Ask each tester to confirm after they see the "You're a tester" or equivalent joined state.
|
||||
- Ask each tester to install and open the app at least once.
|
||||
|
||||
6. Track 14 days.
|
||||
- Day 0 starts when at least 12 testers have opted in.
|
||||
- Keep at least 12 opted in continuously for 14 days.
|
||||
- If the opted-in count drops below 12, recruit replacements immediately and expect the qualifying window may be affected.
|
||||
|
||||
7. Apply for production access.
|
||||
- Apply from the Play Console dashboard when eligible.
|
||||
- Answer questions using the notes in this file.
|
||||
|
||||
## 12 Tester Strategy For A Solo Developer
|
||||
|
||||
Best compliant sources:
|
||||
|
||||
- Friends and family with Android phones and Google accounts.
|
||||
- Former classmates, coworkers, or community contacts.
|
||||
- Small indie developer groups where members exchange real testing help.
|
||||
- Productivity/focus communities where you honestly ask for feedback.
|
||||
- Existing users from your personal network.
|
||||
|
||||
Recommended count:
|
||||
|
||||
- Invite 20 people.
|
||||
- Aim for 15 opted-in.
|
||||
- Keep a buffer above 12 because some people will forget, use the wrong account, or fail to install.
|
||||
|
||||
Do not:
|
||||
|
||||
- Use fake Google accounts.
|
||||
- Buy bot installs.
|
||||
- Promise paid reviews or fake ratings.
|
||||
- Ask testers to leave public reviews during closed testing.
|
||||
- Misrepresent who tested or what feedback you received.
|
||||
|
||||
Tester success checklist:
|
||||
|
||||
- Tester email is added to the closed test email list or Google Group.
|
||||
- Tester opens the opt-in link while signed into the same Google account.
|
||||
- Tester joins the test.
|
||||
- Tester installs from Google Play.
|
||||
- Tester opens the app.
|
||||
- Tester tries at least one real flow.
|
||||
- Tester sends short feedback privately.
|
||||
|
||||
## Tester Invitation Message - Chinese
|
||||
|
||||
```text
|
||||
我在 Google Play 上做一个新的 Android 应用封闭测试,名字是 Focus Seed。
|
||||
它是一个轻量的专注计时、习惯追踪和每日反思工具,不需要账号、不收费、没有广告。
|
||||
|
||||
能麻烦你帮我测试一下吗?
|
||||
|
||||
步骤:
|
||||
1. 用你的 Android 手机打开这个测试链接:
|
||||
[粘贴 Play Console opt-in 链接]
|
||||
2. 确认加入测试。
|
||||
3. 从 Google Play 安装 Focus Seed。
|
||||
4. 打开 App,试一下:
|
||||
- Today 页面设置一个今日重点
|
||||
- Timer 页面启动或完成一次专注
|
||||
- Habits 页面勾选一个习惯
|
||||
- Reflect 页面写一句反思
|
||||
- Settings 页面看一下隐私/导出入口
|
||||
5. 测完后告诉我:
|
||||
- 是否能正常安装和打开
|
||||
- 哪个页面最不清楚
|
||||
- 有没有崩溃、卡顿、文字看不清或按钮点不了
|
||||
|
||||
这个测试需要保持加入状态 14 天。你不需要每天使用,但请不要退出测试。
|
||||
谢谢!
|
||||
```
|
||||
|
||||
## Tester Invitation Message - English
|
||||
|
||||
```text
|
||||
I am running a closed Google Play test for a new Android app called Focus Seed.
|
||||
It is a lightweight focus timer, habit tracker, and one-minute reflection app. It does not require an account, does not include ads, and does not include payments.
|
||||
|
||||
Could you help me test it?
|
||||
|
||||
Steps:
|
||||
1. Open this test link on your Android phone:
|
||||
[Paste Play Console opt-in link]
|
||||
2. Join the test.
|
||||
3. Install Focus Seed from Google Play.
|
||||
4. Try these flows:
|
||||
- Set a daily anchor on Today
|
||||
- Start or complete a focus timer
|
||||
- Check off a habit
|
||||
- Save a short reflection
|
||||
- Open Settings and review privacy/export controls
|
||||
5. Send me quick feedback:
|
||||
- Did install/open work?
|
||||
- Was any screen confusing?
|
||||
- Any crash, lag, unreadable text, or button issue?
|
||||
|
||||
The test needs testers to stay opted in for 14 days. You do not need to use it every day, but please do not leave the test during that period.
|
||||
Thank you!
|
||||
```
|
||||
|
||||
## Quick Feedback Questions
|
||||
|
||||
Send these as a message or simple form:
|
||||
|
||||
```text
|
||||
1. Your phone model:
|
||||
2. Android version, if known:
|
||||
3. Did the app install from Google Play? Yes / No
|
||||
4. Did the app open successfully? Yes / No
|
||||
5. Which screen did you try? Today / Timer / Habits / Reflect / Settings
|
||||
6. Any crash, freeze, lag, or layout issue?
|
||||
7. What felt confusing?
|
||||
8. One thing you would improve:
|
||||
```
|
||||
|
||||
## Tester Tracking Table
|
||||
|
||||
Use a spreadsheet or notes app:
|
||||
|
||||
```text
|
||||
Tester name | Google account email | Invited | Opted in | Installed | Opened app | Feedback received | Still opted in day 14
|
||||
```
|
||||
|
||||
Target:
|
||||
|
||||
- Invited: 20
|
||||
- Opted in: 15+
|
||||
- Installed/opened: 12+
|
||||
- Feedback: 8+
|
||||
|
||||
## Production Access Application Notes
|
||||
|
||||
Use these points when Play Console asks about the closed test:
|
||||
|
||||
- App purpose: Focus Seed is a private focus timer, habit tracker, and daily reflection app.
|
||||
- Test scope: Today anchor, timer, habits, reflection, settings, export/import, and local data deletion.
|
||||
- Tester recruitment: invited trusted Android users through direct email/message using the Play Console opt-in link.
|
||||
- Feedback collected: installation success, first-run clarity, timer usability, habit check-off, reflection save flow, settings/privacy clarity, layout or crash reports.
|
||||
- Actions taken: fixed native Android launch package path, removed unfinished ads/subscription placeholders from settings, verified release build on Android 14.
|
||||
- Readiness: no account, no server, no ads, no billing, no analytics SDK, no sensitive permissions, privacy policy and Data safety match the shipped app.
|
||||
@@ -0,0 +1,30 @@
|
||||
# Data Safety Guidance
|
||||
|
||||
This guidance assumes the submitted build is the current Focus Seed V1:
|
||||
|
||||
- No account.
|
||||
- No server.
|
||||
- No ad SDK.
|
||||
- No analytics SDK.
|
||||
- No billing SDK.
|
||||
- No network permission.
|
||||
- Data stored locally with `shared_preferences`.
|
||||
- Export/import is user-controlled copy/paste JSON.
|
||||
|
||||
## Suggested Data Safety Answers
|
||||
|
||||
- Does your app collect or share any of the required user data types? No, if no SDKs are added before upload.
|
||||
- Is all user data collected by your app encrypted in transit? Not applicable for V1 because the app does not transmit user data.
|
||||
- Do users have a way to request that their data is deleted? Yes. Users can clear local data in app settings or clear/uninstall app storage.
|
||||
- Does your app share user data with third parties? No, if no SDKs are added before upload.
|
||||
|
||||
## App Content Notes
|
||||
|
||||
- Ads: No.
|
||||
- In-app purchases: No.
|
||||
- Target audience: Adults/general productivity users. Do not mark as children-directed.
|
||||
- Content rating: Productivity app, no UGC, no violence, no medical/financial claims.
|
||||
|
||||
## Important
|
||||
|
||||
If you add ads, analytics, cloud sync, billing, crash reporting, file upload, or account sign-in before uploading, update this guidance, the privacy policy, and Play Console Data safety answers.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Region And Localization Strategy
|
||||
|
||||
Status: V1 launch guardrail for Focus Seed.
|
||||
|
||||
## Principle
|
||||
|
||||
Focus Seed is intended to become a global productivity app, but V1 should not open every country by default. The first release is for account rescue, policy safety, and product validation. Availability, language, privacy wording, and monetization should expand by region only when the app has the right local readiness.
|
||||
|
||||
Google Play country targeting is based on the user's Play country, not the user's current physical location. Internal testing is the exception: internal testers can receive the app even if their country is not selected for production/open/closed tracks.
|
||||
|
||||
## V1 Recommended Launch Scope
|
||||
|
||||
Use English-only store listing and app UI.
|
||||
|
||||
Recommended first production or closed-test countries/regions:
|
||||
|
||||
- United States
|
||||
- Canada
|
||||
- Australia
|
||||
- New Zealand
|
||||
- Singapore
|
||||
- India
|
||||
- Philippines
|
||||
- Malaysia
|
||||
|
||||
Rationale:
|
||||
|
||||
- English is broadly usable in these markets.
|
||||
- The app is culturally neutral: focus timer, habit tracking, local reflection.
|
||||
- V1 has no account, no server, no ads, no analytics, no billing, and no sensitive permissions.
|
||||
- This avoids adding rushed EU/EEA, Korean, Japanese, or other local-language obligations during the dormant-account rescue window.
|
||||
|
||||
If only internal testing is used for urgent account activity, do not over-optimize regions yet. Upload the AAB, finish required declarations, and keep the country plan ready for the first closed/open/production rollout.
|
||||
|
||||
## Regions To Defer
|
||||
|
||||
Defer EU / EEA until a later release unless there is a clear need:
|
||||
|
||||
- Add EU/EEA privacy wording and legal review.
|
||||
- Confirm trader / non-trader presentation requirements shown in Play Console.
|
||||
- Review geo-blocking and country availability choices.
|
||||
- Confirm consent flow if ads, analytics, remote config, crash reporting, or cloud sync are added later.
|
||||
|
||||
Defer United Kingdom and Switzerland if the goal is the simplest possible legal surface. They are English-friendly but should still get region-specific privacy review before paid or data-collecting features.
|
||||
|
||||
Defer South Korea:
|
||||
|
||||
- No Korean localization in V1.
|
||||
- Review local rating, local store expectations, and translated listing before launch.
|
||||
|
||||
Defer Japan:
|
||||
|
||||
- No Japanese localization in V1.
|
||||
- Productivity apps can work there, but store conversion and support quality need Japanese copy.
|
||||
|
||||
Defer Brazil / LATAM:
|
||||
|
||||
- Add Portuguese or Spanish listing and privacy/support wording first.
|
||||
|
||||
Defer Middle East / North Africa:
|
||||
|
||||
- Keep the app culturally neutral, but review local listing language and screenshots before expansion.
|
||||
|
||||
## Store Listing Language
|
||||
|
||||
V1:
|
||||
|
||||
- Default language: English (United States).
|
||||
- Do not add machine-translated Korean, Japanese, German, French, Spanish, or Portuguese just to look global.
|
||||
- Do not claim local-language support that the app does not provide.
|
||||
|
||||
Wave 2:
|
||||
|
||||
- English variants for UK/Australia only if those regions are opened.
|
||||
- Spanish or Portuguese only after translated screenshots and support text exist.
|
||||
|
||||
Wave 3:
|
||||
|
||||
- Japanese and Korean only after native-quality listing, screenshots, and cultural QA.
|
||||
|
||||
## Privacy And Data Safety
|
||||
|
||||
V1 privacy posture:
|
||||
|
||||
- No account.
|
||||
- No server.
|
||||
- No analytics SDK.
|
||||
- No ad SDK.
|
||||
- No billing SDK.
|
||||
- No network permission.
|
||||
- Local-only user-controlled data.
|
||||
|
||||
Even when no data is collected, Google Play still requires accurate Data safety answers and a privacy policy for published/testing apps outside internal-only use. Do not use region selection as an excuse to make inaccurate privacy statements.
|
||||
|
||||
If any of the following are added, revisit every target region before release:
|
||||
|
||||
- Ads
|
||||
- Analytics
|
||||
- Crash reporting
|
||||
- Remote config
|
||||
- Billing or subscriptions
|
||||
- Cloud backup/sync
|
||||
- Login/account creation
|
||||
- Push notifications
|
||||
- Location, contacts, calendar, storage, health, finance, or other sensitive data
|
||||
|
||||
## Culture And Content Guardrails
|
||||
|
||||
Keep V1 content neutral:
|
||||
|
||||
- No religious, political, medical, financial, gambling, dating, or adult claims.
|
||||
- No children-directed positioning.
|
||||
- No culturally specific imagery or slogans in screenshots.
|
||||
- No productivity claims that imply clinical treatment or guaranteed outcomes.
|
||||
- Use plain, calm English and avoid idioms that translate poorly.
|
||||
|
||||
## Expansion Checklist
|
||||
|
||||
Before adding a new region:
|
||||
|
||||
1. Confirm the app UI language and store listing language fit the region.
|
||||
2. Confirm privacy policy and support email wording fit the region.
|
||||
3. Confirm Data safety answers still match the shipped build.
|
||||
4. Confirm content rating and target audience are appropriate.
|
||||
5. Confirm screenshots do not include culturally sensitive text or imagery.
|
||||
6. Confirm ads/subscriptions, if added, are available and priced correctly for that country.
|
||||
7. Run at least one smoke test for the exact build being released.
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# Release Checklist
|
||||
|
||||
## Dormant Account Rescue Priority
|
||||
|
||||
Google Play warning shown by account owner:
|
||||
|
||||
- Risk: developer account may be closed because it is dormant / unused.
|
||||
- Fix deadline: 2026-06-14.
|
||||
- Current date when recorded: 2026-06-09.
|
||||
|
||||
Google's official inactive-account guidance says the account owner should complete both actions before the deadline:
|
||||
|
||||
1. Verify Play developer account contact email and phone number.
|
||||
2. Create and publish an app, publish an update, or safely upload through internal testing / closed testing if the app is not ready for public production.
|
||||
|
||||
For this project, the fastest low-risk route is:
|
||||
|
||||
1. Verify account email and phone in Play Console account details.
|
||||
2. Create the Focus Seed app record.
|
||||
3. Upload the signed AAB to internal testing first.
|
||||
4. Complete required App content sections: privacy policy, Data safety, content rating, target audience, ads declaration, app access.
|
||||
5. Add store listing text, screenshots, icon, and feature graphic.
|
||||
6. Send changes for review as soon as Play Console allows submission.
|
||||
7. If production access is not available, keep the internal/closed testing upload as the activity evidence and follow any Play Console testing requirement shown in the account.
|
||||
|
||||
Do not create a new developer account while this warning is active. Resolve this account directly.
|
||||
|
||||
## Required Closed Test For This Account
|
||||
|
||||
Play Console indicates production access is locked for this personal developer account.
|
||||
|
||||
- Internal testing is optional and fast, but does not unlock production access.
|
||||
- Closed testing is required before applying for production.
|
||||
- Minimum: 12 opted-in testers for at least 14 continuous days.
|
||||
- Recommended: invite 20 people and aim for 15+ opted-in testers to keep a buffer.
|
||||
- Use `closed-testing-plan.md` for tester recruitment messages, checklist, tracking table, and production access application notes.
|
||||
|
||||
## Region And Localization Gate
|
||||
|
||||
Use `region-localization-strategy.md` before selecting countries/regions in Play Console.
|
||||
|
||||
V1 recommendation:
|
||||
|
||||
- Do not select all countries by default.
|
||||
- Use English-only listing and UI.
|
||||
- For production or closed testing, start with English-first countries/regions: United States, Canada, Australia, New Zealand, Singapore, India, Philippines, and Malaysia.
|
||||
- Defer EU/EEA, United Kingdom, Switzerland, South Korea, Japan, Brazil/LATAM, and MENA until the matching privacy wording, localization, cultural QA, and monetization checks are complete.
|
||||
- If only internal testing is used for urgent account rescue, upload first; internal testing is not constrained the same way as production/open/closed country targeting.
|
||||
|
||||
## Build Evidence
|
||||
|
||||
- App Bundle: `build/app/outputs/bundle/release/app-release.aab`
|
||||
- Size: about 21.1 MB
|
||||
- Version: `1.0.0+1`
|
||||
- Package ID: `com.dailyanchor.focusseed`
|
||||
- minSdk: 26
|
||||
- targetSdk: 35
|
||||
- Release signing: local upload key configured
|
||||
- 512x512 Play listing icon: `launch/screenshots/focus-seed-icon-512-play.png`
|
||||
|
||||
## Verification Completed
|
||||
|
||||
- `flutter analyze`: passed
|
||||
- `flutter test`: passed
|
||||
- `flutter build appbundle --release`: passed
|
||||
- Modular architecture refactor: passed analyze/test/build after splitting `main.dart` into app, models, data, services, features, and widgets.
|
||||
- Web visual smoke test: passed for Today and Timer screens.
|
||||
- Android release smoke test: passed on MT2110 / Android 14 after fixing native `MainActivity` package path to match `com.dailyanchor.focusseed`.
|
||||
- Real device screenshots captured: `device-01-today.png`, `device-02-timer.png`, `device-03-habits.png`, `device-04-reflect.png`, `device-05-settings-clean.png`.
|
||||
- V1 settings screen hides unfinished ads/subscription placeholders; monetization remains code-level no-op infrastructure only.
|
||||
- Manifest reviewed: no INTERNET, AD_ID, location, camera, contacts, SMS, or storage permissions.
|
||||
- V1 contains no real ad, billing, analytics, or remote config SDK.
|
||||
|
||||
## Manifest Notes
|
||||
|
||||
Merged manifest includes:
|
||||
|
||||
- `android:minSdkVersion="26"`
|
||||
- `android:targetSdkVersion="35"`
|
||||
- Flutter text processing query
|
||||
- AndroidX signature-level dynamic receiver permission
|
||||
|
||||
No dangerous user-data permissions are declared.
|
||||
|
||||
## Before Upload
|
||||
|
||||
1. Publish the privacy policy at a public URL.
|
||||
2. Confirm support email `leavysong@gmail.com` is correct and accessible.
|
||||
3. Add the privacy policy URL in Play Console.
|
||||
4. Confirm account eligibility:
|
||||
- If production access exists, upload to internal testing first, then production.
|
||||
- If this is a new personal developer account created after 2023-11-13, create a closed test with at least 12 testers for 14 days before production access.
|
||||
5. Prepare real screenshots from the app.
|
||||
6. Replace launcher icon if desired.
|
||||
7. Choose countries/regions using `region-localization-strategy.md`.
|
||||
8. Keep `android/focus-seed-upload.jks` and `android/key.properties` safe. You need the same upload key for future updates.
|
||||
|
||||
## Upload Steps
|
||||
|
||||
1. Create app in Play Console.
|
||||
2. Fill app details and App content sections.
|
||||
3. Upload `app-release.aab`.
|
||||
4. Complete Data safety using `data-safety-guide.md`.
|
||||
5. Complete content rating questionnaire.
|
||||
6. Add privacy policy URL.
|
||||
7. Add store listing text from `store-listing.md`.
|
||||
8. Select countries/regions according to the V1 region strategy.
|
||||
9. Add screenshots and feature graphic.
|
||||
10. Submit to internal testing first when possible.
|
||||
@@ -0,0 +1,57 @@
|
||||
# Google Play Store Listing
|
||||
|
||||
## App Name
|
||||
|
||||
Focus Seed
|
||||
|
||||
## Short Description
|
||||
|
||||
Private focus timer, habit tracker, and one-minute daily reflection.
|
||||
|
||||
## Full Description
|
||||
|
||||
Focus Seed helps you turn each day into a small, trackable win. Choose today’s anchor task, start a focused timer, mark habits as done, and write a short reflection when you finish.
|
||||
|
||||
Everything works locally on your device. No account is required, and your focus sessions, habits, and reflections stay on your phone unless you choose to export them.
|
||||
|
||||
Features:
|
||||
|
||||
- Simple 15, 25, and 45 minute focus timer
|
||||
- Daily anchor task
|
||||
- Habit tracking
|
||||
- One-minute reflection log
|
||||
- Mood tags
|
||||
- Local 7-day progress view
|
||||
- JSON export and import
|
||||
- Clear local-data privacy controls
|
||||
|
||||
Focus Seed is designed for people who want a lightweight productivity tool without accounts, feeds, or complicated setup.
|
||||
|
||||
## Category
|
||||
|
||||
Productivity
|
||||
|
||||
## Developer / Support
|
||||
|
||||
- Developer / brand: leavysong
|
||||
- Support email: leavysong@gmail.com
|
||||
|
||||
## Graphic Assets
|
||||
|
||||
- Play listing icon: `launch/screenshots/focus-seed-icon-512-play.png`
|
||||
- Feature graphic: `launch/screenshots/focus-seed-feature-graphic-1024x500.png`
|
||||
|
||||
## Tags / Positioning Keywords
|
||||
|
||||
focus timer, habit tracker, reflection, productivity, offline, private, daily routine
|
||||
|
||||
## Release Notes
|
||||
|
||||
First release:
|
||||
|
||||
- Focus timer
|
||||
- Habit tracker
|
||||
- Daily reflection
|
||||
- Local progress stats
|
||||
- Local JSON export/import
|
||||
- In-app privacy controls
|
||||
@@ -0,0 +1,32 @@
|
||||
# Upload Key Notes
|
||||
|
||||
An upload keystore was generated for this app:
|
||||
|
||||
```text
|
||||
android/focus-seed-upload.jks
|
||||
```
|
||||
|
||||
Gradle signing config:
|
||||
|
||||
```text
|
||||
android/key.properties
|
||||
```
|
||||
|
||||
Alias:
|
||||
|
||||
```text
|
||||
focusseed_upload
|
||||
```
|
||||
|
||||
SHA-256 fingerprint:
|
||||
|
||||
```text
|
||||
BA:5D:72:B4:8F:03:E4:92:99:E1:01:9F:F7:E6:40:61:54:22:73:0D:6A:E1:C2:7B:34:E2:66:73:80:99:30:DA
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
- Keep this keystore safe.
|
||||
- Do not publish it publicly.
|
||||
- Do not lose it after uploading the app; future updates need the same upload key unless you reset it in Play Console.
|
||||
- For a real brand, you may regenerate the upload key before the first Play upload.
|
||||
@@ -0,0 +1,100 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Focus Seed Privacy Policy</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--primary: #2e7d6b;
|
||||
--bg: #f7f8f5;
|
||||
--text: #17211e;
|
||||
--muted: #5d6b66;
|
||||
--card: #ffffff;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #101816;
|
||||
--text: #eef5f1;
|
||||
--muted: #b6c4bf;
|
||||
--card: #1b2522;
|
||||
}
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
}
|
||||
main {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px 56px;
|
||||
}
|
||||
article {
|
||||
background: var(--card);
|
||||
border-radius: 8px;
|
||||
padding: 28px;
|
||||
}
|
||||
h1, h2 {
|
||||
line-height: 1.2;
|
||||
}
|
||||
h1 {
|
||||
color: var(--primary);
|
||||
margin-top: 0;
|
||||
}
|
||||
h2 {
|
||||
margin-top: 28px;
|
||||
}
|
||||
.muted {
|
||||
color: var(--muted);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<article>
|
||||
<h1>Focus Seed Privacy Policy</h1>
|
||||
<p class="muted">Effective date: June 9, 2026</p>
|
||||
|
||||
<p>Focus Seed is designed to work privately on your device.</p>
|
||||
|
||||
<h2>Data We Collect</h2>
|
||||
<p>Focus Seed V1 does not collect personal data through a server, account system, advertising SDK, analytics SDK, or billing SDK.</p>
|
||||
|
||||
<h2>Local Data Stored On Your Device</h2>
|
||||
<p>The app stores the following data locally on your device:</p>
|
||||
<ul>
|
||||
<li>Today anchor task</li>
|
||||
<li>Habit names and completion dates</li>
|
||||
<li>Focus session completion time and duration</li>
|
||||
<li>Reflection notes and mood tags</li>
|
||||
<li>Basic app state needed for the app to work</li>
|
||||
</ul>
|
||||
|
||||
<h2>Data Sharing</h2>
|
||||
<p>Focus Seed V1 does not share your app data with the developer or third parties.</p>
|
||||
|
||||
<h2>Export And Import</h2>
|
||||
<p>You can export your local data as JSON from the app settings. You control where that exported text is copied or stored. You can also paste a previous JSON export into the app to import it.</p>
|
||||
|
||||
<h2>Data Deletion</h2>
|
||||
<p>You can clear local app data from Settings. You can also remove data by uninstalling the app or clearing app storage in Android settings.</p>
|
||||
|
||||
<h2>Ads, Analytics, And Purchases</h2>
|
||||
<p>Focus Seed V1 does not include advertising SDKs, analytics SDKs, or in-app purchase SDKs.</p>
|
||||
|
||||
<h2>Children</h2>
|
||||
<p>Focus Seed is not directed to children.</p>
|
||||
|
||||
<h2>Changes</h2>
|
||||
<p>If this policy changes, the updated version will be made available in the app listing and, where appropriate, inside the app.</p>
|
||||
|
||||
<h2>Contact</h2>
|
||||
<p>Contact email: leavysong@gmail.com</p>
|
||||
</article>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,47 @@
|
||||
# Privacy Policy
|
||||
|
||||
Effective date: 2026-06-09
|
||||
|
||||
Focus Seed is designed to work privately on your device.
|
||||
|
||||
## Data We Collect
|
||||
|
||||
Focus Seed V1 does not collect personal data through a server, account system, advertising SDK, analytics SDK, or billing SDK.
|
||||
|
||||
## Local Data Stored On Your Device
|
||||
|
||||
The app stores the following data locally on your device:
|
||||
|
||||
- Today anchor task
|
||||
- Habit names and completion dates
|
||||
- Focus session completion time and duration
|
||||
- Reflection notes and mood tags
|
||||
- Basic app state needed for the app to work
|
||||
|
||||
## Data Sharing
|
||||
|
||||
Focus Seed V1 does not share your app data with the developer or third parties.
|
||||
|
||||
## Export And Import
|
||||
|
||||
You can export your local data as JSON from the app settings. You control where that exported text is copied or stored. You can also paste a previous JSON export into the app to import it.
|
||||
|
||||
## Data Deletion
|
||||
|
||||
You can clear local app data from Settings. You can also remove data by uninstalling the app or clearing app storage in Android settings.
|
||||
|
||||
## Ads, Analytics, And Purchases
|
||||
|
||||
Focus Seed V1 does not include advertising SDKs, analytics SDKs, or in-app purchase SDKs.
|
||||
|
||||
## Children
|
||||
|
||||
Focus Seed is not directed to children.
|
||||
|
||||
## Changes
|
||||
|
||||
If this policy changes, the updated version will be made available in the app listing and, where appropriate, inside the app.
|
||||
|
||||
## Contact
|
||||
|
||||
Contact email: leavysong@gmail.com
|
||||
@@ -0,0 +1,64 @@
|
||||
# Focus Seed Product Brief
|
||||
|
||||
Status: V1 release candidate
|
||||
|
||||
## Positioning
|
||||
|
||||
Focus Seed is a private, offline-first focus timer, habit tracker, and one-minute reflection app for people who want a quiet daily productivity loop without accounts, feeds, or setup friction.
|
||||
|
||||
## V1 Scope
|
||||
|
||||
- Today anchor task.
|
||||
- 15 / 25 / 45 minute focus timer.
|
||||
- Habit creation, rename, delete, and daily check-off.
|
||||
- One-minute reflection with mood tag.
|
||||
- 7-day progress pulse.
|
||||
- Local JSON export and import by copy/paste.
|
||||
- In-app privacy explanation.
|
||||
- No-op monetization and analytics interfaces.
|
||||
- English-only UI and store listing for the first launch wave.
|
||||
|
||||
## V1 Exclusions
|
||||
|
||||
- No account sign-in.
|
||||
- No server.
|
||||
- No cloud sync.
|
||||
- No real ad SDK.
|
||||
- No billing SDK.
|
||||
- No analytics SDK.
|
||||
- No push notifications.
|
||||
- No sensitive permissions.
|
||||
- No Korean, Japanese, EU/EEA-specific, or other regional localization in V1.
|
||||
|
||||
## Region Strategy
|
||||
|
||||
Focus Seed is designed as a global product, but V1 should launch in a controlled English-first set of markets instead of every country.
|
||||
|
||||
Recommended first countries/regions for production or closed testing:
|
||||
|
||||
- United States
|
||||
- Canada
|
||||
- Australia
|
||||
- New Zealand
|
||||
- Singapore
|
||||
- India
|
||||
- Philippines
|
||||
- Malaysia
|
||||
|
||||
Defer EU/EEA, United Kingdom, Switzerland, South Korea, Japan, Brazil/LATAM, and MENA until localization, privacy wording, cultural QA, and any monetization requirements are reviewed for those regions.
|
||||
|
||||
## Future Revenue Path
|
||||
|
||||
- V2/V3: optional low-interruption ads after retention validation.
|
||||
- V4/V5: one-time unlock or subscription for advanced statistics, themes, export templates, widgets, and backup/sync.
|
||||
- Free core loop remains usable.
|
||||
|
||||
## App IDs
|
||||
|
||||
- App name: Focus Seed
|
||||
- Developer / brand: leavysong
|
||||
- Support email: leavysong@gmail.com
|
||||
- Package/application ID: `com.dailyanchor.focusseed`
|
||||
- Version: `1.0.0+1`
|
||||
- Android min SDK: 26
|
||||
- Android target SDK: 35
|
||||
@@ -0,0 +1,38 @@
|
||||
# Focus Seed 1.0.0 Release Record
|
||||
|
||||
Release status: sealed Google Play testing build.
|
||||
|
||||
## Identity
|
||||
|
||||
- Version name: `1.0.0`
|
||||
- Version code: `1`
|
||||
- Package ID: `com.dailyanchor.focusseed`
|
||||
- Git tag: `v1.0.0`
|
||||
- Android min SDK: `26`
|
||||
- Android target SDK: `35`
|
||||
|
||||
## Artifact
|
||||
|
||||
- Local path: `build/app/outputs/bundle/release/app-release.aab`
|
||||
- Size: `22,168,710` bytes
|
||||
- SHA-256: `2987C5403C88CA7A37FD05C5C77C285B650F21056450690360633242D6B92385`
|
||||
|
||||
The AAB is intentionally not stored in Git. Verify any archived copy against this checksum before uploading or retaining it as the sealed 1.0.0 artifact.
|
||||
|
||||
## Verification
|
||||
|
||||
- `flutter analyze`: passed
|
||||
- `flutter test`: passed
|
||||
- `flutter build appbundle --release`: passed
|
||||
- Android 14 release smoke test: passed
|
||||
- Real-device screenshots captured
|
||||
- Manifest permission review: passed
|
||||
|
||||
## Privacy And Monetization
|
||||
|
||||
- No account or server
|
||||
- No ads or Advertising ID
|
||||
- No billing or purchases
|
||||
- No analytics or crash-reporting SDK
|
||||
- No network permission
|
||||
- Local-only user data with user-controlled export
|
||||
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 143 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 43 KiB |
@@ -0,0 +1,47 @@
|
||||
# Screenshot Plan
|
||||
|
||||
Use real app screenshots from the release build.
|
||||
|
||||
Captured on MT2110 / Android 14:
|
||||
|
||||
- `device-01-today.png`
|
||||
- `device-02-timer.png`
|
||||
- `device-03-habits.png`
|
||||
- `device-04-reflect.png`
|
||||
- `device-05-settings-clean.png`
|
||||
|
||||
Use these as the first Play Console screenshot candidates. Avoid older `device-05-settings.png` because it showed internal monetization placeholder wording that has now been removed from the app.
|
||||
|
||||
Recommended phone screenshots:
|
||||
|
||||
1. Today screen with anchor task and daily metrics.
|
||||
2. Focus timer with progress ring.
|
||||
3. Habits screen with completed and pending habits.
|
||||
4. Reflection screen with mood tags and text area.
|
||||
5. Settings screen showing local data export/import and privacy.
|
||||
|
||||
Suggested captions:
|
||||
|
||||
- Plan one clear task for today.
|
||||
- Start a calm focus session.
|
||||
- Track tiny habits without an account.
|
||||
- Finish with a one-minute reflection.
|
||||
- Keep your data local and export when needed.
|
||||
|
||||
Feature graphic direction:
|
||||
|
||||
- Size: 1024x500.
|
||||
- Visual: calm green/teal background, Focus Seed name, simple phone mockup showing timer/progress.
|
||||
- Copy: "Focus. Track. Reflect."
|
||||
- Generated asset: `launch/screenshots/focus-seed-feature-graphic-1024x500.png`
|
||||
|
||||
Icon:
|
||||
|
||||
- Generated 512x512 asset: `launch/screenshots/focus-seed-icon-512.png`
|
||||
- Play Console listing icon without transparent corners: `launch/screenshots/focus-seed-icon-512-play.png`
|
||||
|
||||
Avoid:
|
||||
|
||||
- Claims about medical benefit.
|
||||
- Subscription or ad messaging in V1 screenshots.
|
||||
- Screens that show fake cloud sync or features not in the app.
|
||||
@@ -0,0 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'src/app/focus_seed_app.dart';
|
||||
|
||||
void main() {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
runApp(const FocusSeedApp());
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
static const seed = Color(0xFF2E7D6B);
|
||||
static const accent = Color(0xFFF4B740);
|
||||
static const paper = Color(0xFFF7F8F5);
|
||||
|
||||
static ThemeData light() {
|
||||
final scheme = ColorScheme.fromSeed(seedColor: seed, surface: paper);
|
||||
return _base(scheme);
|
||||
}
|
||||
|
||||
static ThemeData dark() {
|
||||
final scheme = ColorScheme.fromSeed(
|
||||
seedColor: seed,
|
||||
brightness: Brightness.dark,
|
||||
);
|
||||
return _base(scheme);
|
||||
}
|
||||
|
||||
static ThemeData _base(ColorScheme scheme) {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: scheme,
|
||||
scaffoldBackgroundColor: scheme.surface,
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
indicatorColor: scheme.primaryContainer,
|
||||
labelTextStyle: WidgetStateProperty.resolveWith(
|
||||
(_) => const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
color: scheme.surfaceContainerHighest.withValues(alpha: 0.55),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
filledButtonTheme: FilledButtonThemeData(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(48, 48),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
minimumSize: const Size(48, 48),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../features/shell/focus_seed_home.dart';
|
||||
import 'app_theme.dart';
|
||||
|
||||
class FocusSeedApp extends StatelessWidget {
|
||||
const FocusSeedApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Focus Seed',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.light(),
|
||||
darkTheme: AppTheme.dark(),
|
||||
home: const FocusSeedHome(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import '../models/focus_session.dart';
|
||||
import '../models/habit.dart';
|
||||
import '../models/reflection_entry.dart';
|
||||
|
||||
String todayKey() => dateKey(DateTime.now());
|
||||
|
||||
String dateKey(DateTime date) {
|
||||
final local = DateTime(date.year, date.month, date.day);
|
||||
return '${local.year.toString().padLeft(4, '0')}-'
|
||||
'${local.month.toString().padLeft(2, '0')}-'
|
||||
'${local.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String readableToday() {
|
||||
const months = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
'Mar',
|
||||
'Apr',
|
||||
'May',
|
||||
'Jun',
|
||||
'Jul',
|
||||
'Aug',
|
||||
'Sep',
|
||||
'Oct',
|
||||
'Nov',
|
||||
'Dec',
|
||||
];
|
||||
final now = DateTime.now();
|
||||
return '${months[now.month - 1]} ${now.day}, ${now.year}';
|
||||
}
|
||||
|
||||
List<String> lastSevenDateKeys() {
|
||||
final now = DateTime.now();
|
||||
return List.generate(
|
||||
7,
|
||||
(index) => dateKey(now.subtract(Duration(days: 6 - index))),
|
||||
);
|
||||
}
|
||||
|
||||
int minutesForDate(List<FocusSession> sessions, String key) {
|
||||
return sessions
|
||||
.where((session) => dateKey(session.completedAt) == key)
|
||||
.fold<int>(0, (total, session) => total + session.minutes);
|
||||
}
|
||||
|
||||
int currentStreak(
|
||||
List<Habit> habits,
|
||||
List<FocusSession> sessions,
|
||||
List<ReflectionEntry> reflections,
|
||||
) {
|
||||
var streak = 0;
|
||||
var cursor = DateTime.now();
|
||||
while (true) {
|
||||
final key = dateKey(cursor);
|
||||
final hasHabit = habits.any((habit) => habit.completedDates.contains(key));
|
||||
final hasSession = sessions.any(
|
||||
(session) => dateKey(session.completedAt) == key,
|
||||
);
|
||||
final hasReflection = reflections.any(
|
||||
(reflection) => reflection.dateKey == key,
|
||||
);
|
||||
if (!hasHabit && !hasSession && !hasReflection) {
|
||||
return streak;
|
||||
}
|
||||
streak += 1;
|
||||
cursor = cursor.subtract(const Duration(days: 1));
|
||||
}
|
||||
}
|
||||
|
||||
String formatSeconds(int seconds) {
|
||||
final minutes = seconds ~/ 60;
|
||||
final remaining = seconds % 60;
|
||||
return '${minutes.toString().padLeft(2, '0')}:'
|
||||
'${remaining.toString().padLeft(2, '0')}';
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
extension FirstOrNull<T> on Iterable<T> {
|
||||
T? get firstOrNull {
|
||||
final iterator = this.iterator;
|
||||
if (iterator.moveNext()) {
|
||||
return iterator.current;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
String prettyJson(Map<String, dynamic> value) {
|
||||
return const JsonEncoder.withIndent(' ').convert(value);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../core/json_utils.dart';
|
||||
import '../models/app_state.dart';
|
||||
|
||||
class FocusSeedStore {
|
||||
static const _storageKey = 'focus_seed_state_v1';
|
||||
|
||||
Future<AppState> load() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final raw = prefs.getString(_storageKey);
|
||||
if (raw == null || raw.isEmpty) {
|
||||
return AppState.starter();
|
||||
}
|
||||
try {
|
||||
return AppState.fromJson(jsonDecode(raw) as Map<String, dynamic>);
|
||||
} on Object {
|
||||
return AppState.starter();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> save(AppState state) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_storageKey, prettyJson(state.toJson()));
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_storageKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/date_keys.dart';
|
||||
import '../../models/app_state.dart';
|
||||
import '../../models/habit.dart';
|
||||
import '../../widgets/app_scaffold.dart';
|
||||
import '../../widgets/dialogs.dart';
|
||||
import '../../widgets/empty_state.dart';
|
||||
import '../../widgets/habit_row.dart';
|
||||
|
||||
class HabitsScreen extends StatelessWidget {
|
||||
const HabitsScreen({
|
||||
required this.state,
|
||||
required this.onAddHabit,
|
||||
required this.onRenameHabit,
|
||||
required this.onDeleteHabit,
|
||||
required this.onToggleHabit,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final AppState state;
|
||||
final ValueChanged<String> onAddHabit;
|
||||
final void Function(Habit habit, String title) onRenameHabit;
|
||||
final ValueChanged<Habit> onDeleteHabit;
|
||||
final ValueChanged<Habit> onToggleHabit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final today = todayKey();
|
||||
return AppScaffold(
|
||||
title: 'Habits',
|
||||
subtitle: 'Keep the list small and repeatable.',
|
||||
action: IconButton.filledTonal(
|
||||
tooltip: 'Add habit',
|
||||
onPressed: () => showHabitDialog(context, onAddHabit),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
|
||||
children: [
|
||||
if (state.habits.isEmpty)
|
||||
EmptyState(
|
||||
icon: Icons.checklist_outlined,
|
||||
title: 'No habits yet',
|
||||
action: 'Add habit',
|
||||
onAction: () => showHabitDialog(context, onAddHabit),
|
||||
)
|
||||
else
|
||||
...state.habits.map(
|
||||
(habit) => HabitRow(
|
||||
habit: habit,
|
||||
dateKey: today,
|
||||
onToggle: () => onToggleHabit(habit),
|
||||
onEdit:
|
||||
() => showHabitDialog(
|
||||
context,
|
||||
(title) => onRenameHabit(habit, title),
|
||||
initialValue: habit.title,
|
||||
),
|
||||
onDelete:
|
||||
() =>
|
||||
confirmDeleteHabit(context, () => onDeleteHabit(habit)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../app/app_theme.dart';
|
||||
import '../../core/date_keys.dart';
|
||||
import '../../models/app_state.dart';
|
||||
import '../../models/habit.dart';
|
||||
import '../../widgets/app_scaffold.dart';
|
||||
import '../../widgets/dialogs.dart';
|
||||
import '../../widgets/empty_state.dart';
|
||||
import '../../widgets/habit_row.dart';
|
||||
import '../../widgets/inline_editable_text.dart';
|
||||
import '../../widgets/metric_tile.dart';
|
||||
import '../../widgets/section_label.dart';
|
||||
import '../../widgets/seed_card.dart';
|
||||
import '../../widgets/weekly_progress.dart';
|
||||
|
||||
class HomeScreen extends StatelessWidget {
|
||||
const HomeScreen({
|
||||
required this.state,
|
||||
required this.onAnchorChanged,
|
||||
required this.onOpenTimer,
|
||||
required this.onToggleHabit,
|
||||
required this.onAddHabit,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final AppState state;
|
||||
final ValueChanged<String> onAnchorChanged;
|
||||
final VoidCallback onOpenTimer;
|
||||
final ValueChanged<Habit> onToggleHabit;
|
||||
final ValueChanged<String> onAddHabit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final today = todayKey();
|
||||
final completed =
|
||||
state.habits
|
||||
.where((habit) => habit.completedDates.contains(today))
|
||||
.length;
|
||||
final focusMinutes = minutesForDate(state.sessions, today);
|
||||
final streak = currentStreak(
|
||||
state.habits,
|
||||
state.sessions,
|
||||
state.reflections,
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
title: 'Focus Seed',
|
||||
subtitle: readableToday(),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
|
||||
children: [
|
||||
SeedCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionLabel(
|
||||
icon: Icons.flag_outlined,
|
||||
text: 'Today anchor',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
InlineEditableText(
|
||||
value: state.anchor,
|
||||
emptyText: 'Choose one important task',
|
||||
onSubmitted: onAnchorChanged,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton.icon(
|
||||
onPressed: onOpenTimer,
|
||||
icon: const Icon(Icons.play_arrow_rounded),
|
||||
label: const Text('Start focus'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: MetricTile(
|
||||
label: 'Focus',
|
||||
value: '$focusMinutes',
|
||||
suffix: 'min',
|
||||
icon: Icons.timer_outlined,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: MetricTile(
|
||||
label: 'Habits',
|
||||
value: '$completed/${state.habits.length}',
|
||||
suffix: '',
|
||||
icon: Icons.check_circle_outline,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: MetricTile(
|
||||
label: 'Streak',
|
||||
value: '$streak',
|
||||
suffix: 'days',
|
||||
icon: Icons.local_fire_department_outlined,
|
||||
accent: AppTheme.accent,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Today habits',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
IconButton.filledTonal(
|
||||
tooltip: 'Add habit',
|
||||
onPressed: () => showHabitDialog(context, onAddHabit),
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (state.habits.isEmpty)
|
||||
EmptyState(
|
||||
icon: Icons.checklist_rtl,
|
||||
title: 'Add one tiny habit',
|
||||
action: 'Add habit',
|
||||
onAction: () => showHabitDialog(context, onAddHabit),
|
||||
)
|
||||
else
|
||||
...state.habits
|
||||
.take(5)
|
||||
.map(
|
||||
(habit) => HabitRow(
|
||||
habit: habit,
|
||||
dateKey: today,
|
||||
onToggle: () => onToggleHabit(habit),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
WeeklyProgress(state: state),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/date_keys.dart';
|
||||
import '../../core/iterable_extensions.dart';
|
||||
import '../../models/app_state.dart';
|
||||
import '../../models/mood.dart';
|
||||
import '../../widgets/app_scaffold.dart';
|
||||
import '../../widgets/section_label.dart';
|
||||
import '../../widgets/seed_card.dart';
|
||||
|
||||
class ReflectionScreen extends StatefulWidget {
|
||||
const ReflectionScreen({
|
||||
required this.state,
|
||||
required this.onSave,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final AppState state;
|
||||
final void Function(String note, Mood mood) onSave;
|
||||
|
||||
@override
|
||||
State<ReflectionScreen> createState() => _ReflectionScreenState();
|
||||
}
|
||||
|
||||
class _ReflectionScreenState extends State<ReflectionScreen> {
|
||||
final _controller = TextEditingController();
|
||||
Mood _mood = Mood.clear;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final today = todayKey();
|
||||
final todayReflection =
|
||||
widget.state.reflections
|
||||
.where((item) => item.dateKey == today)
|
||||
.firstOrNull;
|
||||
if (todayReflection != null && _controller.text.isEmpty) {
|
||||
_controller.text = todayReflection.note;
|
||||
_mood = todayReflection.mood;
|
||||
}
|
||||
|
||||
return AppScaffold(
|
||||
title: 'Reflection',
|
||||
subtitle: 'One sentence is enough.',
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
|
||||
children: [
|
||||
SeedCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SectionLabel(
|
||||
icon: Icons.psychology_outlined,
|
||||
text: 'How did it feel?',
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children:
|
||||
Mood.values
|
||||
.map(
|
||||
(mood) => ChoiceChip(
|
||||
avatar: Icon(mood.icon, size: 18),
|
||||
label: Text(mood.label),
|
||||
selected: _mood == mood,
|
||||
onSelected: (_) => setState(() => _mood = mood),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
TextField(
|
||||
controller: _controller,
|
||||
minLines: 4,
|
||||
maxLines: 7,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: 'What made today a little better?',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
FilledButton.icon(
|
||||
onPressed: () => widget.onSave(_controller.text, _mood),
|
||||
icon: const Icon(Icons.save_outlined),
|
||||
label: const Text('Save reflection'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text('Recent notes', style: Theme.of(context).textTheme.titleMedium),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.state.reflections.isEmpty)
|
||||
const SeedCard(
|
||||
child: Text(
|
||||
'Your reflections will appear here after you save one.',
|
||||
),
|
||||
)
|
||||
else
|
||||
...widget.state.reflections.reversed
|
||||
.take(7)
|
||||
.map(
|
||||
(entry) => SeedCard(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: Icon(entry.mood.icon),
|
||||
title: Text(entry.note),
|
||||
subtitle: Text(entry.dateKey),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../models/app_state.dart';
|
||||
import '../../widgets/app_scaffold.dart';
|
||||
import '../../widgets/dialogs.dart';
|
||||
import '../../widgets/seed_card.dart';
|
||||
import '../../widgets/settings_tile.dart';
|
||||
|
||||
class SettingsScreen extends StatelessWidget {
|
||||
const SettingsScreen({
|
||||
required this.state,
|
||||
required this.onImport,
|
||||
required this.onClear,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final AppState state;
|
||||
final ValueChanged<String> onImport;
|
||||
final VoidCallback onClear;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppScaffold(
|
||||
title: 'Settings',
|
||||
subtitle: 'Private by default. No account required.',
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 24),
|
||||
children: [
|
||||
SeedCard(
|
||||
child: Column(
|
||||
children: [
|
||||
SettingsTile(
|
||||
icon: Icons.ios_share,
|
||||
title: 'Export JSON',
|
||||
subtitle: 'Copy your local data as readable JSON.',
|
||||
onTap: () => showExportDialog(context, state),
|
||||
),
|
||||
const Divider(),
|
||||
SettingsTile(
|
||||
icon: Icons.file_upload_outlined,
|
||||
title: 'Import JSON',
|
||||
subtitle: 'Paste a previous Focus Seed export.',
|
||||
onTap: () => showImportDialog(context, onImport),
|
||||
),
|
||||
const Divider(),
|
||||
SettingsTile(
|
||||
icon: Icons.privacy_tip_outlined,
|
||||
title: 'Privacy',
|
||||
subtitle:
|
||||
'Your data stays on this device unless you export it.',
|
||||
onTap: () => showPrivacySheet(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
SeedCard(
|
||||
child: SettingsTile(
|
||||
icon: Icons.delete_outline,
|
||||
title: 'Clear local data',
|
||||
subtitle: 'Reset habits, sessions, and reflections.',
|
||||
destructive: true,
|
||||
onTap: () => confirmClearData(context, onClear),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../core/date_keys.dart';
|
||||
import '../../data/focus_seed_store.dart';
|
||||
import '../../models/app_state.dart';
|
||||
import '../../models/focus_session.dart';
|
||||
import '../../models/habit.dart';
|
||||
import '../../models/mood.dart';
|
||||
import '../../models/reflection_entry.dart';
|
||||
import '../../services/analytics_provider.dart';
|
||||
import '../../widgets/dialogs.dart';
|
||||
import '../../widgets/loading_view.dart';
|
||||
import '../habits/habits_screen.dart';
|
||||
import '../home/home_screen.dart';
|
||||
import '../reflection/reflection_screen.dart';
|
||||
import '../settings/settings_screen.dart';
|
||||
import '../timer/timer_screen.dart';
|
||||
|
||||
class FocusSeedHome extends StatefulWidget {
|
||||
const FocusSeedHome({super.key});
|
||||
|
||||
@override
|
||||
State<FocusSeedHome> createState() => _FocusSeedHomeState();
|
||||
}
|
||||
|
||||
class _FocusSeedHomeState extends State<FocusSeedHome> {
|
||||
final _store = FocusSeedStore();
|
||||
final _analytics = const NoopAnalyticsProvider();
|
||||
AppState? _state;
|
||||
int _tabIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
final state = await _store.load();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() => _state = state);
|
||||
}
|
||||
|
||||
Future<void> _setStateAndSave(AppState state) async {
|
||||
setState(() => _state = state);
|
||||
await _store.save(state);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = _state;
|
||||
return Scaffold(
|
||||
body: AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 180),
|
||||
child:
|
||||
state == null
|
||||
? const LoadingView()
|
||||
: SafeArea(
|
||||
child: IndexedStack(
|
||||
index: _tabIndex,
|
||||
children: [
|
||||
HomeScreen(
|
||||
state: state,
|
||||
onAnchorChanged:
|
||||
(anchor) => _setStateAndSave(
|
||||
state.copyWith(anchor: anchor.trim()),
|
||||
),
|
||||
onOpenTimer: () => setState(() => _tabIndex = 1),
|
||||
onToggleHabit: _toggleHabit,
|
||||
onAddHabit: _addHabit,
|
||||
),
|
||||
TimerScreen(
|
||||
onComplete: _completeFocusSession,
|
||||
onReflect: () => setState(() => _tabIndex = 3),
|
||||
),
|
||||
HabitsScreen(
|
||||
state: state,
|
||||
onAddHabit: _addHabit,
|
||||
onRenameHabit: _renameHabit,
|
||||
onDeleteHabit: _deleteHabit,
|
||||
onToggleHabit: _toggleHabit,
|
||||
),
|
||||
ReflectionScreen(state: state, onSave: _saveReflection),
|
||||
SettingsScreen(
|
||||
state: state,
|
||||
onImport: _importState,
|
||||
onClear: _clearData,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar:
|
||||
state == null
|
||||
? null
|
||||
: NavigationBar(
|
||||
selectedIndex: _tabIndex,
|
||||
onDestinationSelected: (index) {
|
||||
HapticFeedback.selectionClick();
|
||||
setState(() => _tabIndex = index);
|
||||
},
|
||||
destinations: const [
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.home_outlined),
|
||||
selectedIcon: Icon(Icons.home),
|
||||
label: 'Today',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.timer_outlined),
|
||||
selectedIcon: Icon(Icons.timer),
|
||||
label: 'Timer',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.check_circle_outline),
|
||||
selectedIcon: Icon(Icons.check_circle),
|
||||
label: 'Habits',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.edit_note_outlined),
|
||||
selectedIcon: Icon(Icons.edit_note),
|
||||
label: 'Reflect',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
selectedIcon: Icon(Icons.settings),
|
||||
label: 'Settings',
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _toggleHabit(Habit habit) async {
|
||||
final state = _state;
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
final today = todayKey();
|
||||
final completedDates = Set<String>.from(habit.completedDates);
|
||||
if (completedDates.contains(today)) {
|
||||
completedDates.remove(today);
|
||||
} else {
|
||||
completedDates.add(today);
|
||||
await HapticFeedback.lightImpact();
|
||||
}
|
||||
final habits =
|
||||
state.habits
|
||||
.map(
|
||||
(item) =>
|
||||
item.id == habit.id
|
||||
? habit.copyWith(completedDates: completedDates)
|
||||
: item,
|
||||
)
|
||||
.toList();
|
||||
_analytics.track('habit_toggled');
|
||||
await _setStateAndSave(state.copyWith(habits: habits));
|
||||
}
|
||||
|
||||
Future<void> _addHabit(String title) async {
|
||||
final state = _state;
|
||||
final clean = title.trim();
|
||||
if (state == null || clean.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final habit = Habit(
|
||||
id: 'habit-${DateTime.now().microsecondsSinceEpoch}',
|
||||
title: clean,
|
||||
createdAt: DateTime.now(),
|
||||
completedDates: {},
|
||||
);
|
||||
_analytics.track('habit_created');
|
||||
await _setStateAndSave(state.copyWith(habits: [...state.habits, habit]));
|
||||
}
|
||||
|
||||
Future<void> _renameHabit(Habit habit, String title) async {
|
||||
final state = _state;
|
||||
final clean = title.trim();
|
||||
if (state == null || clean.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final habits =
|
||||
state.habits
|
||||
.map(
|
||||
(item) =>
|
||||
item.id == habit.id ? item.copyWith(title: clean) : item,
|
||||
)
|
||||
.toList();
|
||||
await _setStateAndSave(state.copyWith(habits: habits));
|
||||
}
|
||||
|
||||
Future<void> _deleteHabit(Habit habit) async {
|
||||
final state = _state;
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
final habits = state.habits.where((item) => item.id != habit.id).toList();
|
||||
await _setStateAndSave(state.copyWith(habits: habits));
|
||||
}
|
||||
|
||||
Future<void> _completeFocusSession(int minutes) async {
|
||||
final state = _state;
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
final session = FocusSession(
|
||||
id: 'session-${DateTime.now().microsecondsSinceEpoch}',
|
||||
minutes: minutes,
|
||||
completedAt: DateTime.now(),
|
||||
);
|
||||
_analytics.track('focus_completed', {'minutes': minutes});
|
||||
await _setStateAndSave(
|
||||
state.copyWith(sessions: [...state.sessions, session]),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveReflection(String note, Mood mood) async {
|
||||
final state = _state;
|
||||
final clean = note.trim();
|
||||
if (state == null || clean.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final today = todayKey();
|
||||
final entry = ReflectionEntry(
|
||||
id: 'reflection-${DateTime.now().microsecondsSinceEpoch}',
|
||||
dateKey: today,
|
||||
note: clean,
|
||||
mood: mood,
|
||||
createdAt: DateTime.now(),
|
||||
);
|
||||
final reflections = [
|
||||
...state.reflections.where((item) => item.dateKey != today),
|
||||
entry,
|
||||
];
|
||||
_analytics.track('reflection_saved');
|
||||
await _setStateAndSave(state.copyWith(reflections: reflections));
|
||||
if (mounted) {
|
||||
showSnack(context, 'Reflection saved');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _importState(String raw) async {
|
||||
try {
|
||||
final imported = AppState.fromJson(
|
||||
jsonDecode(raw) as Map<String, dynamic>,
|
||||
);
|
||||
await _setStateAndSave(imported);
|
||||
if (mounted) {
|
||||
showSnack(context, 'Data imported');
|
||||
}
|
||||
} on Object {
|
||||
if (mounted) {
|
||||
showSnack(context, 'Import failed. Check the JSON and try again.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearData() async {
|
||||
await _store.clear();
|
||||
await _setStateAndSave(AppState.starter());
|
||||
if (mounted) {
|
||||
showSnack(context, 'Local data cleared');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../../core/date_keys.dart';
|
||||
import '../../widgets/app_scaffold.dart';
|
||||
import '../../widgets/seed_card.dart';
|
||||
|
||||
class TimerScreen extends StatefulWidget {
|
||||
const TimerScreen({
|
||||
required this.onComplete,
|
||||
required this.onReflect,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ValueChanged<int> onComplete;
|
||||
final VoidCallback onReflect;
|
||||
|
||||
@override
|
||||
State<TimerScreen> createState() => _TimerScreenState();
|
||||
}
|
||||
|
||||
class _TimerScreenState extends State<TimerScreen> {
|
||||
static const _presets = [15, 25, 45];
|
||||
Timer? _timer;
|
||||
int _selectedMinutes = 25;
|
||||
int _remainingSeconds = 25 * 60;
|
||||
bool _running = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final totalSeconds = _selectedMinutes * 60;
|
||||
final progress = 1 - (_remainingSeconds / totalSeconds);
|
||||
return AppScaffold(
|
||||
title: 'Focus timer',
|
||||
subtitle: 'No ads or distractions here.',
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
|
||||
children: [
|
||||
SeedCard(
|
||||
child: Column(
|
||||
children: [
|
||||
Wrap(
|
||||
alignment: WrapAlignment.center,
|
||||
spacing: 8,
|
||||
children:
|
||||
_presets
|
||||
.map(
|
||||
(minutes) => ChoiceChip(
|
||||
label: Text('$minutes min'),
|
||||
selected: _selectedMinutes == minutes,
|
||||
onSelected:
|
||||
_running
|
||||
? null
|
||||
: (_) => setState(() {
|
||||
_selectedMinutes = minutes;
|
||||
_remainingSeconds = minutes * 60;
|
||||
}),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
TweenAnimationBuilder<double>(
|
||||
tween: Tween(begin: 0, end: progress.clamp(0, 1)),
|
||||
duration: const Duration(milliseconds: 240),
|
||||
builder:
|
||||
(context, value, child) => SizedBox.square(
|
||||
dimension: 220,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: value,
|
||||
strokeWidth: 14,
|
||||
strokeCap: StrokeCap.round,
|
||||
backgroundColor:
|
||||
Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child!,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
formatSeconds(_remainingSeconds),
|
||||
style: Theme.of(context).textTheme.displayMedium
|
||||
?.copyWith(fontWeight: FontWeight.w800),
|
||||
),
|
||||
Text(
|
||||
_running ? 'Focus in progress' : 'Ready when you are',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 28),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FilledButton.icon(
|
||||
onPressed: _running ? _pause : _start,
|
||||
icon: Icon(_running ? Icons.pause : Icons.play_arrow),
|
||||
label: Text(_running ? 'Pause' : 'Start'),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
IconButton.filledTonal(
|
||||
tooltip: 'Reset timer',
|
||||
onPressed: _reset,
|
||||
icon: const Icon(Icons.restart_alt),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextButton.icon(
|
||||
onPressed: _completeNow,
|
||||
icon: const Icon(Icons.check),
|
||||
label: const Text('Mark session complete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SeedCard(
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.edit_note_outlined),
|
||||
title: const Text('Finish with a one-minute reflection'),
|
||||
subtitle: const Text('Capture what worked while it is fresh.'),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: widget.onReflect,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _start() {
|
||||
if (_running) {
|
||||
return;
|
||||
}
|
||||
HapticFeedback.selectionClick();
|
||||
setState(() => _running = true);
|
||||
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||||
if (_remainingSeconds <= 1) {
|
||||
_completeNow();
|
||||
return;
|
||||
}
|
||||
setState(() => _remainingSeconds -= 1);
|
||||
});
|
||||
}
|
||||
|
||||
void _pause() {
|
||||
_timer?.cancel();
|
||||
setState(() => _running = false);
|
||||
}
|
||||
|
||||
void _reset() {
|
||||
_timer?.cancel();
|
||||
setState(() {
|
||||
_running = false;
|
||||
_remainingSeconds = _selectedMinutes * 60;
|
||||
});
|
||||
}
|
||||
|
||||
void _completeNow() {
|
||||
_timer?.cancel();
|
||||
final completedMinutes = _selectedMinutes;
|
||||
setState(() {
|
||||
_running = false;
|
||||
_remainingSeconds = _selectedMinutes * 60;
|
||||
});
|
||||
HapticFeedback.mediumImpact();
|
||||
widget.onComplete(completedMinutes);
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder:
|
||||
(context) => AlertDialog(
|
||||
title: const Text('Session saved'),
|
||||
content: const Text(
|
||||
'Take one minute to write what helped you focus.',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Later'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
widget.onReflect();
|
||||
},
|
||||
child: const Text('Reflect'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||