release: seal Focus Seed 1.0.0

This commit is contained in:
2026-06-11 00:10:50 +08:00
commit 67ecb15c03
121 changed files with 5253 additions and 0 deletions
+18
View File
@@ -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
+54
View File
@@ -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
+33
View File
@@ -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'
+31
View File
@@ -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.
+46
View File
@@ -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.
+44
View File
@@ -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
+11
View File
@@ -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
+56
View File
@@ -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>
+45
View File
@@ -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>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

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>
+21
View File
@@ -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)
}
+3
View File
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
+160
View File
@@ -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 "$@"
+90
View File
@@ -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
+25
View File
@@ -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")
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+616
View File
@@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 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>
+7
View File
@@ -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>
+13
View File
@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -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>
+26
View File
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>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>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+12
View File
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+84
View File
@@ -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.
+206
View File
@@ -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.
+30
View File
@@ -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.
+109
View File
@@ -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.
+57
View File
@@ -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
+32
View File
@@ -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.
+100
View File
@@ -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>
+47
View File
@@ -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
+64
View File
@@ -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
+38
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

+47
View File
@@ -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.
+8
View File
@@ -0,0 +1,8 @@
import 'package:flutter/material.dart';
import 'src/app/focus_seed_app.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const FocusSeedApp());
}
+52
View File
@@ -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)),
),
),
);
}
}
+19
View File
@@ -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(),
);
}
}
+76
View File
@@ -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')}';
}
+9
View File
@@ -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;
}
}
+5
View File
@@ -0,0 +1,5 @@
import 'dart:convert';
String prettyJson(Map<String, dynamic> value) {
return const JsonEncoder.withIndent(' ').convert(value);
}
+33
View File
@@ -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)),
),
),
],
),
);
}
}
+147
View File
@@ -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),
),
),
],
),
);
}
}
+269
View File
@@ -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');
}
}
}
+212
View File
@@ -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'),
),
],
),
);
}
}

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