diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..8a3f1c2
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,16 @@
+version: 2
+updates:
+ - package-ecosystem: "pub"
+ directory: "/"
+ schedule:
+ interval: daily
+ time: "09:00"
+ timezone: Europe/Berlin
+ open-pull-requests-limit: 5
+ - package-ecosystem: "gradle"
+ directory: "/android"
+ schedule:
+ interval: weekly
+ time: "09:00"
+ timezone: Europe/Berlin
+ open-pull-requests-limit: 1
diff --git a/.github/workflows/deploy-mobile-merge.yml b/.github/workflows/deploy-mobile-merge.yml
new file mode 100644
index 0000000..af56c19
--- /dev/null
+++ b/.github/workflows/deploy-mobile-merge.yml
@@ -0,0 +1,50 @@
+name: Deploys mobile targets to GitHub Releases on merge
+'on':
+ push:
+ branches:
+ - main
+
+jobs:
+ build_mobile:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ - name: Decode Keystore
+ env:
+ ENCODED_STRING: ${{ secrets.SIGNING_KEY_STORE_BASE64 }}
+ SIGNING_KEY_STORE_PATH: ${{ secrets.SIGNING_KEY_STORE_PATH }}
+
+ run: |
+ echo $ENCODED_STRING > keystore-b64.txt
+ base64 -d keystore-b64.txt > $SIGNING_KEY_STORE_PATH
+
+ - name: Get dependencies
+ run: cd app && flutter pub get && dart run build_runner build --delete-conflicting-outputs --release
+
+ - name: Build Release App bundle
+ env:
+ SIGNING_KEY_STORE_PATH: ${{ secrets.SIGNING_KEY_STORE_PATH }}
+ SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
+ SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
+ SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }}
+ run: cd app && flutter build appbundle
+
+ # TODO: iOS build
+
+ - name: Upload Release Build to Artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: release-android
+ path: app/build/app/outputs/bundle/app.aab
+
+ - name: Create Github Release
+ uses: softprops/action-gh-release@v2
+ with:
+ generate_release_notes: true
+ prerelease: true
+ files: app/build/app/outputs/bundle/app.aab
diff --git a/.github/workflows/deploy-webapp-merge.yml b/.github/workflows/deploy-webapp-merge.yml
new file mode 100644
index 0000000..bace79c
--- /dev/null
+++ b/.github/workflows/deploy-webapp-merge.yml
@@ -0,0 +1,21 @@
+name: Deploy WebApp on merge
+'on':
+ push:
+ branches:
+ - main
+
+jobs:
+ build_and_deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: subosito/flutter-action@v2
+ with:
+ channel: "stable"
+
+ - name: Get dependencies
+ run: cd app && flutter pub get && dart run build_runner build --delete-conflicting-outputs --release
+
+ - name: Build Flutter web
+ run: cd app && flutter build web --web-renderer canvaskit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e1d808a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,51 @@
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
+
+# Environment variables (keep those secret!)
+.env
+.env.debug
+.env.production
+lib/env/*.g.dart
+
+# build output
+dist/
+.output/
+.json/
+
+# macOS-specific files
+.DS_Store
+
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..a731760
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,45 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+analyzer:
+ plugins:
+ - custom_lint
+ exclude:
+ - "android/**"
+ - "**/*.g.dart"
+ - "**/*.mocks.dart"
+ - "**/*.freezed.dart"
+ errors:
+ invalid_annotation_target: ignore
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ avoid_print: true
+ require_trailing_commas: true
+ eol_at_end_of_file: true
+ prefer_const_constructors: true
+ prefer_const_literals_to_create_immutables: true
+ prefer_const_declarations: true
+ unnecessary_const: true
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..6f56801
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,13 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/android/app/build.gradle b/android/app/build.gradle
new file mode 100644
index 0000000..4f0d7a2
--- /dev/null
+++ b/android/app/build.gradle
@@ -0,0 +1,79 @@
+plugins {
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+android {
+ namespace "com.shipflutter.android"
+ compileSdk flutter.compileSdkVersion
+ ndkVersion "26.3.11579264"
+
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "com.shipflutter.android"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
+ minSdkVersion 23
+ targetSdkVersion flutter.targetSdkVersion
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ multiDexEnabled true
+ }
+ signingConfigs {
+ release {
+ storeFile file("keystore.jks")
+ storePassword System.getenv("SIGNING_STORE_PASSWORD")
+ keyAlias System.getenv("SIGNING_KEY_ALIAS")
+ keyPassword System.getenv("SIGNING_KEY_PASSWORD")
+ }
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ debug {
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..00ac009
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/com/shipflutter/android/MainActivity.kt b/android/app/src/main/kotlin/com/shipflutter/android/MainActivity.kt
new file mode 100644
index 0000000..a663d8c
--- /dev/null
+++ b/android/app/src/main/kotlin/com/shipflutter/android/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.shipflutter.android
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity()
diff --git a/android/app/src/main/res/drawable-hdpi/ic_stat_ic_notification.png b/android/app/src/main/res/drawable-hdpi/ic_stat_ic_notification.png
new file mode 100644
index 0000000..e62adf0
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_stat_ic_notification.png differ
diff --git a/android/app/src/main/res/drawable-hdpi/splash.png b/android/app/src/main/res/drawable-hdpi/splash.png
new file mode 100644
index 0000000..5abe692
Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/ic_stat_ic_notification.png b/android/app/src/main/res/drawable-mdpi/ic_stat_ic_notification.png
new file mode 100644
index 0000000..dadb571
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_stat_ic_notification.png differ
diff --git a/android/app/src/main/res/drawable-mdpi/splash.png b/android/app/src/main/res/drawable-mdpi/splash.png
new file mode 100644
index 0000000..c2cb194
Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-v21/background.png b/android/app/src/main/res/drawable-v21/background.png
new file mode 100644
index 0000000..b65e1c1
Binary files /dev/null and b/android/app/src/main/res/drawable-v21/background.png differ
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..3cc4948
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,9 @@
+
+
+ -
+
+
+ -
+
+
+
diff --git a/android/app/src/main/res/drawable-xhdpi/ic_stat_ic_notification.png b/android/app/src/main/res/drawable-xhdpi/ic_stat_ic_notification.png
new file mode 100644
index 0000000..8ed8bf0
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_stat_ic_notification.png differ
diff --git a/android/app/src/main/res/drawable-xhdpi/splash.png b/android/app/src/main/res/drawable-xhdpi/splash.png
new file mode 100644
index 0000000..679439d
Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_stat_ic_notification.png b/android/app/src/main/res/drawable-xxhdpi/ic_stat_ic_notification.png
new file mode 100644
index 0000000..cc874ec
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_stat_ic_notification.png differ
diff --git a/android/app/src/main/res/drawable-xxhdpi/splash.png b/android/app/src/main/res/drawable-xxhdpi/splash.png
new file mode 100644
index 0000000..c1fa3f2
Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_stat_ic_notification.png b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_ic_notification.png
new file mode 100644
index 0000000..5bc34a4
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_stat_ic_notification.png differ
diff --git a/android/app/src/main/res/drawable-xxxhdpi/splash.png b/android/app/src/main/res/drawable-xxxhdpi/splash.png
new file mode 100644
index 0000000..69af94a
Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/splash.png differ
diff --git a/android/app/src/main/res/drawable/background.png b/android/app/src/main/res/drawable/background.png
new file mode 100644
index 0000000..b65e1c1
Binary files /dev/null and b/android/app/src/main/res/drawable/background.png differ
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..3cc4948
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,9 @@
+
+
+ -
+
+
+ -
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..6e6a473
Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..caa8e70
Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..912f6ac
Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..1a1b598
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..442c1a8
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-night-v31/styles.xml b/android/app/src/main/res/values-night-v31/styles.xml
new file mode 100644
index 0000000..1bd9976
--- /dev/null
+++ b/android/app/src/main/res/values-night-v31/styles.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..3c4a1fe
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values-v31/styles.xml b/android/app/src/main/res/values-v31/styles.xml
new file mode 100644
index 0000000..2704571
--- /dev/null
+++ b/android/app/src/main/res/values-v31/styles.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..847e1be
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..bc157bd
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,18 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+tasks.register("clean", Delete) {
+ delete rootProject.buildDir
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..598d13f
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx4G
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e1ca574
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..1d6d19b
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1,26 @@
+pluginManagement {
+ def flutterSdkPath = {
+ def properties = new Properties()
+ file("local.properties").withInputStream { properties.load(it) }
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ return flutterSdkPath
+ }
+ settings.ext.flutterSdkPath = flutterSdkPath()
+
+ includeBuild("${settings.ext.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 "7.3.0" apply false
+ id "org.jetbrains.kotlin.android" version "1.7.10" apply false
+}
+
+include ":app"
diff --git a/assets/images/flutter_logo.png b/assets/images/flutter_logo.png
new file mode 100644
index 0000000..b5c6ca7
Binary files /dev/null and b/assets/images/flutter_logo.png differ
diff --git a/assets/images/icon-512-maskable.png b/assets/images/icon-512-maskable.png
new file mode 100644
index 0000000..f86c759
Binary files /dev/null and b/assets/images/icon-512-maskable.png differ
diff --git a/assets/images/icon-512.png b/assets/images/icon-512.png
new file mode 100644
index 0000000..243bc13
Binary files /dev/null and b/assets/images/icon-512.png differ
diff --git a/assets/remote_config.json b/assets/remote_config.json
new file mode 100644
index 0000000..6ed1c37
--- /dev/null
+++ b/assets/remote_config.json
@@ -0,0 +1,4 @@
+{
+ "example_param_2": "4",
+ "example_param_1": "1"
+}
\ No newline at end of file
diff --git a/assets/rive/success.riv b/assets/rive/success.riv
new file mode 100644
index 0000000..1010504
Binary files /dev/null and b/assets/rive/success.riv differ
diff --git a/assets/svg/apple.svg b/assets/svg/apple.svg
new file mode 100644
index 0000000..5e17610
--- /dev/null
+++ b/assets/svg/apple.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/svg/facebook.svg b/assets/svg/facebook.svg
new file mode 100644
index 0000000..ea81a71
--- /dev/null
+++ b/assets/svg/facebook.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/svg/google.svg b/assets/svg/google.svg
new file mode 100644
index 0000000..c728a3d
--- /dev/null
+++ b/assets/svg/google.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/svg/logo.svg b/assets/svg/logo.svg
new file mode 100644
index 0000000..5d90b0e
--- /dev/null
+++ b/assets/svg/logo.svg
@@ -0,0 +1,57 @@
+
+
+
diff --git a/build.yaml b/build.yaml
new file mode 100644
index 0000000..703664f
--- /dev/null
+++ b/build.yaml
@@ -0,0 +1,10 @@
+targets:
+ $default:
+ builders:
+ envied_generator|envied:
+ options:
+ path: .env.debug
+ override: true
+ release_options:
+ path: .env
+ override: true
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..7e7e7f6
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1 @@
+extensions:
diff --git a/integration_test/app_test.dart b/integration_test/app_test.dart
new file mode 100644
index 0000000..2350ab0
--- /dev/null
+++ b/integration_test/app_test.dart
@@ -0,0 +1,18 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:flutter_template/app.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ group('end-to-end test', () {
+ testWidgets('do something, then check something',
+ (tester) async {
+ // Load app widget.
+ await tester.pumpWidget(const MainApp());
+
+ // Verify something
+ // expect(find.text('1'), findsOneWidget);
+ });
+ });
+}
diff --git a/ios/.gitignore b/ios/.gitignore
new file mode 100644
index 0000000..7a7f987
--- /dev/null
+++ b/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..1dc6cf7
--- /dev/null
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 13.0
+
+
diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..ec97fc6
--- /dev/null
+++ b/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..c4855bf
--- /dev/null
+++ b/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/ios/Podfile b/ios/Podfile
new file mode 100644
index 0000000..3e44f9c
--- /dev/null
+++ b/ios/Podfile
@@ -0,0 +1,44 @@
+# Uncomment this line to define a global platform for your project
+platform :ios, '13.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
new file mode 100644
index 0000000..af8508b
--- /dev/null
+++ b/ios/Podfile.lock
@@ -0,0 +1,72 @@
+PODS:
+ - Flutter (1.0.0)
+ - flutter_native_splash (0.0.1):
+ - Flutter
+ - in_app_review (0.2.0):
+ - Flutter
+ - integration_test (0.0.1):
+ - Flutter
+ - path_provider_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
+ - permission_handler_apple (9.3.0):
+ - Flutter
+ - rive_common (0.0.1):
+ - Flutter
+ - share_plus (0.0.1):
+ - Flutter
+ - shared_preferences_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
+ - url_launcher_ios (0.0.1):
+ - Flutter
+
+DEPENDENCIES:
+ - Flutter (from `Flutter`)
+ - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
+ - in_app_review (from `.symlinks/plugins/in_app_review/ios`)
+ - integration_test (from `.symlinks/plugins/integration_test/ios`)
+ - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
+ - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
+ - rive_common (from `.symlinks/plugins/rive_common/ios`)
+ - share_plus (from `.symlinks/plugins/share_plus/ios`)
+ - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
+ - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
+
+EXTERNAL SOURCES:
+ Flutter:
+ :path: Flutter
+ flutter_native_splash:
+ :path: ".symlinks/plugins/flutter_native_splash/ios"
+ in_app_review:
+ :path: ".symlinks/plugins/in_app_review/ios"
+ integration_test:
+ :path: ".symlinks/plugins/integration_test/ios"
+ path_provider_foundation:
+ :path: ".symlinks/plugins/path_provider_foundation/darwin"
+ permission_handler_apple:
+ :path: ".symlinks/plugins/permission_handler_apple/ios"
+ rive_common:
+ :path: ".symlinks/plugins/rive_common/ios"
+ share_plus:
+ :path: ".symlinks/plugins/share_plus/ios"
+ shared_preferences_foundation:
+ :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
+ url_launcher_ios:
+ :path: ".symlinks/plugins/url_launcher_ios/ios"
+
+SPEC CHECKSUMS:
+ Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+ flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
+ in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
+ integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
+ path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
+ permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
+ rive_common: cbbac3192af00d7341f19dae2f26298e9e37d99e
+ share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
+ shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
+ url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
+
+PODFILE CHECKSUM: a57f30d18f102dd3ce366b1d62a55ecbef2158e5
+
+COCOAPODS: 1.15.2
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..5cd2313
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,757 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 1FDFF1182C29D3B1003B6B0F /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1FDFF1172C29D3B1003B6B0F /* StoreKit.framework */; };
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 540C19F73193CC81711A82C1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 21E25BADDBD36E377B85B081 /* Pods_Runner.framework */; };
+ 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 */; };
+ D34ECD9CECAC56058E7149B5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9DE1BE5FCD4385559FC2CF04 /* Pods_RunnerTests.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+ remoteInfo = Runner;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 158963EB955BAC5407518C24 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
+ 158AFFE2E9703F0FE6FAF02C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 1FA7BD3D2C414D8A00E47DA1 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
+ 1FDFF1172C29D3B1003B6B0F /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
+ 21E25BADDBD36E377B85B081 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 4AD7D8E599CDB37559A57C9B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 66C9DD4B8C3F6661921A902D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 975BAA7AA44FFDAD078F9F2E /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 9DE1BE5FCD4385559FC2CF04 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ D1811CA78EECCBDC4D379F54 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 1FDFF1182C29D3B1003B6B0F /* StoreKit.framework in Frameworks */,
+ 540C19F73193CC81711A82C1 /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ CC216CA5E4AA19FD3A22B8E0 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ D34ECD9CECAC56058E7149B5 /* Pods_RunnerTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 1FDFF1162C29D3B1003B6B0F /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 1FDFF1172C29D3B1003B6B0F /* StoreKit.framework */,
+ 21E25BADDBD36E377B85B081 /* Pods_Runner.framework */,
+ 9DE1BE5FCD4385559FC2CF04 /* Pods_RunnerTests.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 331C8082294A63A400263BE5 /* RunnerTests */,
+ 1FDFF1162C29D3B1003B6B0F /* Frameworks */,
+ B7EDC93886077BF6DFB261BD /* Pods */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 1FA7BD3D2C414D8A00E47DA1 /* Runner.entitlements */,
+ 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 = "";
+ };
+ B7EDC93886077BF6DFB261BD /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 4AD7D8E599CDB37559A57C9B /* Pods-Runner.debug.xcconfig */,
+ 158AFFE2E9703F0FE6FAF02C /* Pods-Runner.release.xcconfig */,
+ D1811CA78EECCBDC4D379F54 /* Pods-Runner.profile.xcconfig */,
+ 158963EB955BAC5407518C24 /* Pods-RunnerTests.debug.xcconfig */,
+ 66C9DD4B8C3F6661921A902D /* Pods-RunnerTests.release.xcconfig */,
+ 975BAA7AA44FFDAD078F9F2E /* Pods-RunnerTests.profile.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 008AE77922DC959687D0976B /* [CP] Check Pods Manifest.lock */,
+ 331C807D294A63A400263BE5 /* Sources */,
+ 331C807F294A63A400263BE5 /* Resources */,
+ CC216CA5E4AA19FD3A22B8E0 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ C3A14BA12F3F5E09743D4DED /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ FD972C2A387338152F52FE66 /* [CP] Embed Pods Frameworks */,
+ 4B47CFD844A4AA2FFBA46FDE /* [CP] Copy Pods Resources */,
+ );
+ 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 */
+ 008AE77922DC959687D0976B /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 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";
+ };
+ 4B47CFD844A4AA2FFBA46FDE /* [CP] Copy Pods Resources */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Copy Pods Resources";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+ C3A14BA12F3F5E09743D4DED /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ FD972C2A387338152F52FE66 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C807D294A63A400263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97C146ED1CF9000F007C117D /* Runner */;
+ targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 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;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = Z983U8SV94;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = dev.shipflutter.ios;
+ 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;
+ baseConfigurationReference = 158963EB955BAC5407518C24 /* Pods-RunnerTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.shipflutter.ios.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Debug;
+ };
+ 331C8089294A63A400263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 66C9DD4B8C3F6661921A902D /* Pods-RunnerTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.shipflutter.ios.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Release;
+ };
+ 331C808A294A63A400263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 975BAA7AA44FFDAD078F9F2E /* Pods-RunnerTests.profile.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = dev.shipflutter.ios.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 = AppIcon;
+ 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 = AppIcon;
+ 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;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = Z983U8SV94;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = dev.shipflutter.ios;
+ 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;
+ CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = Z983U8SV94;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = dev.shipflutter.ios;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C8088294A63A400263BE5 /* Debug */,
+ 331C8089294A63A400263BE5 /* Release */,
+ 331C808A294A63A400263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..d5bcd68
--- /dev/null
+++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..b636303
--- /dev/null
+++ b/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@main
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..f332a06
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..ce02011
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..4c09939
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..1bf067c
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..0f51dee
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..d05ac17
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..cace010
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..4c09939
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..391d63c
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..6440f74
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
new file mode 100644
index 0000000..96ac58a
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
new file mode 100644
index 0000000..4e48298
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
new file mode 100644
index 0000000..359d92c
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
new file mode 100644
index 0000000..883a1c0
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..6440f74
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..89c470a
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
new file mode 100644
index 0000000..6e6a473
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
new file mode 100644
index 0000000..1a1b598
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..384a52f
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..936172c
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..a07b67b
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json
new file mode 100644
index 0000000..9f447e1
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "background.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png
new file mode 100644
index 0000000..b65e1c1
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchBackground.imageset/background.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..00cabce
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "LaunchImage.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "LaunchImage@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "LaunchImage@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..c2cb194
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..679439d
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..c1fa3f2
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..7aa6dfb
--- /dev/null
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
new file mode 100644
index 0000000..5b22958
--- /dev/null
+++ b/ios/Runner/Info.plist
@@ -0,0 +1,57 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Flutter template
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleLocalizations
+
+ en
+
+ CFBundleName
+ flutter_template
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UIApplicationSupportsIndirectInputEvents
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UIStatusBarHidden
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements
new file mode 100644
index 0000000..017015a
--- /dev/null
+++ b/ios/Runner/Runner.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+
+ aps-environment
+ development
+
+
+
diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 0000000..86a7c3b
--- /dev/null
+++ b/ios/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Flutter
+import UIKit
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/lib/app.dart b/lib/app.dart
new file mode 100644
index 0000000..b81c2d4
--- /dev/null
+++ b/lib/app.dart
@@ -0,0 +1,76 @@
+import 'package:context_watch_signals/context_watch_signals.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_localizations/flutter_localizations.dart';
+import 'package:go_router/go_router.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/modules/base/about/about_route.dart';
+import 'package:flutter_template/modules/base/settings/settings_route.dart';
+import 'package:flutter_template/navigation.dart';
+import 'package:flutter_template/modules/base/settings/settings_controller.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+import 'package:flutter_template/modules/base/widgets/app_error_widget.dart';
+import 'package:flutter_template/modules/base/home/home_route.dart';
+
+/// The Widget that configures your application.
+class MainApp extends StatefulWidget {
+ const MainApp({super.key});
+
+ @override
+ State createState() => _MainAppState();
+}
+
+class _MainAppState extends State {
+ late final GoRouter _config;
+
+ @override
+ void initState() {
+ _config = createRouter(
+ initialLocation: HomeRoute.path,
+ redirect: (context, state) async {
+ return null;
+ },
+ navigationRoutes: [
+ HomeRoute(),
+ AboutRoute(),
+ SettingsRoute(),
+ ],
+ otherRoutes: [
+ ],
+ );
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // Display a custom error view
+ ErrorWidget.builder = (errorDetails) {
+ return AppErrorWidget(errorDetails: errorDetails);
+ };
+
+ return MaterialApp.router(
+ // Providing a restorationScopeId allows the Navigator built by the
+ // MaterialApp to restore the navigation stack when a user leaves and
+ // returns to the app after it has been killed while running in the
+ // background.
+ restorationScopeId: 'app',
+ locale: TranslationProvider.of(context).flutterLocale, // use provider
+ supportedLocales: AppLocaleUtils.supportedLocales,
+ localizationsDelegates: GlobalMaterialLocalizations.delegates,
+ // Use AppLocalizations to configure the correct application title
+ // depending on the user's locale.
+ //
+ // The appTitle is defined in .arb files found in the localization
+ // directory.
+ onGenerateTitle: (BuildContext context) => context.t.app_name,
+
+ // Define a light and dark color theme. Then, read the user's
+ // preferred ThemeMode (light, dark, or system default) from the
+ // SettingsController to display the correct theme.
+ theme: lightTheme,
+ darkTheme: darkTheme,
+ debugShowCheckedModeBanner: false,
+ themeMode: settingsController.instance.themeMode.watch(context),
+ routerConfig: _config,
+ );
+ }
+}
diff --git a/lib/env/app_env.dart b/lib/env/app_env.dart
new file mode 100644
index 0000000..7a15bdf
--- /dev/null
+++ b/lib/env/app_env.dart
@@ -0,0 +1,8 @@
+// lib/env/env.dart
+import 'package:envied/envied.dart';
+
+part 'app_env.g.dart';
+
+@Envied(name: 'Env', path: '.env')
+abstract class Env {
+}
diff --git a/lib/i18n/strings.i18n.json b/lib/i18n/strings.i18n.json
new file mode 100644
index 0000000..a775d94
--- /dev/null
+++ b/lib/i18n/strings.i18n.json
@@ -0,0 +1,68 @@
+{
+ "app_name": "ShipFlutter template",
+ "privacy_url": "https://shipflutter.com/privacy",
+ "terms_url": "https://shipflutter.com/terms",
+ "support_url": "https://shipflutter.com/#contact",
+ "rate_url": "https://shipflutter.com/#feedback",
+ "cancel": "Cancel",
+ "retry": "Retry",
+ "errors": {
+ "generic_title": "Oops...",
+ "generic_text": "Something went wrong",
+ "network_title": "Hmmm...",
+ "network_text": "The operation failed. Do you have connection?",
+ "error_view": {
+ "title": "Argh... :(",
+ "content": "We are really sorry, but something went wrong and we could not display the view. You can restart the app or try to navigate back home",
+ "back": "Go Home",
+ "exit": "Exit"
+ }
+ },
+ "navigation": {
+ "home": "Home",
+ "about": "About",
+ "settings": "Settings"
+ },
+ "home": {
+ "title": "ShipFlutter template",
+ "subtitle": "Ship everywhere with Flutter. Fast!",
+ "details_title": "Details",
+ "cta": {
+ "title": "Bootstrap your app",
+ "button": "Try now",
+ "content": "Use our AI-powered builder to generate and customize your project.",
+ "link": "https://builder.shipflutter.com"
+ }
+ },
+ "about": {
+ "title": "👋 Hi there!",
+ "learn_more": "Learn more",
+ "learn_more_url(ignore)": "https://pibi.studio",
+ "intro_title": "About us",
+ "intro_content": "We are ex-Googlers, GDEs and experts from top companies helping Founders and Makers build high-quality consumer apps.",
+ "purpose_title": "Our purpose",
+ "purpose_content": "We build our own consumer apps with our tools and expertise. The same quality is offered to our clients. We built this template to show Flutter base template of ShipFlutter project. It shows best practices and base structure",
+ "hint_title": "Did you know...",
+ "hint_content": "ShipFlutter is more than a boilerplate. It's a fully customizable starter kit to seamlessly launch responsive Android, iOS, and Web apps with Flutter powered by Firebase and Vertex AI."
+ },
+ "settings": {
+ "title": "Settings",
+ "theme_mode": "Light/Dark mode",
+ "privacy": "Privacy Policy",
+ "terms": "Terms of service",
+ "support": "Support",
+ "rate": "Rate us",
+ "feedback": "Share feedback",
+ "disconnect": "Disconnect",
+ "delete": "Delete account"
+ },
+ "onboarding": {
+ "hero_title_start": "Welcome to ShipFlutter template!",
+ "hero_text_start": "A base template to show Flutter power",
+ "hero_title_end": "Flutter template Tutorial end title",
+ "hero_text_end": "Create an account now to enjoy all the features",
+ "skip": "Skip",
+ "login": "Register now",
+ "not_now": "Not now"
+ }
+}
\ No newline at end of file
diff --git a/lib/i18n/translations.g.dart b/lib/i18n/translations.g.dart
new file mode 100644
index 0000000..e50c678
--- /dev/null
+++ b/lib/i18n/translations.g.dart
@@ -0,0 +1,366 @@
+/// Generated file. Do not edit.
+///
+/// Original: lib/i18n
+/// To regenerate, run: `dart run slang`
+///
+/// Locales: 1
+/// Strings: 50
+///
+/// Built on 2024-08-30 at 14:41 UTC
+
+// coverage:ignore-file
+// ignore_for_file: type=lint
+
+import 'package:flutter/widgets.dart';
+import 'package:slang/api/translation_overrides.dart';
+import 'package:slang/builder/model/build_model_config.dart';
+import 'package:slang/builder/model/enums.dart';
+import 'package:slang/builder/model/node.dart';
+import 'package:slang_flutter/slang_flutter.dart';
+export 'package:slang_flutter/slang_flutter.dart';
+
+/// Generated by the "Translation Overrides" feature.
+/// This config is needed to recreate the translation model exactly
+/// the same way as this file was created.
+final _buildConfig = BuildModelConfig(
+ fallbackStrategy: FallbackStrategy.baseLocale,
+ keyCase: null,
+ keyMapCase: null,
+ paramCase: null,
+ stringInterpolation: StringInterpolation.dart,
+ maps: [],
+ pluralAuto: PluralAuto.cardinal,
+ pluralParameter: 'n',
+ pluralCardinal: [],
+ pluralOrdinal: [],
+ contexts: [],
+ interfaces: [], // currently not supported
+);
+
+const AppLocale _baseLocale = AppLocale.en;
+
+/// Supported locales, see extension methods below.
+///
+/// Usage:
+/// - LocaleSettings.setLocale(AppLocale.en) // set locale
+/// - Locale locale = AppLocale.en.flutterLocale // get flutter locale from enum
+/// - if (LocaleSettings.currentLocale == AppLocale.en) // locale check
+enum AppLocale with BaseAppLocale {
+ en(languageCode: 'en', build: Translations.build);
+
+ const AppLocale({required this.languageCode, this.scriptCode, this.countryCode, required this.build}); // ignore: unused_element
+
+ @override final String languageCode;
+ @override final String? scriptCode;
+ @override final String? countryCode;
+ @override final TranslationBuilder build;
+
+ /// Gets current instance managed by [LocaleSettings].
+ Translations get translations => LocaleSettings.instance.translationMap[this]!;
+}
+
+/// Method A: Simple
+///
+/// No rebuild after locale change.
+/// Translation happens during initialization of the widget (call of t).
+/// Configurable via 'translate_var'.
+///
+/// Usage:
+/// String a = t.someKey.anotherKey;
+/// String b = t['someKey.anotherKey']; // Only for edge cases!
+Translations get t => LocaleSettings.instance.currentTranslations;
+
+/// Method B: Advanced
+///
+/// All widgets using this method will trigger a rebuild when locale changes.
+/// Use this if you have e.g. a settings page where the user can select the locale during runtime.
+///
+/// Step 1:
+/// wrap your App with
+/// TranslationProvider(
+/// child: MyApp()
+/// );
+///
+/// Step 2:
+/// final t = Translations.of(context); // Get t variable.
+/// String a = t.someKey.anotherKey; // Use t variable.
+/// String b = t['someKey.anotherKey']; // Only for edge cases!
+class TranslationProvider extends BaseTranslationProvider {
+ TranslationProvider({required super.child}) : super(settings: LocaleSettings.instance);
+
+ static InheritedLocaleData of(BuildContext context) => InheritedLocaleData.of(context);
+}
+
+/// Method B shorthand via [BuildContext] extension method.
+/// Configurable via 'translate_var'.
+///
+/// Usage (e.g. in a widget's build method):
+/// context.t.someKey.anotherKey
+extension BuildContextTranslationsExtension on BuildContext {
+ Translations get t => TranslationProvider.of(this).translations;
+}
+
+/// Manages all translation instances and the current locale
+class LocaleSettings extends BaseFlutterLocaleSettings {
+ LocaleSettings._() : super(utils: AppLocaleUtils.instance);
+
+ static final instance = LocaleSettings._();
+
+ // static aliases (checkout base methods for documentation)
+ static AppLocale get currentLocale => instance.currentLocale;
+ static Stream getLocaleStream() => instance.getLocaleStream();
+ static AppLocale setLocale(AppLocale locale, {bool? listenToDeviceLocale = false}) => instance.setLocale(locale, listenToDeviceLocale: listenToDeviceLocale);
+ static AppLocale setLocaleRaw(String rawLocale, {bool? listenToDeviceLocale = false}) => instance.setLocaleRaw(rawLocale, listenToDeviceLocale: listenToDeviceLocale);
+ static AppLocale useDeviceLocale() => instance.useDeviceLocale();
+ @Deprecated('Use [AppLocaleUtils.supportedLocales]') static List get supportedLocales => instance.supportedLocales;
+ @Deprecated('Use [AppLocaleUtils.supportedLocalesRaw]') static List get supportedLocalesRaw => instance.supportedLocalesRaw;
+ static void setPluralResolver({String? language, AppLocale? locale, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.setPluralResolver(
+ language: language,
+ locale: locale,
+ cardinalResolver: cardinalResolver,
+ ordinalResolver: ordinalResolver,
+ );
+ static void overrideTranslations({required AppLocale locale, required FileType fileType, required String content}) => instance.overrideTranslations(locale: locale, fileType: fileType, content: content);
+ static void overrideTranslationsFromMap({required AppLocale locale, required bool isFlatMap, required Map map}) => instance.overrideTranslationsFromMap(locale: locale, isFlatMap: isFlatMap, map: map);
+}
+
+/// Provides utility functions without any side effects.
+class AppLocaleUtils extends BaseAppLocaleUtils {
+ AppLocaleUtils._() : super(baseLocale: _baseLocale, locales: AppLocale.values, buildConfig: _buildConfig);
+
+ static final instance = AppLocaleUtils._();
+
+ // static aliases (checkout base methods for documentation)
+ static AppLocale parse(String rawLocale) => instance.parse(rawLocale);
+ static AppLocale parseLocaleParts({required String languageCode, String? scriptCode, String? countryCode}) => instance.parseLocaleParts(languageCode: languageCode, scriptCode: scriptCode, countryCode: countryCode);
+ static AppLocale findDeviceLocale() => instance.findDeviceLocale();
+ static List get supportedLocales => instance.supportedLocales;
+ static List get supportedLocalesRaw => instance.supportedLocalesRaw;
+ static Translations buildWithOverrides({required AppLocale locale, required FileType fileType, required String content, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverrides(locale: locale, fileType: fileType, content: content, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);
+ static Translations buildWithOverridesFromMap({required AppLocale locale, required bool isFlatMap, required Map map, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver}) => instance.buildWithOverridesFromMap(locale: locale, isFlatMap: isFlatMap, map: map, cardinalResolver: cardinalResolver, ordinalResolver: ordinalResolver);
+}
+
+// translations
+
+// Path:
+class Translations implements BaseTranslations {
+ /// Returns the current translations of the given [context].
+ ///
+ /// Usage:
+ /// final t = Translations.of(context);
+ static Translations of(BuildContext context) => InheritedLocaleData.of(context).translations;
+
+ /// You can call this constructor and build your own translation instance of this locale.
+ /// Constructing via the enum [AppLocale.build] is preferred.
+ /// [AppLocaleUtils.buildWithOverrides] is recommended for overriding.
+ Translations.build({Map? overrides, PluralResolver? cardinalResolver, PluralResolver? ordinalResolver})
+ : $meta = TranslationMetadata(
+ locale: AppLocale.en,
+ overrides: overrides ?? {},
+ cardinalResolver: cardinalResolver,
+ ordinalResolver: ordinalResolver,
+ ) {
+ $meta.setFlatMapFunction(_flatMapFunction);
+ }
+
+ /// Metadata for the translations of .
+ @override final TranslationMetadata $meta;
+
+ /// Access flat map
+ dynamic operator[](String key) => $meta.getTranslation(key);
+
+ late final Translations _root = this; // ignore: unused_field
+
+ // Translations
+ String get app_name => TranslationOverrides.string(_root.$meta, 'app_name', {}) ?? 'ShipFlutter template';
+ String get privacy_url => TranslationOverrides.string(_root.$meta, 'privacy_url', {}) ?? 'https://shipflutter.com/privacy';
+ String get terms_url => TranslationOverrides.string(_root.$meta, 'terms_url', {}) ?? 'https://shipflutter.com/terms';
+ String get support_url => TranslationOverrides.string(_root.$meta, 'support_url', {}) ?? 'https://shipflutter.com/#contact';
+ String get rate_url => TranslationOverrides.string(_root.$meta, 'rate_url', {}) ?? 'https://shipflutter.com/#feedback';
+ String get cancel => TranslationOverrides.string(_root.$meta, 'cancel', {}) ?? 'Cancel';
+ String get retry => TranslationOverrides.string(_root.$meta, 'retry', {}) ?? 'Retry';
+ late final _TranslationsErrorsEn errors = _TranslationsErrorsEn._(_root);
+ late final _TranslationsNavigationEn navigation = _TranslationsNavigationEn._(_root);
+ late final _TranslationsHomeEn home = _TranslationsHomeEn._(_root);
+ late final _TranslationsAboutEn about = _TranslationsAboutEn._(_root);
+ late final _TranslationsSettingsEn settings = _TranslationsSettingsEn._(_root);
+ late final _TranslationsOnboardingEn onboarding = _TranslationsOnboardingEn._(_root);
+}
+
+// Path: errors
+class _TranslationsErrorsEn {
+ _TranslationsErrorsEn._(this._root);
+
+ final Translations _root; // ignore: unused_field
+
+ // Translations
+ String get generic_title => TranslationOverrides.string(_root.$meta, 'errors.generic_title', {}) ?? 'Oops...';
+ String get generic_text => TranslationOverrides.string(_root.$meta, 'errors.generic_text', {}) ?? 'Something went wrong';
+ String get network_title => TranslationOverrides.string(_root.$meta, 'errors.network_title', {}) ?? 'Hmmm...';
+ String get network_text => TranslationOverrides.string(_root.$meta, 'errors.network_text', {}) ?? 'The operation failed. Do you have connection?';
+ late final _TranslationsErrorsErrorViewEn error_view = _TranslationsErrorsErrorViewEn._(_root);
+}
+
+// Path: navigation
+class _TranslationsNavigationEn {
+ _TranslationsNavigationEn._(this._root);
+
+ final Translations _root; // ignore: unused_field
+
+ // Translations
+ String get home => TranslationOverrides.string(_root.$meta, 'navigation.home', {}) ?? 'Home';
+ String get about => TranslationOverrides.string(_root.$meta, 'navigation.about', {}) ?? 'About';
+ String get settings => TranslationOverrides.string(_root.$meta, 'navigation.settings', {}) ?? 'Settings';
+}
+
+// Path: home
+class _TranslationsHomeEn {
+ _TranslationsHomeEn._(this._root);
+
+ final Translations _root; // ignore: unused_field
+
+ // Translations
+ String get title => TranslationOverrides.string(_root.$meta, 'home.title', {}) ?? 'ShipFlutter template';
+ String get subtitle => TranslationOverrides.string(_root.$meta, 'home.subtitle', {}) ?? 'Ship everywhere with Flutter. Fast!';
+ String get details_title => TranslationOverrides.string(_root.$meta, 'home.details_title', {}) ?? 'Details';
+ late final _TranslationsHomeCtaEn cta = _TranslationsHomeCtaEn._(_root);
+}
+
+// Path: about
+class _TranslationsAboutEn {
+ _TranslationsAboutEn._(this._root);
+
+ final Translations _root; // ignore: unused_field
+
+ // Translations
+ String get title => TranslationOverrides.string(_root.$meta, 'about.title', {}) ?? '👋 Hi there!';
+ String get learn_more => TranslationOverrides.string(_root.$meta, 'about.learn_more', {}) ?? 'Learn more';
+ String get learn_more_url => TranslationOverrides.string(_root.$meta, 'about.learn_more_url', {}) ?? 'https://pibi.studio';
+ String get intro_title => TranslationOverrides.string(_root.$meta, 'about.intro_title', {}) ?? 'About us';
+ String get intro_content => TranslationOverrides.string(_root.$meta, 'about.intro_content', {}) ?? 'We are ex-Googlers, GDEs and experts from top companies helping Founders and Makers build high-quality consumer apps.';
+ String get purpose_title => TranslationOverrides.string(_root.$meta, 'about.purpose_title', {}) ?? 'Our purpose';
+ String get purpose_content => TranslationOverrides.string(_root.$meta, 'about.purpose_content', {}) ?? 'We build our own consumer apps with our tools and expertise. The same quality is offered to our clients. We built this template to show Flutter base template of ShipFlutter project. It shows best practices and base structure';
+ String get hint_title => TranslationOverrides.string(_root.$meta, 'about.hint_title', {}) ?? 'Did you know...';
+ String get hint_content => TranslationOverrides.string(_root.$meta, 'about.hint_content', {}) ?? 'ShipFlutter is more than a boilerplate. It\'s a fully customizable starter kit to seamlessly launch responsive Android, iOS, and Web apps with Flutter powered by Firebase and Vertex AI.';
+}
+
+// Path: settings
+class _TranslationsSettingsEn {
+ _TranslationsSettingsEn._(this._root);
+
+ final Translations _root; // ignore: unused_field
+
+ // Translations
+ String get title => TranslationOverrides.string(_root.$meta, 'settings.title', {}) ?? 'Settings';
+ String get theme_mode => TranslationOverrides.string(_root.$meta, 'settings.theme_mode', {}) ?? 'Light/Dark mode';
+ String get privacy => TranslationOverrides.string(_root.$meta, 'settings.privacy', {}) ?? 'Privacy Policy';
+ String get terms => TranslationOverrides.string(_root.$meta, 'settings.terms', {}) ?? 'Terms of service';
+ String get support => TranslationOverrides.string(_root.$meta, 'settings.support', {}) ?? 'Support';
+ String get rate => TranslationOverrides.string(_root.$meta, 'settings.rate', {}) ?? 'Rate us';
+ String get feedback => TranslationOverrides.string(_root.$meta, 'settings.feedback', {}) ?? 'Share feedback';
+ String get disconnect => TranslationOverrides.string(_root.$meta, 'settings.disconnect', {}) ?? 'Disconnect';
+ String get delete => TranslationOverrides.string(_root.$meta, 'settings.delete', {}) ?? 'Delete account';
+}
+
+// Path: onboarding
+class _TranslationsOnboardingEn {
+ _TranslationsOnboardingEn._(this._root);
+
+ final Translations _root; // ignore: unused_field
+
+ // Translations
+ String get hero_title_start => TranslationOverrides.string(_root.$meta, 'onboarding.hero_title_start', {}) ?? 'Welcome to ShipFlutter template!';
+ String get hero_text_start => TranslationOverrides.string(_root.$meta, 'onboarding.hero_text_start', {}) ?? 'A base template to show Flutter power';
+ String get hero_title_end => TranslationOverrides.string(_root.$meta, 'onboarding.hero_title_end', {}) ?? 'Flutter template Tutorial end title';
+ String get hero_text_end => TranslationOverrides.string(_root.$meta, 'onboarding.hero_text_end', {}) ?? 'Create an account now to enjoy all the features';
+ String get skip => TranslationOverrides.string(_root.$meta, 'onboarding.skip', {}) ?? 'Skip';
+ String get login => TranslationOverrides.string(_root.$meta, 'onboarding.login', {}) ?? 'Register now';
+ String get not_now => TranslationOverrides.string(_root.$meta, 'onboarding.not_now', {}) ?? 'Not now';
+}
+
+// Path: errors.error_view
+class _TranslationsErrorsErrorViewEn {
+ _TranslationsErrorsErrorViewEn._(this._root);
+
+ final Translations _root; // ignore: unused_field
+
+ // Translations
+ String get title => TranslationOverrides.string(_root.$meta, 'errors.error_view.title', {}) ?? 'Argh... :(';
+ String get content => TranslationOverrides.string(_root.$meta, 'errors.error_view.content', {}) ?? 'We are really sorry, but something went wrong and we could not display the view. You can restart the app or try to navigate back home';
+ String get back => TranslationOverrides.string(_root.$meta, 'errors.error_view.back', {}) ?? 'Go Home';
+ String get exit => TranslationOverrides.string(_root.$meta, 'errors.error_view.exit', {}) ?? 'Exit';
+}
+
+// Path: home.cta
+class _TranslationsHomeCtaEn {
+ _TranslationsHomeCtaEn._(this._root);
+
+ final Translations _root; // ignore: unused_field
+
+ // Translations
+ String get title => TranslationOverrides.string(_root.$meta, 'home.cta.title', {}) ?? 'Bootstrap your app';
+ String get button => TranslationOverrides.string(_root.$meta, 'home.cta.button', {}) ?? 'Try now';
+ String get content => TranslationOverrides.string(_root.$meta, 'home.cta.content', {}) ?? 'Use our AI-powered builder to generate and customize your project.';
+ String get link => TranslationOverrides.string(_root.$meta, 'home.cta.link', {}) ?? 'https://builder.shipflutter.com';
+}
+
+/// Flat map(s) containing all translations.
+/// Only for edge cases! For simple maps, use the map function of this library.
+
+extension on Translations {
+ dynamic _flatMapFunction(String path) {
+ switch (path) {
+ case 'app_name': return TranslationOverrides.string(_root.$meta, 'app_name', {}) ?? 'ShipFlutter template';
+ case 'privacy_url': return TranslationOverrides.string(_root.$meta, 'privacy_url', {}) ?? 'https://shipflutter.com/privacy';
+ case 'terms_url': return TranslationOverrides.string(_root.$meta, 'terms_url', {}) ?? 'https://shipflutter.com/terms';
+ case 'support_url': return TranslationOverrides.string(_root.$meta, 'support_url', {}) ?? 'https://shipflutter.com/#contact';
+ case 'rate_url': return TranslationOverrides.string(_root.$meta, 'rate_url', {}) ?? 'https://shipflutter.com/#feedback';
+ case 'cancel': return TranslationOverrides.string(_root.$meta, 'cancel', {}) ?? 'Cancel';
+ case 'retry': return TranslationOverrides.string(_root.$meta, 'retry', {}) ?? 'Retry';
+ case 'errors.generic_title': return TranslationOverrides.string(_root.$meta, 'errors.generic_title', {}) ?? 'Oops...';
+ case 'errors.generic_text': return TranslationOverrides.string(_root.$meta, 'errors.generic_text', {}) ?? 'Something went wrong';
+ case 'errors.network_title': return TranslationOverrides.string(_root.$meta, 'errors.network_title', {}) ?? 'Hmmm...';
+ case 'errors.network_text': return TranslationOverrides.string(_root.$meta, 'errors.network_text', {}) ?? 'The operation failed. Do you have connection?';
+ case 'errors.error_view.title': return TranslationOverrides.string(_root.$meta, 'errors.error_view.title', {}) ?? 'Argh... :(';
+ case 'errors.error_view.content': return TranslationOverrides.string(_root.$meta, 'errors.error_view.content', {}) ?? 'We are really sorry, but something went wrong and we could not display the view. You can restart the app or try to navigate back home';
+ case 'errors.error_view.back': return TranslationOverrides.string(_root.$meta, 'errors.error_view.back', {}) ?? 'Go Home';
+ case 'errors.error_view.exit': return TranslationOverrides.string(_root.$meta, 'errors.error_view.exit', {}) ?? 'Exit';
+ case 'navigation.home': return TranslationOverrides.string(_root.$meta, 'navigation.home', {}) ?? 'Home';
+ case 'navigation.about': return TranslationOverrides.string(_root.$meta, 'navigation.about', {}) ?? 'About';
+ case 'navigation.settings': return TranslationOverrides.string(_root.$meta, 'navigation.settings', {}) ?? 'Settings';
+ case 'home.title': return TranslationOverrides.string(_root.$meta, 'home.title', {}) ?? 'ShipFlutter template';
+ case 'home.subtitle': return TranslationOverrides.string(_root.$meta, 'home.subtitle', {}) ?? 'Ship everywhere with Flutter. Fast!';
+ case 'home.details_title': return TranslationOverrides.string(_root.$meta, 'home.details_title', {}) ?? 'Details';
+ case 'home.cta.title': return TranslationOverrides.string(_root.$meta, 'home.cta.title', {}) ?? 'Bootstrap your app';
+ case 'home.cta.button': return TranslationOverrides.string(_root.$meta, 'home.cta.button', {}) ?? 'Try now';
+ case 'home.cta.content': return TranslationOverrides.string(_root.$meta, 'home.cta.content', {}) ?? 'Use our AI-powered builder to generate and customize your project.';
+ case 'home.cta.link': return TranslationOverrides.string(_root.$meta, 'home.cta.link', {}) ?? 'https://builder.shipflutter.com';
+ case 'about.title': return TranslationOverrides.string(_root.$meta, 'about.title', {}) ?? '👋 Hi there!';
+ case 'about.learn_more': return TranslationOverrides.string(_root.$meta, 'about.learn_more', {}) ?? 'Learn more';
+ case 'about.learn_more_url': return TranslationOverrides.string(_root.$meta, 'about.learn_more_url', {}) ?? 'https://pibi.studio';
+ case 'about.intro_title': return TranslationOverrides.string(_root.$meta, 'about.intro_title', {}) ?? 'About us';
+ case 'about.intro_content': return TranslationOverrides.string(_root.$meta, 'about.intro_content', {}) ?? 'We are ex-Googlers, GDEs and experts from top companies helping Founders and Makers build high-quality consumer apps.';
+ case 'about.purpose_title': return TranslationOverrides.string(_root.$meta, 'about.purpose_title', {}) ?? 'Our purpose';
+ case 'about.purpose_content': return TranslationOverrides.string(_root.$meta, 'about.purpose_content', {}) ?? 'We build our own consumer apps with our tools and expertise. The same quality is offered to our clients. We built this template to show Flutter base template of ShipFlutter project. It shows best practices and base structure';
+ case 'about.hint_title': return TranslationOverrides.string(_root.$meta, 'about.hint_title', {}) ?? 'Did you know...';
+ case 'about.hint_content': return TranslationOverrides.string(_root.$meta, 'about.hint_content', {}) ?? 'ShipFlutter is more than a boilerplate. It\'s a fully customizable starter kit to seamlessly launch responsive Android, iOS, and Web apps with Flutter powered by Firebase and Vertex AI.';
+ case 'settings.title': return TranslationOverrides.string(_root.$meta, 'settings.title', {}) ?? 'Settings';
+ case 'settings.theme_mode': return TranslationOverrides.string(_root.$meta, 'settings.theme_mode', {}) ?? 'Light/Dark mode';
+ case 'settings.privacy': return TranslationOverrides.string(_root.$meta, 'settings.privacy', {}) ?? 'Privacy Policy';
+ case 'settings.terms': return TranslationOverrides.string(_root.$meta, 'settings.terms', {}) ?? 'Terms of service';
+ case 'settings.support': return TranslationOverrides.string(_root.$meta, 'settings.support', {}) ?? 'Support';
+ case 'settings.rate': return TranslationOverrides.string(_root.$meta, 'settings.rate', {}) ?? 'Rate us';
+ case 'settings.feedback': return TranslationOverrides.string(_root.$meta, 'settings.feedback', {}) ?? 'Share feedback';
+ case 'settings.disconnect': return TranslationOverrides.string(_root.$meta, 'settings.disconnect', {}) ?? 'Disconnect';
+ case 'settings.delete': return TranslationOverrides.string(_root.$meta, 'settings.delete', {}) ?? 'Delete account';
+ case 'onboarding.hero_title_start': return TranslationOverrides.string(_root.$meta, 'onboarding.hero_title_start', {}) ?? 'Welcome to ShipFlutter template!';
+ case 'onboarding.hero_text_start': return TranslationOverrides.string(_root.$meta, 'onboarding.hero_text_start', {}) ?? 'A base template to show Flutter power';
+ case 'onboarding.hero_title_end': return TranslationOverrides.string(_root.$meta, 'onboarding.hero_title_end', {}) ?? 'Flutter template Tutorial end title';
+ case 'onboarding.hero_text_end': return TranslationOverrides.string(_root.$meta, 'onboarding.hero_text_end', {}) ?? 'Create an account now to enjoy all the features';
+ case 'onboarding.skip': return TranslationOverrides.string(_root.$meta, 'onboarding.skip', {}) ?? 'Skip';
+ case 'onboarding.login': return TranslationOverrides.string(_root.$meta, 'onboarding.login', {}) ?? 'Register now';
+ case 'onboarding.not_now': return TranslationOverrides.string(_root.$meta, 'onboarding.not_now', {}) ?? 'Not now';
+ default: return null;
+ }
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 0000000..4dd88b2
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,38 @@
+import 'dart:async';
+
+import 'package:context_watch/context_watch.dart';
+import 'package:context_watch_signals/context_watch_signals.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_web_plugins/url_strategy.dart';
+import 'package:go_router/go_router.dart';
+import 'package:lite_ref/lite_ref.dart';
+import 'package:flutter_template/env/app_env.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/app.dart';
+import 'package:flutter_template/modules/base/logger.dart';
+import 'package:flutter_template/modules/base/preferences.dart';
+
+void main() async {
+ WidgetsFlutterBinding.ensureInitialized();
+ LocaleSettings.useDeviceLocale();
+
+ // Force load the preferences instance to ensure are reading before starting
+ // the app. You can remove this if needed but then you'll need to replace
+ // all instances of `preferences.assertInstance` and wait for the future
+ await preferences.instance;
+ // Set up GoRouter for web
+ GoRouter.optionURLReflectsImperativeAPIs = true;
+ usePathUrlStrategy();
+ runApp(
+ LiteRefScope(
+ child: TranslationProvider(
+ child: ContextWatch.root(
+ additionalWatchers: [
+ SignalContextWatcher.instance,
+ ],
+ child: const MainApp(),
+ ),
+ ),
+ ),
+ );
+}
diff --git a/lib/modules/base/about/about_route.dart b/lib/modules/base/about/about_route.dart
new file mode 100644
index 0000000..73b11a8
--- /dev/null
+++ b/lib/modules/base/about/about_route.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/modules/base/about/about_view.dart';
+import 'package:flutter_template/navigation.dart';
+
+class AboutRoute implements NavigationRoute {
+ static const String path = "/about";
+
+ @override
+ GoRoute createRoute() {
+ return GoRoute(
+ path: path,
+ builder: (context, state) => const AboutView(),
+ );
+ }
+
+ @override
+ NavigationDestination createDestination(BuildContext context) {
+ return NavigationDestination(
+ icon: const Icon(Icons.info_rounded),
+ label: context.t.navigation.about,
+ );
+ }
+}
diff --git a/lib/modules/base/about/about_view.dart b/lib/modules/base/about/about_view.dart
new file mode 100644
index 0000000..1fe91e9
--- /dev/null
+++ b/lib/modules/base/about/about_view.dart
@@ -0,0 +1,104 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+class AboutView extends StatelessWidget {
+ static const routeName = "/about";
+
+ const AboutView({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return SingleChildScrollView(
+ clipBehavior: Clip.none,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Container(
+ width: double.infinity,
+ height: 200,
+ alignment: Alignment.center,
+ color: context.theme.colorScheme.secondaryContainer,
+ child: Padding(
+ padding: const EdgeInsets.only(
+ top: 48.0,
+ left: 16,
+ right: 16,
+ bottom: 16,
+ ),
+ child: Text(
+ context.t.about.title,
+ style: context.textTheme.displayMedium,
+ softWrap: true,
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ context.t.about.intro_title,
+ style: context.textTheme.displaySmall,
+ ),
+ Container(
+ width: 30,
+ height: 3,
+ color: context.theme.colorScheme.primary,
+ ),
+ const SizedBox(height: 20),
+ Text(context.t.about.intro_content),
+ const SizedBox(height: 20),
+ FilledButton.icon(
+ onPressed: () => launchUrl(
+ Uri.parse(context.t.about.learn_more_url),
+ ),
+ icon: const Icon(Icons.open_in_new_rounded),
+ iconAlignment: IconAlignment.end,
+ label: Text(context.t.about.learn_more),
+ ),
+ const SizedBox(height: 20),
+ Text(
+ context.t.about.purpose_title,
+ style: context.textTheme.displaySmall,
+ ),
+ Container(
+ width: 30,
+ height: 3,
+ color: context.theme.colorScheme.primary,
+ ),
+ const SizedBox(height: 20),
+ Text(context.t.about.purpose_content),
+ const SizedBox(height: 20),
+ Card.filled(
+ margin: const EdgeInsets.all(0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ListTile(
+ contentPadding:
+ const EdgeInsets.only(top: 16, left: 16, right: 16),
+ leading: const Icon(Icons.tips_and_updates_rounded),
+ title: Text(
+ context.t.about.hint_title,
+ style: context.textTheme.titleMedium,
+ ),
+ dense: true,
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Text(context.t.about.hint_content),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/modules/base/home/details/details_controller.dart b/lib/modules/base/home/details/details_controller.dart
new file mode 100644
index 0000000..05b752c
--- /dev/null
+++ b/lib/modules/base/home/details/details_controller.dart
@@ -0,0 +1,33 @@
+import 'package:lite_ref/lite_ref.dart';
+import 'package:flutter_template/modules/base/home/home_service.dart';
+import 'package:flutter_template/modules/base/home/model/home_item.dart';
+import 'package:context_watch_signals/context_watch_signals.dart';
+
+final detailsController = Ref.scopedFamily(
+ (ctx, String key) => DetailsController(
+ homeService.instance,
+ )..dispatch(key),
+);
+
+class DetailsController extends Disposable {
+ DetailsController(this._service);
+
+ final HomeService _service;
+
+ final _state = asyncSignal(AsyncState.loading());
+ ReadonlySignal> get state => _state;
+
+ Future dispatch(String id) {
+ _state.value = const AsyncLoading();
+ return _service.loadSingle(id).then((item) {
+ _state.value = AsyncData(item);
+ }).catchError((e, s) {
+ _state.value = AsyncError(e, s);
+ });
+ }
+
+ @override
+ void dispose() {
+ _state.dispose();
+ }
+}
diff --git a/lib/modules/base/home/details/details_route.dart b/lib/modules/base/home/details/details_route.dart
new file mode 100644
index 0000000..7556396
--- /dev/null
+++ b/lib/modules/base/home/details/details_route.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
+import 'package:go_router/go_router.dart';
+import 'package:flutter_template/modules/base/widgets/app_scaffold.dart';
+import 'package:flutter_template/modules/base/home/details/details_view.dart';
+import 'package:flutter_template/modules/base/home/home_view.dart';
+import 'package:flutter_template/navigation.dart';
+
+class DetailsRoute implements AppRoute {
+ static const String path = ":id";
+
+ @override
+ GoRoute createRoute() {
+ return GoRoute(
+ path: path,
+ pageBuilder: (context, state) {
+ final id = state.pathParameters["id"]!;
+ if (Breakpoints.largeAndUp.isActive(context)) {
+ return NoTransitionPage(
+ child: AppScaffoldContent(
+ body: HomeView(selectedId: id),
+ secondaryBody: DetailsView(id: id),
+ ),
+ );
+ }
+ return MaterialPage(
+ key: state.pageKey,
+ child: DetailsView(id: id),
+ );
+ },
+ );
+ }
+}
diff --git a/lib/modules/base/home/details/details_view.dart b/lib/modules/base/home/details/details_view.dart
new file mode 100644
index 0000000..c950a12
--- /dev/null
+++ b/lib/modules/base/home/details/details_view.dart
@@ -0,0 +1,121 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:share_plus/share_plus.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/modules/base/home/details/details_controller.dart';
+import 'package:flutter_template/modules/base/home/home_route.dart';
+import 'package:flutter_template/modules/base/home/model/home_item.dart';
+import 'package:flutter_template/modules/base/widgets/async_image.dart';
+import 'package:context_watch_signals/context_watch_signals.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+import 'package:skeletonizer/skeletonizer.dart';
+
+class DetailsView extends StatelessWidget {
+ final String id;
+
+ const DetailsView({super.key, required this.id});
+
+ @override
+ Widget build(BuildContext context) {
+ final controller = detailsController.of(context, id);
+ final state = controller.state.watch(context);
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(context.t.home.details_title),
+ leading: IconButton(
+ icon: const Icon(Icons.close_rounded),
+ onPressed: () {
+ if (context.canPop()) {
+ context.pop();
+ } else {
+ context.go(HomeRoute.path);
+ }
+ },
+ ),
+ actions: [
+ IconButton(
+ onPressed: () => Share.share(
+ "${state.value?.title}... ${state.value?.description}",
+ ),
+ icon: const Icon(Icons.share_rounded),
+ ),
+ ],
+ ),
+ body: Skeletonizer(
+ enabled: state.isLoading,
+ child: state.hasError
+ ? Center(child: Text(state.error.toString()))
+ : _DetailsContent(
+ item: state.value ??
+ HomeItem(
+ id: id,
+ title: BoneMock.title,
+ description: BoneMock.paragraph,
+ imageUrl: "",
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class _DetailsContent extends StatelessWidget {
+ const _DetailsContent({
+ required this.item,
+ });
+
+ final HomeItem item;
+
+ @override
+ Widget build(BuildContext context) {
+ return SingleChildScrollView(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (item.imageUrl.isNotEmpty) ...[
+ AsyncImage(
+ url: item.imageUrl,
+ width: double.infinity,
+ height: 350,
+ fit: BoxFit.cover,
+ ),
+ ] else ...[
+ const Divider(
+ thickness: 350,
+ height: 350,
+ ),
+ ],
+ const SizedBox(height: 12),
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Text(
+ item.title,
+ style: context.textTheme.headlineMedium,
+ ),
+ ),
+ const SizedBox(height: 12),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Text(item.description),
+ ),
+ const SizedBox(height: 12),
+ if (item.tags != null && item.tags!.isNotEmpty) ...[
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Wrap(
+ runSpacing: 8,
+ spacing: 8,
+ children: item.tags!
+ .map(
+ (tag) => Chip(label: Text(tag)),
+ )
+ .toList(),
+ ),
+ ),
+ ],
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/modules/base/home/home_controller.dart b/lib/modules/base/home/home_controller.dart
new file mode 100644
index 0000000..8c087d5
--- /dev/null
+++ b/lib/modules/base/home/home_controller.dart
@@ -0,0 +1,48 @@
+import 'package:lite_ref/lite_ref.dart';
+import 'package:flutter_template/modules/base/home/home_events.dart';
+import 'package:flutter_template/modules/base/home/home_service.dart';
+import 'package:flutter_template/modules/base/home/model/home_state.dart';
+import 'package:context_watch_signals/context_watch_signals.dart';
+
+final homeController = Ref.scoped(
+ (ctx) => HomeController(
+ homeService.instance,
+ )..dispatch(HomeEvent.init),
+);
+
+class HomeController extends Disposable {
+ HomeController(this._service);
+
+ final HomeService _service;
+
+ final _state = asyncSignal(AsyncState.loading());
+ ReadonlySignal> get state => _state;
+
+ Future dispatch(HomeEvent event) {
+ switch (event) {
+ case HomeEvent.init:
+ return _load();
+ case HomeEvent.reload:
+ return _load(force: true);
+ }
+ }
+
+ Future _load({bool force = false}) {
+ _state.value = const AsyncLoading();
+ return _service.load(force: force).then((items) {
+ _state.value = AsyncData(
+ HomeState(
+ showBanner: true, // Add logic about showing or not the banner
+ items: items,
+ ),
+ );
+ }).catchError((e, s) {
+ _state.value = AsyncError(e, s);
+ });
+ }
+
+ @override
+ void dispose() {
+ _state.dispose();
+ }
+}
diff --git a/lib/modules/base/home/home_events.dart b/lib/modules/base/home/home_events.dart
new file mode 100644
index 0000000..abdd5f8
--- /dev/null
+++ b/lib/modules/base/home/home_events.dart
@@ -0,0 +1,4 @@
+enum HomeEvent {
+ init,
+ reload,
+}
diff --git a/lib/modules/base/home/home_route.dart b/lib/modules/base/home/home_route.dart
new file mode 100644
index 0000000..fa00e72
--- /dev/null
+++ b/lib/modules/base/home/home_route.dart
@@ -0,0 +1,45 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
+import 'package:go_router/go_router.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/modules/base/widgets/app_scaffold.dart';
+import 'package:flutter_template/modules/base/home/details/details_route.dart';
+import 'package:flutter_template/modules/base/home/home_view.dart';
+import 'package:flutter_template/navigation.dart';
+
+class HomeRoute implements NavigationRoute {
+ static const String path = "/home";
+ static const String name = "home";
+
+ @override
+ GoRoute createRoute() {
+ return GoRoute(
+ path: path,
+ name: name,
+ pageBuilder: (context, state) {
+ if (Breakpoints.largeAndUp.isActive(context)) {
+ return const NoTransitionPage(
+ child: AppScaffoldContent(
+ body: HomeView(),
+ ),
+ );
+ }
+ return MaterialPage(
+ key: state.pageKey,
+ child: const HomeView(),
+ );
+ },
+ routes: [
+ DetailsRoute().createRoute(),
+ ],
+ );
+ }
+
+ @override
+ NavigationDestination createDestination(BuildContext context) {
+ return NavigationDestination(
+ icon: const Icon(Icons.home_rounded),
+ label: context.t.navigation.home,
+ );
+ }
+}
diff --git a/lib/modules/base/home/home_service.dart b/lib/modules/base/home/home_service.dart
new file mode 100644
index 0000000..0c41494
--- /dev/null
+++ b/lib/modules/base/home/home_service.dart
@@ -0,0 +1,49 @@
+import 'package:collection/collection.dart';
+import 'package:dio/dio.dart';
+import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
+import 'package:lite_ref/lite_ref.dart';
+import 'package:flutter_template/modules/base/home/model/home_item.dart';
+
+final homeService = Ref.singleton(() => HomeService());
+
+class HomeService {
+ late final options = CacheOptions(
+ store: MemCacheStore(),
+ policy: CachePolicy.forceCache,
+ );
+
+ late final _dio = Dio()
+ ..interceptors.add(
+ DioCacheInterceptor(options: options),
+ );
+
+ List _items = [];
+
+ Future loadSingle(String id) async {
+ final cached = _items.firstWhereOrNull((item) => item.id == id);
+ if (cached != null) {
+ return cached.copyWith(imageUrl: "https://picsum.photos/id/$id/512/512");
+ }
+ return _dio.get("https://official-joke-api.appspot.com/jokes/$id").then(
+ (response) => _parse(response.data, size: 512),
+ );
+ }
+
+ Future> load({bool force = false}) {
+ return _dio.get("https://official-joke-api.appspot.com/random_ten").then(
+ (response) {
+ return _items = (response.data as List)
+ .map((joke) => _parse(joke))
+ .toList();
+ },
+ );
+ }
+
+ HomeItem _parse(dynamic json, {int size = 128}) => HomeItem(
+ id: json["id"].toString(),
+ title: json["setup"],
+ description: json["punchline"],
+ imageUrl: "https://picsum.photos/id/${json["id"]}/$size/$size",
+ tags: [json["type"]],
+ );
+}
diff --git a/lib/modules/base/home/home_view.dart b/lib/modules/base/home/home_view.dart
new file mode 100644
index 0000000..dc28aa3
--- /dev/null
+++ b/lib/modules/base/home/home_view.dart
@@ -0,0 +1,137 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/modules/base/home/home_controller.dart';
+import 'package:flutter_template/modules/base/home/model/home_item.dart';
+import 'package:flutter_template/modules/base/home/model/home_state.dart';
+import 'package:flutter_template/modules/base/home/widgets/home_card_view.dart';
+import 'package:flutter_template/modules/base/widgets/async_image.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+import 'package:context_watch_signals/context_watch_signals.dart';
+import 'package:skeletonizer/skeletonizer.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+class HomeView extends StatelessWidget {
+ final String? selectedId;
+
+ const HomeView({
+ super.key,
+ this.selectedId,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final state = homeController.of(context).state.watch(context);
+ return SafeArea(
+ child: Scaffold(
+ body: SingleChildScrollView(
+ clipBehavior: Clip.none,
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ context.t.home.title,
+ style: context.textTheme.headlineLarge,
+ ),
+ Text(
+ context.t.home.subtitle,
+ style: context.textTheme.labelLarge,
+ ),
+ const SizedBox(height: 24),
+ Skeletonizer(
+ enabled: state.isLoading,
+ child: _buildFeed(state),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFeed(AsyncState state) {
+ if (state.hasError) {
+ return Center(child: Text(state.error.toString()));
+ }
+
+ final items = state.value?.items ?? List.empty(growable: true);
+ if (items.isEmpty && state.isLoading) {
+ for (int i = 0; i < 3; i++) {
+ items.add(
+ HomeItem(
+ id: "$i",
+ title: BoneMock.name,
+ description: BoneMock.paragraph,
+ imageUrl: "",
+ ),
+ );
+ }
+ }
+ return _HomeFeed(
+ isLoading: state.isLoading,
+ showBanner: state.value?.showBanner ?? false,
+ selectedId: selectedId,
+ items: items,
+ );
+ }
+}
+
+class _HomeFeed extends StatelessWidget {
+ final bool isLoading;
+ final String? selectedId;
+ final bool showBanner;
+ final List items;
+
+ const _HomeFeed({
+ required this.isLoading,
+ required this.showBanner,
+ required this.selectedId,
+ required this.items,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ if (showBanner) ...[
+ HomeCardView(
+ title: context.t.home.cta.title,
+ content: context.t.home.cta.content,
+ image: const Icon(Icons.rocket_launch_rounded, size: 48),
+ actions: [
+ FilledButton(
+ onPressed: () => launchUrl(Uri.parse(context.t.home.cta.link)),
+ child: Text(context.t.home.cta.button),
+ ),
+ ],
+ ),
+ const SizedBox(height: 18),
+ ],
+ ...items.map(
+ (e) => HomeCardView(
+ title: e.title,
+ content: e.description,
+ dense: true,
+ selected: e.id == selectedId,
+ image: ClipRRect(
+ borderRadius: BorderRadius.circular(8),
+ child: e.imageUrl.isNotEmpty
+ ? AsyncImage(url: e.imageUrl, width: 48)
+ : const Bone.icon(size: 48),
+ ),
+ onTap: () {
+ if (context.canPop()) {
+ context.pushReplacement("/home/${e.id}");
+ } else {
+ context.push("/home/${e.id}");
+ }
+ },
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/modules/base/home/model/home_item.dart b/lib/modules/base/home/model/home_item.dart
new file mode 100644
index 0000000..5dbd2f9
--- /dev/null
+++ b/lib/modules/base/home/model/home_item.dart
@@ -0,0 +1,18 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'home_item.freezed.dart';
+part 'home_item.g.dart';
+
+@freezed
+class HomeItem with _$HomeItem {
+ const factory HomeItem({
+ required String id,
+ required String title,
+ required String description,
+ required String imageUrl,
+ List? tags,
+ }) = _HomeItem;
+
+ factory HomeItem.fromJson(Map json) =>
+ _$HomeItemFromJson(json);
+}
diff --git a/lib/modules/base/home/model/home_item.freezed.dart b/lib/modules/base/home/model/home_item.freezed.dart
new file mode 100644
index 0000000..175988c
--- /dev/null
+++ b/lib/modules/base/home/model/home_item.freezed.dart
@@ -0,0 +1,258 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'home_item.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
+
+HomeItem _$HomeItemFromJson(Map json) {
+ return _HomeItem.fromJson(json);
+}
+
+/// @nodoc
+mixin _$HomeItem {
+ String get id => throw _privateConstructorUsedError;
+ String get title => throw _privateConstructorUsedError;
+ String get description => throw _privateConstructorUsedError;
+ String get imageUrl => throw _privateConstructorUsedError;
+ List? get tags => throw _privateConstructorUsedError;
+
+ /// Serializes this HomeItem to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of HomeItem
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $HomeItemCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $HomeItemCopyWith<$Res> {
+ factory $HomeItemCopyWith(HomeItem value, $Res Function(HomeItem) then) =
+ _$HomeItemCopyWithImpl<$Res, HomeItem>;
+ @useResult
+ $Res call(
+ {String id,
+ String title,
+ String description,
+ String imageUrl,
+ List? tags});
+}
+
+/// @nodoc
+class _$HomeItemCopyWithImpl<$Res, $Val extends HomeItem>
+ implements $HomeItemCopyWith<$Res> {
+ _$HomeItemCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of HomeItem
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? id = null,
+ Object? title = null,
+ Object? description = null,
+ Object? imageUrl = null,
+ Object? tags = freezed,
+ }) {
+ return _then(_value.copyWith(
+ id: null == id
+ ? _value.id
+ : id // ignore: cast_nullable_to_non_nullable
+ as String,
+ title: null == title
+ ? _value.title
+ : title // ignore: cast_nullable_to_non_nullable
+ as String,
+ description: null == description
+ ? _value.description
+ : description // ignore: cast_nullable_to_non_nullable
+ as String,
+ imageUrl: null == imageUrl
+ ? _value.imageUrl
+ : imageUrl // ignore: cast_nullable_to_non_nullable
+ as String,
+ tags: freezed == tags
+ ? _value.tags
+ : tags // ignore: cast_nullable_to_non_nullable
+ as List?,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$HomeItemImplCopyWith<$Res>
+ implements $HomeItemCopyWith<$Res> {
+ factory _$$HomeItemImplCopyWith(
+ _$HomeItemImpl value, $Res Function(_$HomeItemImpl) then) =
+ __$$HomeItemImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call(
+ {String id,
+ String title,
+ String description,
+ String imageUrl,
+ List? tags});
+}
+
+/// @nodoc
+class __$$HomeItemImplCopyWithImpl<$Res>
+ extends _$HomeItemCopyWithImpl<$Res, _$HomeItemImpl>
+ implements _$$HomeItemImplCopyWith<$Res> {
+ __$$HomeItemImplCopyWithImpl(
+ _$HomeItemImpl _value, $Res Function(_$HomeItemImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of HomeItem
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? id = null,
+ Object? title = null,
+ Object? description = null,
+ Object? imageUrl = null,
+ Object? tags = freezed,
+ }) {
+ return _then(_$HomeItemImpl(
+ id: null == id
+ ? _value.id
+ : id // ignore: cast_nullable_to_non_nullable
+ as String,
+ title: null == title
+ ? _value.title
+ : title // ignore: cast_nullable_to_non_nullable
+ as String,
+ description: null == description
+ ? _value.description
+ : description // ignore: cast_nullable_to_non_nullable
+ as String,
+ imageUrl: null == imageUrl
+ ? _value.imageUrl
+ : imageUrl // ignore: cast_nullable_to_non_nullable
+ as String,
+ tags: freezed == tags
+ ? _value._tags
+ : tags // ignore: cast_nullable_to_non_nullable
+ as List?,
+ ));
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$HomeItemImpl implements _HomeItem {
+ const _$HomeItemImpl(
+ {required this.id,
+ required this.title,
+ required this.description,
+ required this.imageUrl,
+ final List? tags})
+ : _tags = tags;
+
+ factory _$HomeItemImpl.fromJson(Map json) =>
+ _$$HomeItemImplFromJson(json);
+
+ @override
+ final String id;
+ @override
+ final String title;
+ @override
+ final String description;
+ @override
+ final String imageUrl;
+ final List? _tags;
+ @override
+ List? get tags {
+ final value = _tags;
+ if (value == null) return null;
+ if (_tags is EqualUnmodifiableListView) return _tags;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(value);
+ }
+
+ @override
+ String toString() {
+ return 'HomeItem(id: $id, title: $title, description: $description, imageUrl: $imageUrl, tags: $tags)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$HomeItemImpl &&
+ (identical(other.id, id) || other.id == id) &&
+ (identical(other.title, title) || other.title == title) &&
+ (identical(other.description, description) ||
+ other.description == description) &&
+ (identical(other.imageUrl, imageUrl) ||
+ other.imageUrl == imageUrl) &&
+ const DeepCollectionEquality().equals(other._tags, _tags));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, id, title, description, imageUrl,
+ const DeepCollectionEquality().hash(_tags));
+
+ /// Create a copy of HomeItem
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$HomeItemImplCopyWith<_$HomeItemImpl> get copyWith =>
+ __$$HomeItemImplCopyWithImpl<_$HomeItemImpl>(this, _$identity);
+
+ @override
+ Map toJson() {
+ return _$$HomeItemImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class _HomeItem implements HomeItem {
+ const factory _HomeItem(
+ {required final String id,
+ required final String title,
+ required final String description,
+ required final String imageUrl,
+ final List? tags}) = _$HomeItemImpl;
+
+ factory _HomeItem.fromJson(Map json) =
+ _$HomeItemImpl.fromJson;
+
+ @override
+ String get id;
+ @override
+ String get title;
+ @override
+ String get description;
+ @override
+ String get imageUrl;
+ @override
+ List? get tags;
+
+ /// Create a copy of HomeItem
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$HomeItemImplCopyWith<_$HomeItemImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/modules/base/home/model/home_item.g.dart b/lib/modules/base/home/model/home_item.g.dart
new file mode 100644
index 0000000..d3a6524
--- /dev/null
+++ b/lib/modules/base/home/model/home_item.g.dart
@@ -0,0 +1,25 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'home_item.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$HomeItemImpl _$$HomeItemImplFromJson(Map json) =>
+ _$HomeItemImpl(
+ id: json['id'] as String,
+ title: json['title'] as String,
+ description: json['description'] as String,
+ imageUrl: json['imageUrl'] as String,
+ tags: (json['tags'] as List?)?.map((e) => e as String).toList(),
+ );
+
+Map _$$HomeItemImplToJson(_$HomeItemImpl instance) =>
+ {
+ 'id': instance.id,
+ 'title': instance.title,
+ 'description': instance.description,
+ 'imageUrl': instance.imageUrl,
+ 'tags': instance.tags,
+ };
diff --git a/lib/modules/base/home/model/home_state.dart b/lib/modules/base/home/model/home_state.dart
new file mode 100644
index 0000000..c55e630
--- /dev/null
+++ b/lib/modules/base/home/model/home_state.dart
@@ -0,0 +1,16 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:flutter_template/modules/base/home/model/home_item.dart';
+
+part 'home_state.freezed.dart';
+part 'home_state.g.dart';
+
+@freezed
+class HomeState with _$HomeState {
+ const factory HomeState({
+ required bool showBanner,
+ required List items,
+ }) = _HomeState;
+
+ factory HomeState.fromJson(Map json) =>
+ _$HomeStateFromJson(json);
+}
diff --git a/lib/modules/base/home/model/home_state.freezed.dart b/lib/modules/base/home/model/home_state.freezed.dart
new file mode 100644
index 0000000..84d83e9
--- /dev/null
+++ b/lib/modules/base/home/model/home_state.freezed.dart
@@ -0,0 +1,190 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'home_state.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
+
+HomeState _$HomeStateFromJson(Map json) {
+ return _HomeState.fromJson(json);
+}
+
+/// @nodoc
+mixin _$HomeState {
+ bool get showBanner => throw _privateConstructorUsedError;
+ List get items => throw _privateConstructorUsedError;
+
+ /// Serializes this HomeState to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of HomeState
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $HomeStateCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $HomeStateCopyWith<$Res> {
+ factory $HomeStateCopyWith(HomeState value, $Res Function(HomeState) then) =
+ _$HomeStateCopyWithImpl<$Res, HomeState>;
+ @useResult
+ $Res call({bool showBanner, List items});
+}
+
+/// @nodoc
+class _$HomeStateCopyWithImpl<$Res, $Val extends HomeState>
+ implements $HomeStateCopyWith<$Res> {
+ _$HomeStateCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of HomeState
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? showBanner = null,
+ Object? items = null,
+ }) {
+ return _then(_value.copyWith(
+ showBanner: null == showBanner
+ ? _value.showBanner
+ : showBanner // ignore: cast_nullable_to_non_nullable
+ as bool,
+ items: null == items
+ ? _value.items
+ : items // ignore: cast_nullable_to_non_nullable
+ as List,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$HomeStateImplCopyWith<$Res>
+ implements $HomeStateCopyWith<$Res> {
+ factory _$$HomeStateImplCopyWith(
+ _$HomeStateImpl value, $Res Function(_$HomeStateImpl) then) =
+ __$$HomeStateImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({bool showBanner, List items});
+}
+
+/// @nodoc
+class __$$HomeStateImplCopyWithImpl<$Res>
+ extends _$HomeStateCopyWithImpl<$Res, _$HomeStateImpl>
+ implements _$$HomeStateImplCopyWith<$Res> {
+ __$$HomeStateImplCopyWithImpl(
+ _$HomeStateImpl _value, $Res Function(_$HomeStateImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of HomeState
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? showBanner = null,
+ Object? items = null,
+ }) {
+ return _then(_$HomeStateImpl(
+ showBanner: null == showBanner
+ ? _value.showBanner
+ : showBanner // ignore: cast_nullable_to_non_nullable
+ as bool,
+ items: null == items
+ ? _value._items
+ : items // ignore: cast_nullable_to_non_nullable
+ as List,
+ ));
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$HomeStateImpl implements _HomeState {
+ const _$HomeStateImpl(
+ {required this.showBanner, required final List items})
+ : _items = items;
+
+ factory _$HomeStateImpl.fromJson(Map json) =>
+ _$$HomeStateImplFromJson(json);
+
+ @override
+ final bool showBanner;
+ final List _items;
+ @override
+ List get items {
+ if (_items is EqualUnmodifiableListView) return _items;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_items);
+ }
+
+ @override
+ String toString() {
+ return 'HomeState(showBanner: $showBanner, items: $items)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$HomeStateImpl &&
+ (identical(other.showBanner, showBanner) ||
+ other.showBanner == showBanner) &&
+ const DeepCollectionEquality().equals(other._items, _items));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(
+ runtimeType, showBanner, const DeepCollectionEquality().hash(_items));
+
+ /// Create a copy of HomeState
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$HomeStateImplCopyWith<_$HomeStateImpl> get copyWith =>
+ __$$HomeStateImplCopyWithImpl<_$HomeStateImpl>(this, _$identity);
+
+ @override
+ Map toJson() {
+ return _$$HomeStateImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class _HomeState implements HomeState {
+ const factory _HomeState(
+ {required final bool showBanner,
+ required final List items}) = _$HomeStateImpl;
+
+ factory _HomeState.fromJson(Map json) =
+ _$HomeStateImpl.fromJson;
+
+ @override
+ bool get showBanner;
+ @override
+ List get items;
+
+ /// Create a copy of HomeState
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$HomeStateImplCopyWith<_$HomeStateImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/modules/base/home/model/home_state.g.dart b/lib/modules/base/home/model/home_state.g.dart
new file mode 100644
index 0000000..43f0d43
--- /dev/null
+++ b/lib/modules/base/home/model/home_state.g.dart
@@ -0,0 +1,21 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'home_state.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$HomeStateImpl _$$HomeStateImplFromJson(Map json) =>
+ _$HomeStateImpl(
+ showBanner: json['showBanner'] as bool,
+ items: (json['items'] as List)
+ .map((e) => HomeItem.fromJson(e as Map))
+ .toList(),
+ );
+
+Map _$$HomeStateImplToJson(_$HomeStateImpl instance) =>
+ {
+ 'showBanner': instance.showBanner,
+ 'items': instance.items,
+ };
diff --git a/lib/modules/base/home/widgets/home_card_view.dart b/lib/modules/base/home/widgets/home_card_view.dart
new file mode 100644
index 0000000..40b30d1
--- /dev/null
+++ b/lib/modules/base/home/widgets/home_card_view.dart
@@ -0,0 +1,72 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+
+class HomeCardView extends StatelessWidget {
+ final String title;
+ final String content;
+ final Widget? image;
+ final bool selected;
+ final bool dense;
+ final List? actions;
+ final VoidCallback? onTap;
+
+ const HomeCardView({
+ super.key,
+ required this.title,
+ required this.content,
+ this.selected = false,
+ this.dense = false,
+ this.image,
+ this.actions,
+ this.onTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ if (selected) {
+ WidgetsBinding.instance.addPostFrameCallback(
+ (duration) => Scrollable.ensureVisible(
+ context,
+ alignment: 0.5,
+ curve: Curves.decelerate,
+ duration: const Duration(milliseconds: 300),
+ ),
+ );
+ }
+ return Card(
+ margin: const EdgeInsets.symmetric(horizontal: 0, vertical: 8),
+ child: ListTile(
+ dense: dense,
+ title: Text(
+ title,
+ style: context.textTheme.titleMedium,
+ maxLines: dense ? 1 : null,
+ overflow: TextOverflow.ellipsis,
+ ),
+ onTap: onTap,
+ leading: image,
+ titleAlignment: ListTileTitleAlignment.titleHeight,
+ subtitle: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ content,
+ textAlign: TextAlign.start,
+ maxLines: dense ? 1 : null,
+ overflow: TextOverflow.fade,
+ ),
+ if (actions != null && actions!.isNotEmpty) ...[
+ const SizedBox(height: 12),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: actions ?? [],
+ ),
+ ],
+ ],
+ ),
+ contentPadding: const EdgeInsets.all(16),
+ ),
+ );
+ }
+}
diff --git a/lib/modules/base/image/app_image.dart b/lib/modules/base/image/app_image.dart
new file mode 100644
index 0000000..582eddc
--- /dev/null
+++ b/lib/modules/base/image/app_image.dart
@@ -0,0 +1,23 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'app_image.freezed.dart';
+part 'app_image.g.dart';
+
+@freezed
+sealed class AppImage with _$AppImage {
+
+ const factory AppImage.static({
+ required String uri,
+ }) = StaticImage;
+
+ const factory AppImage.network({
+ required String uri,
+ }) = NetworkImage;
+
+ const factory AppImage.rive({
+ required String uri,
+ required String name,
+ }) = RiveImage;
+
+ factory AppImage.fromJson(Map json) => _$AppImageFromJson(json);
+}
diff --git a/lib/modules/base/image/app_image.freezed.dart b/lib/modules/base/image/app_image.freezed.dart
new file mode 100644
index 0000000..578e710
--- /dev/null
+++ b/lib/modules/base/image/app_image.freezed.dart
@@ -0,0 +1,638 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'app_image.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
+
+AppImage _$AppImageFromJson(Map json) {
+ switch (json['runtimeType']) {
+ case 'static':
+ return StaticImage.fromJson(json);
+ case 'network':
+ return NetworkImage.fromJson(json);
+ case 'rive':
+ return RiveImage.fromJson(json);
+
+ default:
+ throw CheckedFromJsonException(json, 'runtimeType', 'AppImage',
+ 'Invalid union type "${json['runtimeType']}"!');
+ }
+}
+
+/// @nodoc
+mixin _$AppImage {
+ String get uri => throw _privateConstructorUsedError;
+ @optionalTypeArgs
+ TResult when({
+ required TResult Function(String uri) static,
+ required TResult Function(String uri) network,
+ required TResult Function(String uri, String name) rive,
+ }) =>
+ throw _privateConstructorUsedError;
+ @optionalTypeArgs
+ TResult? whenOrNull({
+ TResult? Function(String uri)? static,
+ TResult? Function(String uri)? network,
+ TResult? Function(String uri, String name)? rive,
+ }) =>
+ throw _privateConstructorUsedError;
+ @optionalTypeArgs
+ TResult maybeWhen({
+ TResult Function(String uri)? static,
+ TResult Function(String uri)? network,
+ TResult Function(String uri, String name)? rive,
+ required TResult orElse(),
+ }) =>
+ throw _privateConstructorUsedError;
+ @optionalTypeArgs
+ TResult map({
+ required TResult Function(StaticImage value) static,
+ required TResult Function(NetworkImage value) network,
+ required TResult Function(RiveImage value) rive,
+ }) =>
+ throw _privateConstructorUsedError;
+ @optionalTypeArgs
+ TResult? mapOrNull({
+ TResult? Function(StaticImage value)? static,
+ TResult? Function(NetworkImage value)? network,
+ TResult? Function(RiveImage value)? rive,
+ }) =>
+ throw _privateConstructorUsedError;
+ @optionalTypeArgs
+ TResult maybeMap({
+ TResult Function(StaticImage value)? static,
+ TResult Function(NetworkImage value)? network,
+ TResult Function(RiveImage value)? rive,
+ required TResult orElse(),
+ }) =>
+ throw _privateConstructorUsedError;
+
+ /// Serializes this AppImage to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of AppImage
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $AppImageCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $AppImageCopyWith<$Res> {
+ factory $AppImageCopyWith(AppImage value, $Res Function(AppImage) then) =
+ _$AppImageCopyWithImpl<$Res, AppImage>;
+ @useResult
+ $Res call({String uri});
+}
+
+/// @nodoc
+class _$AppImageCopyWithImpl<$Res, $Val extends AppImage>
+ implements $AppImageCopyWith<$Res> {
+ _$AppImageCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of AppImage
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? uri = null,
+ }) {
+ return _then(_value.copyWith(
+ uri: null == uri
+ ? _value.uri
+ : uri // ignore: cast_nullable_to_non_nullable
+ as String,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$StaticImageImplCopyWith<$Res>
+ implements $AppImageCopyWith<$Res> {
+ factory _$$StaticImageImplCopyWith(
+ _$StaticImageImpl value, $Res Function(_$StaticImageImpl) then) =
+ __$$StaticImageImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({String uri});
+}
+
+/// @nodoc
+class __$$StaticImageImplCopyWithImpl<$Res>
+ extends _$AppImageCopyWithImpl<$Res, _$StaticImageImpl>
+ implements _$$StaticImageImplCopyWith<$Res> {
+ __$$StaticImageImplCopyWithImpl(
+ _$StaticImageImpl _value, $Res Function(_$StaticImageImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of AppImage
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? uri = null,
+ }) {
+ return _then(_$StaticImageImpl(
+ uri: null == uri
+ ? _value.uri
+ : uri // ignore: cast_nullable_to_non_nullable
+ as String,
+ ));
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$StaticImageImpl implements StaticImage {
+ const _$StaticImageImpl({required this.uri, final String? $type})
+ : $type = $type ?? 'static';
+
+ factory _$StaticImageImpl.fromJson(Map json) =>
+ _$$StaticImageImplFromJson(json);
+
+ @override
+ final String uri;
+
+ @JsonKey(name: 'runtimeType')
+ final String $type;
+
+ @override
+ String toString() {
+ return 'AppImage.static(uri: $uri)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$StaticImageImpl &&
+ (identical(other.uri, uri) || other.uri == uri));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, uri);
+
+ /// Create a copy of AppImage
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$StaticImageImplCopyWith<_$StaticImageImpl> get copyWith =>
+ __$$StaticImageImplCopyWithImpl<_$StaticImageImpl>(this, _$identity);
+
+ @override
+ @optionalTypeArgs
+ TResult when({
+ required TResult Function(String uri) static,
+ required TResult Function(String uri) network,
+ required TResult Function(String uri, String name) rive,
+ }) {
+ return static(uri);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult? whenOrNull({
+ TResult? Function(String uri)? static,
+ TResult? Function(String uri)? network,
+ TResult? Function(String uri, String name)? rive,
+ }) {
+ return static?.call(uri);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult maybeWhen({
+ TResult Function(String uri)? static,
+ TResult Function(String uri)? network,
+ TResult Function(String uri, String name)? rive,
+ required TResult orElse(),
+ }) {
+ if (static != null) {
+ return static(uri);
+ }
+ return orElse();
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult map({
+ required TResult Function(StaticImage value) static,
+ required TResult Function(NetworkImage value) network,
+ required TResult Function(RiveImage value) rive,
+ }) {
+ return static(this);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult? mapOrNull({
+ TResult? Function(StaticImage value)? static,
+ TResult? Function(NetworkImage value)? network,
+ TResult? Function(RiveImage value)? rive,
+ }) {
+ return static?.call(this);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult maybeMap({
+ TResult Function(StaticImage value)? static,
+ TResult Function(NetworkImage value)? network,
+ TResult Function(RiveImage value)? rive,
+ required TResult orElse(),
+ }) {
+ if (static != null) {
+ return static(this);
+ }
+ return orElse();
+ }
+
+ @override
+ Map toJson() {
+ return _$$StaticImageImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class StaticImage implements AppImage {
+ const factory StaticImage({required final String uri}) = _$StaticImageImpl;
+
+ factory StaticImage.fromJson(Map json) =
+ _$StaticImageImpl.fromJson;
+
+ @override
+ String get uri;
+
+ /// Create a copy of AppImage
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$StaticImageImplCopyWith<_$StaticImageImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class _$$NetworkImageImplCopyWith<$Res>
+ implements $AppImageCopyWith<$Res> {
+ factory _$$NetworkImageImplCopyWith(
+ _$NetworkImageImpl value, $Res Function(_$NetworkImageImpl) then) =
+ __$$NetworkImageImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({String uri});
+}
+
+/// @nodoc
+class __$$NetworkImageImplCopyWithImpl<$Res>
+ extends _$AppImageCopyWithImpl<$Res, _$NetworkImageImpl>
+ implements _$$NetworkImageImplCopyWith<$Res> {
+ __$$NetworkImageImplCopyWithImpl(
+ _$NetworkImageImpl _value, $Res Function(_$NetworkImageImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of AppImage
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? uri = null,
+ }) {
+ return _then(_$NetworkImageImpl(
+ uri: null == uri
+ ? _value.uri
+ : uri // ignore: cast_nullable_to_non_nullable
+ as String,
+ ));
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$NetworkImageImpl implements NetworkImage {
+ const _$NetworkImageImpl({required this.uri, final String? $type})
+ : $type = $type ?? 'network';
+
+ factory _$NetworkImageImpl.fromJson(Map json) =>
+ _$$NetworkImageImplFromJson(json);
+
+ @override
+ final String uri;
+
+ @JsonKey(name: 'runtimeType')
+ final String $type;
+
+ @override
+ String toString() {
+ return 'AppImage.network(uri: $uri)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$NetworkImageImpl &&
+ (identical(other.uri, uri) || other.uri == uri));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, uri);
+
+ /// Create a copy of AppImage
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$NetworkImageImplCopyWith<_$NetworkImageImpl> get copyWith =>
+ __$$NetworkImageImplCopyWithImpl<_$NetworkImageImpl>(this, _$identity);
+
+ @override
+ @optionalTypeArgs
+ TResult when({
+ required TResult Function(String uri) static,
+ required TResult Function(String uri) network,
+ required TResult Function(String uri, String name) rive,
+ }) {
+ return network(uri);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult? whenOrNull({
+ TResult? Function(String uri)? static,
+ TResult? Function(String uri)? network,
+ TResult? Function(String uri, String name)? rive,
+ }) {
+ return network?.call(uri);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult maybeWhen({
+ TResult Function(String uri)? static,
+ TResult Function(String uri)? network,
+ TResult Function(String uri, String name)? rive,
+ required TResult orElse(),
+ }) {
+ if (network != null) {
+ return network(uri);
+ }
+ return orElse();
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult map({
+ required TResult Function(StaticImage value) static,
+ required TResult Function(NetworkImage value) network,
+ required TResult Function(RiveImage value) rive,
+ }) {
+ return network(this);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult? mapOrNull({
+ TResult? Function(StaticImage value)? static,
+ TResult? Function(NetworkImage value)? network,
+ TResult? Function(RiveImage value)? rive,
+ }) {
+ return network?.call(this);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult maybeMap({
+ TResult Function(StaticImage value)? static,
+ TResult Function(NetworkImage value)? network,
+ TResult Function(RiveImage value)? rive,
+ required TResult orElse(),
+ }) {
+ if (network != null) {
+ return network(this);
+ }
+ return orElse();
+ }
+
+ @override
+ Map toJson() {
+ return _$$NetworkImageImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class NetworkImage implements AppImage {
+ const factory NetworkImage({required final String uri}) = _$NetworkImageImpl;
+
+ factory NetworkImage.fromJson(Map json) =
+ _$NetworkImageImpl.fromJson;
+
+ @override
+ String get uri;
+
+ /// Create a copy of AppImage
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$NetworkImageImplCopyWith<_$NetworkImageImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class _$$RiveImageImplCopyWith<$Res>
+ implements $AppImageCopyWith<$Res> {
+ factory _$$RiveImageImplCopyWith(
+ _$RiveImageImpl value, $Res Function(_$RiveImageImpl) then) =
+ __$$RiveImageImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({String uri, String name});
+}
+
+/// @nodoc
+class __$$RiveImageImplCopyWithImpl<$Res>
+ extends _$AppImageCopyWithImpl<$Res, _$RiveImageImpl>
+ implements _$$RiveImageImplCopyWith<$Res> {
+ __$$RiveImageImplCopyWithImpl(
+ _$RiveImageImpl _value, $Res Function(_$RiveImageImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of AppImage
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? uri = null,
+ Object? name = null,
+ }) {
+ return _then(_$RiveImageImpl(
+ uri: null == uri
+ ? _value.uri
+ : uri // ignore: cast_nullable_to_non_nullable
+ as String,
+ name: null == name
+ ? _value.name
+ : name // ignore: cast_nullable_to_non_nullable
+ as String,
+ ));
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$RiveImageImpl implements RiveImage {
+ const _$RiveImageImpl(
+ {required this.uri, required this.name, final String? $type})
+ : $type = $type ?? 'rive';
+
+ factory _$RiveImageImpl.fromJson(Map json) =>
+ _$$RiveImageImplFromJson(json);
+
+ @override
+ final String uri;
+ @override
+ final String name;
+
+ @JsonKey(name: 'runtimeType')
+ final String $type;
+
+ @override
+ String toString() {
+ return 'AppImage.rive(uri: $uri, name: $name)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$RiveImageImpl &&
+ (identical(other.uri, uri) || other.uri == uri) &&
+ (identical(other.name, name) || other.name == name));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, uri, name);
+
+ /// Create a copy of AppImage
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$RiveImageImplCopyWith<_$RiveImageImpl> get copyWith =>
+ __$$RiveImageImplCopyWithImpl<_$RiveImageImpl>(this, _$identity);
+
+ @override
+ @optionalTypeArgs
+ TResult when({
+ required TResult Function(String uri) static,
+ required TResult Function(String uri) network,
+ required TResult Function(String uri, String name) rive,
+ }) {
+ return rive(uri, name);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult? whenOrNull({
+ TResult? Function(String uri)? static,
+ TResult? Function(String uri)? network,
+ TResult? Function(String uri, String name)? rive,
+ }) {
+ return rive?.call(uri, name);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult maybeWhen({
+ TResult Function(String uri)? static,
+ TResult Function(String uri)? network,
+ TResult Function(String uri, String name)? rive,
+ required TResult orElse(),
+ }) {
+ if (rive != null) {
+ return rive(uri, name);
+ }
+ return orElse();
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult map({
+ required TResult Function(StaticImage value) static,
+ required TResult Function(NetworkImage value) network,
+ required TResult Function(RiveImage value) rive,
+ }) {
+ return rive(this);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult? mapOrNull({
+ TResult? Function(StaticImage value)? static,
+ TResult? Function(NetworkImage value)? network,
+ TResult? Function(RiveImage value)? rive,
+ }) {
+ return rive?.call(this);
+ }
+
+ @override
+ @optionalTypeArgs
+ TResult maybeMap({
+ TResult Function(StaticImage value)? static,
+ TResult Function(NetworkImage value)? network,
+ TResult Function(RiveImage value)? rive,
+ required TResult orElse(),
+ }) {
+ if (rive != null) {
+ return rive(this);
+ }
+ return orElse();
+ }
+
+ @override
+ Map toJson() {
+ return _$$RiveImageImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class RiveImage implements AppImage {
+ const factory RiveImage(
+ {required final String uri,
+ required final String name}) = _$RiveImageImpl;
+
+ factory RiveImage.fromJson(Map json) =
+ _$RiveImageImpl.fromJson;
+
+ @override
+ String get uri;
+ String get name;
+
+ /// Create a copy of AppImage
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$RiveImageImplCopyWith<_$RiveImageImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/modules/base/image/app_image.g.dart b/lib/modules/base/image/app_image.g.dart
new file mode 100644
index 0000000..ba1b3ea
--- /dev/null
+++ b/lib/modules/base/image/app_image.g.dart
@@ -0,0 +1,45 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'app_image.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$StaticImageImpl _$$StaticImageImplFromJson(Map json) =>
+ _$StaticImageImpl(
+ uri: json['uri'] as String,
+ $type: json['runtimeType'] as String?,
+ );
+
+Map _$$StaticImageImplToJson(_$StaticImageImpl instance) =>
+ {
+ 'uri': instance.uri,
+ 'runtimeType': instance.$type,
+ };
+
+_$NetworkImageImpl _$$NetworkImageImplFromJson(Map json) =>
+ _$NetworkImageImpl(
+ uri: json['uri'] as String,
+ $type: json['runtimeType'] as String?,
+ );
+
+Map _$$NetworkImageImplToJson(_$NetworkImageImpl instance) =>
+ {
+ 'uri': instance.uri,
+ 'runtimeType': instance.$type,
+ };
+
+_$RiveImageImpl _$$RiveImageImplFromJson(Map json) =>
+ _$RiveImageImpl(
+ uri: json['uri'] as String,
+ name: json['name'] as String,
+ $type: json['runtimeType'] as String?,
+ );
+
+Map _$$RiveImageImplToJson(_$RiveImageImpl instance) =>
+ {
+ 'uri': instance.uri,
+ 'name': instance.name,
+ 'runtimeType': instance.$type,
+ };
diff --git a/lib/modules/base/image/app_image_widget.dart b/lib/modules/base/image/app_image_widget.dart
new file mode 100644
index 0000000..4ebab85
--- /dev/null
+++ b/lib/modules/base/image/app_image_widget.dart
@@ -0,0 +1,99 @@
+import 'package:flutter/material.dart';
+import 'package:rive/rive.dart' as rive;
+import 'package:flutter_template/modules/base/image/app_image.dart';
+import 'package:flutter_template/modules/base/widgets/async_image.dart';
+
+class AppImageWidget extends StatelessWidget {
+ final AppImage image;
+ final double width;
+ final double height;
+ final BoxFit? fit;
+
+ const AppImageWidget({
+ super.key,
+ required this.image,
+ required this.width,
+ required this.height,
+ this.fit,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return image.map(
+ static: (value) => Image.asset(
+ value.uri,
+ width: width,
+ height: height,
+ fit: fit,
+ ),
+ network: (value) => AsyncImage(
+ url: value.uri,
+ width: width,
+ height: height,
+ fit: fit,
+ ),
+ rive: (value) => RiveImageWidget(
+ uri: value.uri,
+ name: value.name,
+ width: width,
+ height: height,
+ fit: fit,
+ ),
+ );
+ }
+}
+
+class RiveImageWidget extends StatefulWidget {
+ final String uri;
+ final String name;
+ final double width;
+ final double height;
+ final BoxFit? fit;
+
+ const RiveImageWidget({
+ super.key,
+ required this.uri,
+ required this.name,
+ required this.width,
+ required this.height,
+ this.fit,
+ });
+
+ @override
+ State createState() => _RiveImageWidgetState();
+}
+
+class _RiveImageWidgetState extends State {
+ late final rive.RiveAnimationController _controller;
+
+ @override
+ void initState() {
+ _controller = rive.SimpleAnimation(
+ widget.name,
+ autoplay: true,
+ );
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: SizedBox(
+ width: widget.width,
+ height: widget.height,
+ child: rive.RiveAnimation.asset(
+ widget.uri,
+ fit: widget.fit,
+ animations: [widget.name],
+ controllers: [_controller],
+ ),
+ ),
+ );
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+}
diff --git a/lib/modules/base/logger.dart b/lib/modules/base/logger.dart
new file mode 100644
index 0000000..eedbab3
--- /dev/null
+++ b/lib/modules/base/logger.dart
@@ -0,0 +1,45 @@
+import 'package:logger/logger.dart';
+
+final Logger _logger = Logger();
+
+/// Use this log function everywhere to use the active logger
+/// Use disable logging to disable all logging
+/// Using lambda functions avoid executing them, this avoid possible errors
+/// in the logging function itself but also exectuion time.
+void log({
+ String Function()? d,
+ String Function()? t,
+ String Function()? i,
+ String Function()? w,
+ String Function()? e,
+ String Function()? f,
+ Object? error,
+}) {
+ final level = Logger.level.value;
+ if (d != null && level <= Level.debug.value) {
+ _logger.d(d());
+ }
+ if (t != null && level <= Level.trace.value) {
+ _logger.t(t());
+ }
+ if (i != null && level <= Level.info.value) {
+ _logger.i(i());
+ }
+ if (w != null && level <= Level.warning.value) {
+ _logger.w(w());
+ }
+ if ((e != null || error != null) && level <= Level.error.value) {
+ _logger.e(
+ e != null ? e() : error?.toString(),
+ error: error,
+ stackTrace: StackTrace.current,
+ );
+ }
+ if ((f != null || error != null) && level <= Level.fatal.value) {
+ _logger.f(
+ f != null ? f() : error?.toString(),
+ error: error,
+ stackTrace: StackTrace.current,
+ );
+ }
+}
diff --git a/lib/modules/base/preferences.dart b/lib/modules/base/preferences.dart
new file mode 100644
index 0000000..b593f2a
--- /dev/null
+++ b/lib/modules/base/preferences.dart
@@ -0,0 +1,11 @@
+import 'package:lite_ref/lite_ref.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+/// Create a singleton instance of SharedPreferences
+/// This loads the preferences in memory and can be accessed
+/// with `preferences.assertInstance`.
+///
+/// More info at https://pub.dev/packages/shared_preferences
+final preferences = Ref.asyncSingleton(
+ () => SharedPreferences.getInstance(),
+);
diff --git a/lib/modules/base/rating_controller.dart b/lib/modules/base/rating_controller.dart
new file mode 100644
index 0000000..8b8a873
--- /dev/null
+++ b/lib/modules/base/rating_controller.dart
@@ -0,0 +1,33 @@
+import 'package:flutter/foundation.dart';
+import 'package:in_app_review/in_app_review.dart';
+import 'package:lite_ref/lite_ref.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+final ratingController = Ref.singleton(
+ () => RatingController(InAppReview.instance),
+);
+
+class RatingController {
+ final InAppReview _inAppReview;
+
+ RatingController(this._inAppReview);
+
+ Future requestReview() {
+ return isAvailable().then(
+ (available) => available
+ ? _inAppReview.requestReview().then((_) => true)
+ : Future.value(false),
+ );
+ }
+
+ Future isAvailable() => _inAppReview.isAvailable();
+
+ Future openStoreListing() {
+ if (kIsWeb) {
+ return launchUrl(Uri.parse(t.rate_url));
+ } else {
+ return _inAppReview.openStoreListing();
+ }
+ }
+}
diff --git a/lib/modules/base/settings/settings_controller.dart b/lib/modules/base/settings/settings_controller.dart
new file mode 100644
index 0000000..afdf50f
--- /dev/null
+++ b/lib/modules/base/settings/settings_controller.dart
@@ -0,0 +1,31 @@
+import 'package:context_watch_signals/context_watch_signals.dart';
+import 'package:flutter/material.dart';
+import 'package:lite_ref/lite_ref.dart';
+
+import 'settings_service.dart';
+
+final settingsController = Ref.singleton(
+ () => SettingsController(
+ SettingsService(),
+ ),
+);
+
+/// A class that many Widgets can interact with to read user settings, update
+/// user settings, or listen to user settings changes.
+///
+/// Controllers glue Data Services to Flutter Widgets. The SettingsController
+/// uses the SettingsService to store and retrieve user settings.
+class SettingsController {
+ SettingsController(this._settingsService);
+
+ // Make SettingsService a private variable so it is not used directly.
+ final SettingsService _settingsService;
+
+ late final _themeMode = _settingsService.themeMode().asSignal();
+ ReadonlySignal get themeMode => _themeMode;
+
+ Future dispatch(ThemeMode mode) {
+ _themeMode.value = mode;
+ return _settingsService.updateThemeMode(mode);
+ }
+}
diff --git a/lib/modules/base/settings/settings_route.dart b/lib/modules/base/settings/settings_route.dart
new file mode 100644
index 0000000..08c9236
--- /dev/null
+++ b/lib/modules/base/settings/settings_route.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/navigation.dart';
+import 'package:flutter_template/modules/base/settings/settings_view.dart';
+
+class SettingsRoute implements NavigationRoute {
+ static const String path = "/settings";
+
+ @override
+ GoRoute createRoute() {
+ return GoRoute(
+ path: path,
+ builder: (context, state) => const SettingsView(),
+ );
+ }
+
+ @override
+ NavigationDestination createDestination(BuildContext context) {
+ return NavigationDestination(
+ icon: const Icon(Icons.settings_rounded),
+ label: context.t.navigation.settings,
+ );
+ }
+}
diff --git a/lib/modules/base/settings/settings_service.dart b/lib/modules/base/settings/settings_service.dart
new file mode 100644
index 0000000..1182721
--- /dev/null
+++ b/lib/modules/base/settings/settings_service.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_template/modules/base/preferences.dart';
+
+/// A service that stores and retrieves user settings.
+///
+/// By default, this class does not persist user settings. If you'd like to
+/// persist the user settings locally, use the shared_preferences package. If
+/// you'd like to store settings on a web server, use the http package.
+class SettingsService {
+ /// Loads the User's preferred ThemeMode from local or remote storage.
+ ThemeMode themeMode() {
+ final current = preferences.assertInstance.get("theme_mode");
+ if (current == null) {
+ return ThemeMode.system;
+ }
+ return ThemeMode.values.firstWhere(
+ (element) => element.name == current,
+ );
+ }
+
+ /// Persists the user's preferred ThemeMode to local or remote storage.
+ Future updateThemeMode(ThemeMode theme) {
+ return preferences.assertInstance.setString("theme_mode", theme.name);
+ }
+}
diff --git a/lib/modules/base/settings/settings_view.dart b/lib/modules/base/settings/settings_view.dart
new file mode 100644
index 0000000..a2229f5
--- /dev/null
+++ b/lib/modules/base/settings/settings_view.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/modules/base/rating_controller.dart';
+import 'package:flutter_template/modules/base/settings/widgets/theme_mode_tile.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+import 'package:url_launcher/url_launcher.dart';
+
+class SettingsView extends StatelessWidget {
+ static const routeName = "/settings";
+
+ const SettingsView({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: ListView(
+ clipBehavior: Clip.none,
+ children: [
+ Text(
+ context.t.settings.title,
+ style: context.textTheme.headlineLarge,
+ ),
+ const SizedBox(height: 24),
+ Card.filled(
+ color: context.theme.cardColor,
+ child: Column(
+ children: [
+ const ThemeModeTile(),
+ const Divider(thickness: 1.0, height: 4),
+ ListTile(
+ title: Text(context.t.settings.privacy),
+ leading: const Icon(Icons.policy_rounded),
+ contentPadding: const EdgeInsets.all(12),
+ onTap: () => launchUrl(Uri.parse(context.t.privacy_url)),
+ ),
+ const Divider(thickness: 1.0, height: 4),
+ ListTile(
+ title: Text(context.t.settings.terms),
+ leading: const Icon(Icons.privacy_tip_rounded),
+ contentPadding: const EdgeInsets.all(12),
+ onTap: () => launchUrl(Uri.parse(context.t.terms_url)),
+ ),
+ const Divider(thickness: 1.0, height: 4),
+ ListTile(
+ title: Text(context.t.settings.support),
+ leading: const Icon(Icons.help_rounded),
+ contentPadding: const EdgeInsets.all(12),
+ onTap: () => launchUrl(Uri.parse(context.t.support_url)),
+ ),
+ const Divider(thickness: 1.0, height: 4),
+ ListTile(
+ title: Text(context.t.settings.rate),
+ leading: const Icon(Icons.rate_review_rounded),
+ contentPadding: const EdgeInsets.all(12),
+ onTap: () {
+ ratingController.instance.openStoreListing().ignore();
+ },
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/modules/base/settings/widgets/theme_mode_tile.dart b/lib/modules/base/settings/widgets/theme_mode_tile.dart
new file mode 100644
index 0000000..c0cb206
--- /dev/null
+++ b/lib/modules/base/settings/widgets/theme_mode_tile.dart
@@ -0,0 +1,22 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/modules/base/settings/settings_controller.dart';
+import 'package:context_watch_signals/context_watch_signals.dart';
+
+class ThemeModeTile extends StatelessWidget {
+ const ThemeModeTile({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final mode = settingsController.instance.themeMode.watch(context);
+ return SwitchListTile(
+ title: Text(context.t.settings.theme_mode),
+ secondary: const Icon(Icons.light_rounded),
+ contentPadding: const EdgeInsets.all(12),
+ value: mode == ThemeMode.dark,
+ onChanged: (value) => settingsController.instance.dispatch(
+ value ? ThemeMode.dark : ThemeMode.light,
+ ),
+ );
+ }
+}
diff --git a/lib/modules/base/text_utils.dart b/lib/modules/base/text_utils.dart
new file mode 100644
index 0000000..3141a4c
--- /dev/null
+++ b/lib/modules/base/text_utils.dart
@@ -0,0 +1,8 @@
+extension StringExtension on String {
+ String capitalize() {
+ if (isEmpty) {
+ return this;
+ }
+ return this[0].toUpperCase() + substring(1);
+ }
+}
diff --git a/lib/modules/base/widgets/app_error_widget.dart b/lib/modules/base/widgets/app_error_widget.dart
new file mode 100644
index 0000000..187a52b
--- /dev/null
+++ b/lib/modules/base/widgets/app_error_widget.dart
@@ -0,0 +1,92 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:go_router/go_router.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+import 'package:universal_html/html.dart' as html;
+
+class AppErrorWidget extends StatelessWidget {
+ final FlutterErrorDetails? errorDetails;
+
+ const AppErrorWidget({super.key, this.errorDetails});
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: Center(
+ child: ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 700),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 12.0),
+ child: Text(
+ context.t.errors.error_view.title,
+ style: context.textTheme.displaySmall!.copyWith(
+ color: context.theme.colorScheme.error,
+ ),
+ ),
+ ),
+ Text(
+ context.t.errors.error_view.content,
+ style: context.textTheme.bodyLarge,
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 24.0),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ OutlinedButton.icon(
+ onPressed: () {
+ if (kIsWeb) {
+ html.window.location.reload();
+ return;
+ }
+ SystemNavigator.pop();
+ },
+ icon: Icon(
+ Icons.close_rounded,
+ color: context.theme.colorScheme.error,
+ ),
+ label: Text(
+ context.t.errors.error_view.exit,
+ style: context.textTheme.bodyMedium?.copyWith(
+ color: context.theme.colorScheme.error,
+ ),
+ ),
+ style: OutlinedButton.styleFrom(
+ side: BorderSide(
+ width: 1,
+ color: context.theme.colorScheme.error,
+ ),
+ ),
+ ),
+ const SizedBox(width: 12),
+ FilledButton.icon(
+ onPressed: () => context.go("/"),
+ icon: const Icon(Icons.home_rounded),
+ label: Text(context.t.errors.error_view.back),
+ style: FilledButton.styleFrom(
+ backgroundColor: context.theme.colorScheme.error,
+ ),
+ ),
+ ],
+ ),
+ ),
+ if (kDebugMode && errorDetails != null) ...[
+ SingleChildScrollView(
+ child: Text(
+ errorDetails!.exceptionAsString(),
+ ),
+ ),
+ ],
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/modules/base/widgets/app_scaffold.dart b/lib/modules/base/widgets/app_scaffold.dart
new file mode 100644
index 0000000..4626472
--- /dev/null
+++ b/lib/modules/base/widgets/app_scaffold.dart
@@ -0,0 +1,81 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
+
+class AppScaffoldContent extends StatelessWidget {
+ final Widget body;
+ final Widget? secondaryBody;
+ final double? bodyRatio;
+ final bool animate;
+
+ const AppScaffoldContent({
+ Key? key,
+ required this.body,
+ this.secondaryBody,
+ this.bodyRatio,
+ this.animate = true,
+ }) : super(key: key ?? const ValueKey('AppScaffold'));
+
+ @override
+ Widget build(BuildContext context) {
+ return AdaptiveLayout(
+ internalAnimations: animate,
+ transitionDuration: animate ? Durations.long1 : Duration.zero,
+ body: SlotLayout(
+ config: {
+ Breakpoints.smallAndUp: SlotLayout.from(
+ key: const Key('Body Small'),
+ builder: (_) => secondaryBody ?? body,
+ ),
+ Breakpoints.largeAndUp: SlotLayout.from(
+ key: const Key('Body Medium'),
+ builder: (_) => body,
+ ),
+ },
+ ),
+ secondaryBody: secondaryBody == null
+ ? null
+ : SlotLayout(
+ config: {
+ Breakpoints.smallAndUp: SlotLayout.from(
+ key: const Key('Body Small'),
+ builder: AdaptiveScaffold.emptyBuilder,
+ ),
+ Breakpoints.largeAndUp: SlotLayout.from(
+ key: const Key('Body Medium'),
+ builder: secondaryBody != null
+ ? (_) => secondaryBody!
+ : AdaptiveScaffold.emptyBuilder,
+ ),
+ },
+ ),
+ bodyRatio: bodyRatio,
+ );
+ }
+}
+
+class AppScaffold extends StatelessWidget {
+ final int currentIndex;
+ final Widget body;
+ final List destinations;
+ final void Function(int) onNavigate;
+
+ const AppScaffold({
+ Key? key,
+ required this.currentIndex,
+ required this.body,
+ required this.destinations,
+ required this.onNavigate,
+ }) : super(key: key ?? const ValueKey('AppScaffoldShell'));
+
+ @override
+ Widget build(BuildContext context) {
+ return AdaptiveScaffold(
+ useDrawer: false,
+ transitionDuration: Durations.long1,
+ selectedIndex: currentIndex,
+ onSelectedIndexChange: onNavigate,
+ destinations: destinations,
+ body: (e) => body,
+ );
+ }
+}
diff --git a/lib/modules/base/widgets/async_image.dart b/lib/modules/base/widgets/async_image.dart
new file mode 100644
index 0000000..a5fe5f2
--- /dev/null
+++ b/lib/modules/base/widgets/async_image.dart
@@ -0,0 +1,97 @@
+import 'dart:math';
+
+import 'package:delayed_display/delayed_display.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+import 'package:skeletonizer/skeletonizer.dart';
+import 'package:universal_io/io.dart';
+
+class AsyncImage extends StatelessWidget {
+ const AsyncImage({
+ super.key,
+ required this.url,
+ this.width,
+ this.height,
+ this.fit,
+ });
+
+ final String url;
+ final double? width;
+ final double? height;
+ final BoxFit? fit;
+
+ @override
+ Widget build(BuildContext context) {
+ if (!kIsWeb && !url.startsWith("http")) {
+ return Image.file(
+ File(url),
+ width: width,
+ height: height,
+ fit: fit,
+ frameBuilder: _frameBuilder(),
+ errorBuilder: _errorWidgetBuilder(),
+ );
+ }
+ return Image.network(
+ url,
+ width: width,
+ height: height,
+ fit: fit,
+ frameBuilder: _frameBuilder(),
+ errorBuilder: _errorWidgetBuilder(),
+ );
+ }
+
+ ImageErrorWidgetBuilder _errorWidgetBuilder() {
+ return (context, error, stackTrace) {
+ return SizedBox(
+ width: width,
+ height: height,
+ child: Center(
+ child: Icon(
+ Icons.image_not_supported_rounded,
+ size: min(128, height ?? width ?? 24),
+ ),
+ ),
+ );
+ };
+ }
+
+ ImageFrameBuilder _frameBuilder() {
+ return (context, child, frame, wasSynchronouslyLoaded) {
+ if (wasSynchronouslyLoaded) {
+ return child;
+ }
+ return DelayedDisplay(
+ delay: Durations.long1,
+ slidingBeginOffset: const Offset(0, 0),
+ child: Skeletonizer(
+ effect: ShimmerEffect.raw(
+ colors: [
+ context.theme.colorScheme.surfaceContainer,
+ context.theme.colorScheme.surfaceContainerHigh,
+ context.theme.colorScheme.surfaceContainerHighest,
+ context.theme.colorScheme.surfaceContainerHigh,
+ context.theme.colorScheme.surfaceContainer,
+ ],
+ duration: Durations.extralong4,
+ lowerBound: -1,
+ upperBound: 1,
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ ),
+ enableSwitchAnimation: true,
+ enabled: frame == null,
+ child: frame == null
+ ? SizedBox(
+ width: width,
+ height: height,
+ child: child,
+ )
+ : child,
+ ),
+ );
+ };
+ }
+}
diff --git a/lib/modules/base/widgets/circle_button.dart b/lib/modules/base/widgets/circle_button.dart
new file mode 100644
index 0000000..c85fbfc
--- /dev/null
+++ b/lib/modules/base/widgets/circle_button.dart
@@ -0,0 +1,41 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+import 'package:vector_graphics/vector_graphics.dart';
+
+class CircleButton extends StatelessWidget {
+ final VectorGraphic svg;
+ final VoidCallback onPressed;
+ final Color? lineColor;
+
+ const CircleButton({
+ super.key,
+ required this.svg,
+ required this.onPressed,
+ this.lineColor,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: (svg.width ?? svg.height ?? 24) * 2.4,
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: lineColor ?? context.theme.colorScheme.primary.withAlpha(150),
+ ),
+ shape: BoxShape.circle,
+ ),
+ child: RawMaterialButton(
+ elevation: 0,
+ onPressed: () {
+ HapticFeedback.mediumImpact();
+ onPressed();
+ },
+ shape: const CircleBorder(),
+ fillColor: Colors.transparent,
+ padding: const EdgeInsets.all(16),
+ child: svg,
+ ),
+ );
+ }
+}
diff --git a/lib/modules/base/widgets/fab_card_button.dart b/lib/modules/base/widgets/fab_card_button.dart
new file mode 100644
index 0000000..f52aab3
--- /dev/null
+++ b/lib/modules/base/widgets/fab_card_button.dart
@@ -0,0 +1,95 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+
+class FabCardButton extends StatefulWidget {
+ final Widget child;
+ final Function(bool)? onExpand;
+
+ const FabCardButton({super.key, required this.child, this.onExpand});
+
+ @override
+ FabCardButtonState createState() => FabCardButtonState();
+}
+
+class FabCardButtonState extends State {
+ bool _isExpanded = false;
+ bool _showContent = false;
+
+ @override
+ Widget build(BuildContext context) {
+ final maxHeight = MediaQuery.of(context).size.height * 0.6;
+ final maxWidth = MediaQuery.of(context).size.width * 0.9;
+ return AnimatedSize(
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ alignment: Alignment.bottomRight,
+ child: ConstrainedBox(
+ constraints: BoxConstraints(maxWidth: maxWidth, maxHeight: maxHeight),
+ child: Card(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ ),
+ margin: EdgeInsets.zero,
+ color: context.theme.colorScheme.primaryContainer,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.end,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (_isExpanded) ...[
+ AnimatedOpacity(
+ opacity: _showContent ? 1.0 : 0.0,
+ duration: const Duration(milliseconds: 200),
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ maxHeight: maxHeight - 48,
+ ),
+ child: SingleChildScrollView(
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: widget.child,
+ ),
+ ),
+ ),
+ ),
+ ],
+ Flexible(
+ child: AnimatedRotation(
+ duration: Durations.long1,
+ turns: _isExpanded ? 0.5 : 0.0,
+ child: Padding(
+ padding: const EdgeInsets.all(4.0),
+ child: IconButton(
+ onPressed: () {
+ setState(() {
+ _isExpanded = !_isExpanded;
+ widget.onExpand?.call(_isExpanded);
+ Future.delayed(const Duration(milliseconds: 300), () {
+ setState(() {
+ _showContent = true;
+ });
+ });
+ });
+ },
+ icon: _isExpanded
+ ? _buildFabContent(Icons.close_rounded)
+ : _buildFabContent(Icons.auto_awesome_rounded),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFabContent(IconData icon) {
+ return Icon(
+ key: const ValueKey("fab_icon"),
+ icon,
+ color: context.theme.colorScheme.onPrimaryContainer,
+ );
+ }
+}
diff --git a/lib/modules/base/widgets/responsive_layout.dart b/lib/modules/base/widgets/responsive_layout.dart
new file mode 100644
index 0000000..525152b
--- /dev/null
+++ b/lib/modules/base/widgets/responsive_layout.dart
@@ -0,0 +1,30 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
+
+class ResponsiveLayout extends StatelessWidget {
+ final Widget small;
+ final Widget? medium;
+ final Widget? large;
+
+ const ResponsiveLayout({
+ super.key,
+ required this.small,
+ this.medium,
+ this.large,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return LayoutBuilder(
+ builder: (context, constraints) {
+ if (Breakpoints.largeAndUp.isActive(context)) {
+ return large ?? medium ?? small;
+ } else if (Breakpoints.mediumAndUp.isActive(context)) {
+ return medium ?? small;
+ } else {
+ return small;
+ }
+ },
+ );
+ }
+}
diff --git a/lib/modules/base/widgets/text_separator.dart b/lib/modules/base/widgets/text_separator.dart
new file mode 100644
index 0000000..6db9a84
--- /dev/null
+++ b/lib/modules/base/widgets/text_separator.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+
+class TextSeparator extends StatelessWidget {
+ final String label;
+ final Color? lineColor;
+ final bool vertical;
+
+ const TextSeparator({
+ super.key,
+ required this.label,
+ this.lineColor,
+ this.vertical = false,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final color = lineColor ?? context.theme.colorScheme.primary.withAlpha(150);
+ return _rowOrColumn(
+ isRow: !vertical,
+ children: [
+ Expanded(
+ child: vertical? VerticalDivider(color: color) : Divider(color: color),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Text(
+ label,
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.labelSmall,
+ ),
+ ),
+ Expanded(
+ child: vertical? VerticalDivider(color: color) : Divider(color: color),
+ ),
+ ],
+ );
+ }
+
+ Widget _rowOrColumn({required bool isRow, required List children}) {
+ if (isRow) {
+ return Row(children: children);
+ } else {
+ return Column(children: children);
+ }
+ }
+}
diff --git a/lib/modules/base/widgets/toast.dart b/lib/modules/base/widgets/toast.dart
new file mode 100644
index 0000000..f3ddf67
--- /dev/null
+++ b/lib/modules/base/widgets/toast.dart
@@ -0,0 +1,61 @@
+import 'package:another_flushbar/flushbar.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+import 'package:universal_io/io.dart';
+
+void showSuccessToast({
+ required BuildContext context,
+ required String title,
+ required String text,
+ Duration duration = const Duration(seconds: 2),
+ Widget? mainButton,
+}) {
+ // This is a hack to prevent the toast from showing during tests
+ if (Platform.environment.containsKey('FLUTTER_TEST')) {
+ return;
+ }
+ Flushbar(
+ flushbarPosition: FlushbarPosition.TOP,
+ title: title,
+ message: text,
+ titleSize: 21,
+ messageSize: 16,
+ duration: duration,
+ leftBarIndicatorColor: context.theme.colorScheme.primary,
+ mainButton: mainButton,
+ titleColor: context.theme.colorScheme.onSurface,
+ messageColor: context.theme.colorScheme.onSurface,
+ backgroundColor: context.theme.colorScheme.surfaceContainer,
+ ).show(context);
+}
+
+void showErrorToast({
+ required BuildContext context,
+ required String title,
+ required String text,
+ Duration duration = const Duration(seconds: 2),
+ String? reason,
+ Widget? mainButton,
+}) {
+ // This is a hack to prevent the toast from showing during tests
+ if (Platform.environment.containsKey('FLUTTER_TEST')) {
+ return;
+ }
+ Flushbar(
+ flushbarPosition: FlushbarPosition.TOP,
+ title: title,
+ message: text,
+ titleSize: 21,
+ messageSize: 16,
+ duration: duration,
+ mainButton: mainButton,
+ backgroundColor: context.theme.colorScheme.surfaceContainer,
+ titleColor: context.theme.colorScheme.onSurface,
+ messageColor: context.theme.colorScheme.onSurface,
+ leftBarIndicatorColor: context.theme.colorScheme.error,
+ icon: Icon(
+ Icons.error,
+ color: context.theme.colorScheme.onSurfaceVariant,
+ ),
+ ).show(context);
+}
diff --git a/lib/modules/onboarding/onboarding_controller.dart b/lib/modules/onboarding/onboarding_controller.dart
new file mode 100644
index 0000000..4b39904
--- /dev/null
+++ b/lib/modules/onboarding/onboarding_controller.dart
@@ -0,0 +1,77 @@
+import 'package:context_watch_signals/context_watch_signals.dart';
+import 'package:go_router/go_router.dart';
+import 'package:lite_ref/lite_ref.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/modules/base/home/home_route.dart';
+import 'package:flutter_template/modules/base/image/app_image.dart';
+import 'package:flutter_template/modules/base/logger.dart';
+import 'package:flutter_template/modules/base/preferences.dart';
+import 'package:flutter_template/modules/onboarding/onboarding_page.dart';
+import 'package:flutter_template/navigation.dart';
+
+final onboardingController = Ref.scoped(
+ // You could instead use RemoteConfig to get different type of onboardings
+ // And change the destination page based on it
+ (context) => OnboardingController(
+ destination: HomeRoute.path,
+ destinationLabel: context.t.onboarding.login,
+ pages: [
+ OnboardingPage(
+ title: context.t.onboarding.hero_title_start,
+ subtitle: context.t.onboarding.hero_text_start,
+ image: const AppImage.static(
+ uri: 'assets/images/icon-512.png',
+ ),
+ ),
+ OnboardingPage(
+ title: context.t.onboarding.hero_title_end,
+ subtitle: context.t.onboarding.hero_text_end,
+ image: const AppImage.rive(
+ uri: 'assets/rive/success.riv',
+ name: "Check",
+ ),
+ ),
+ ],
+ ),
+);
+
+class OnboardingController {
+ final String destination;
+ final String destinationLabel;
+ final List pages;
+
+ final _currentIndex = signal(0);
+ late final currentIndex = _currentIndex.readonly();
+
+ OnboardingController({
+ required this.destination,
+ required this.destinationLabel,
+ required this.pages,
+ });
+
+ Future next() async {
+ if (_currentIndex.value == pages.length - 1) {
+ _onCompleted(destination);
+ } else {
+ _currentIndex.value++;
+ }
+ }
+
+ void skip() {
+ if (currentIndex.value == pages.length - 1) {
+ _onCompleted(destination);
+ } else {
+ _currentIndex.value = pages.length - 1;
+ }
+ }
+
+ Future _onCompleted(String path) async {
+ try {
+ final prefs = await preferences.instance;
+ await prefs.setBool("onboarding", true);
+ } catch (e) {
+ log(e: () => "Error while setting onboarding", error: e);
+ }
+ rootNavigatorKey.currentContext!.go(path);
+ }
+}
diff --git a/lib/modules/onboarding/onboarding_page.dart b/lib/modules/onboarding/onboarding_page.dart
new file mode 100644
index 0000000..45c4fd9
--- /dev/null
+++ b/lib/modules/onboarding/onboarding_page.dart
@@ -0,0 +1,17 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:flutter_template/modules/base/image/app_image.dart';
+
+part 'onboarding_page.freezed.dart';
+part 'onboarding_page.g.dart';
+
+@freezed
+class OnboardingPage with _$OnboardingPage {
+ factory OnboardingPage({
+ required String title,
+ required String subtitle,
+ required AppImage image,
+ }) = _OnboardingPage;
+
+ factory OnboardingPage.fromJson(Map json) =>
+ _$OnboardingPageFromJson(json);
+}
diff --git a/lib/modules/onboarding/onboarding_page.freezed.dart b/lib/modules/onboarding/onboarding_page.freezed.dart
new file mode 100644
index 0000000..b18f39d
--- /dev/null
+++ b/lib/modules/onboarding/onboarding_page.freezed.dart
@@ -0,0 +1,217 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'onboarding_page.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
+
+OnboardingPage _$OnboardingPageFromJson(Map json) {
+ return _OnboardingPage.fromJson(json);
+}
+
+/// @nodoc
+mixin _$OnboardingPage {
+ String get title => throw _privateConstructorUsedError;
+ String get subtitle => throw _privateConstructorUsedError;
+ AppImage get image => throw _privateConstructorUsedError;
+
+ /// Serializes this OnboardingPage to a JSON map.
+ Map toJson() => throw _privateConstructorUsedError;
+
+ /// Create a copy of OnboardingPage
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ $OnboardingPageCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $OnboardingPageCopyWith<$Res> {
+ factory $OnboardingPageCopyWith(
+ OnboardingPage value, $Res Function(OnboardingPage) then) =
+ _$OnboardingPageCopyWithImpl<$Res, OnboardingPage>;
+ @useResult
+ $Res call({String title, String subtitle, AppImage image});
+
+ $AppImageCopyWith<$Res> get image;
+}
+
+/// @nodoc
+class _$OnboardingPageCopyWithImpl<$Res, $Val extends OnboardingPage>
+ implements $OnboardingPageCopyWith<$Res> {
+ _$OnboardingPageCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ /// Create a copy of OnboardingPage
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? title = null,
+ Object? subtitle = null,
+ Object? image = null,
+ }) {
+ return _then(_value.copyWith(
+ title: null == title
+ ? _value.title
+ : title // ignore: cast_nullable_to_non_nullable
+ as String,
+ subtitle: null == subtitle
+ ? _value.subtitle
+ : subtitle // ignore: cast_nullable_to_non_nullable
+ as String,
+ image: null == image
+ ? _value.image
+ : image // ignore: cast_nullable_to_non_nullable
+ as AppImage,
+ ) as $Val);
+ }
+
+ /// Create a copy of OnboardingPage
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @pragma('vm:prefer-inline')
+ $AppImageCopyWith<$Res> get image {
+ return $AppImageCopyWith<$Res>(_value.image, (value) {
+ return _then(_value.copyWith(image: value) as $Val);
+ });
+ }
+}
+
+/// @nodoc
+abstract class _$$OnboardingPageImplCopyWith<$Res>
+ implements $OnboardingPageCopyWith<$Res> {
+ factory _$$OnboardingPageImplCopyWith(_$OnboardingPageImpl value,
+ $Res Function(_$OnboardingPageImpl) then) =
+ __$$OnboardingPageImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({String title, String subtitle, AppImage image});
+
+ @override
+ $AppImageCopyWith<$Res> get image;
+}
+
+/// @nodoc
+class __$$OnboardingPageImplCopyWithImpl<$Res>
+ extends _$OnboardingPageCopyWithImpl<$Res, _$OnboardingPageImpl>
+ implements _$$OnboardingPageImplCopyWith<$Res> {
+ __$$OnboardingPageImplCopyWithImpl(
+ _$OnboardingPageImpl _value, $Res Function(_$OnboardingPageImpl) _then)
+ : super(_value, _then);
+
+ /// Create a copy of OnboardingPage
+ /// with the given fields replaced by the non-null parameter values.
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? title = null,
+ Object? subtitle = null,
+ Object? image = null,
+ }) {
+ return _then(_$OnboardingPageImpl(
+ title: null == title
+ ? _value.title
+ : title // ignore: cast_nullable_to_non_nullable
+ as String,
+ subtitle: null == subtitle
+ ? _value.subtitle
+ : subtitle // ignore: cast_nullable_to_non_nullable
+ as String,
+ image: null == image
+ ? _value.image
+ : image // ignore: cast_nullable_to_non_nullable
+ as AppImage,
+ ));
+ }
+}
+
+/// @nodoc
+@JsonSerializable()
+class _$OnboardingPageImpl implements _OnboardingPage {
+ _$OnboardingPageImpl(
+ {required this.title, required this.subtitle, required this.image});
+
+ factory _$OnboardingPageImpl.fromJson(Map json) =>
+ _$$OnboardingPageImplFromJson(json);
+
+ @override
+ final String title;
+ @override
+ final String subtitle;
+ @override
+ final AppImage image;
+
+ @override
+ String toString() {
+ return 'OnboardingPage(title: $title, subtitle: $subtitle, image: $image)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$OnboardingPageImpl &&
+ (identical(other.title, title) || other.title == title) &&
+ (identical(other.subtitle, subtitle) ||
+ other.subtitle == subtitle) &&
+ (identical(other.image, image) || other.image == image));
+ }
+
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ int get hashCode => Object.hash(runtimeType, title, subtitle, image);
+
+ /// Create a copy of OnboardingPage
+ /// with the given fields replaced by the non-null parameter values.
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$OnboardingPageImplCopyWith<_$OnboardingPageImpl> get copyWith =>
+ __$$OnboardingPageImplCopyWithImpl<_$OnboardingPageImpl>(
+ this, _$identity);
+
+ @override
+ Map toJson() {
+ return _$$OnboardingPageImplToJson(
+ this,
+ );
+ }
+}
+
+abstract class _OnboardingPage implements OnboardingPage {
+ factory _OnboardingPage(
+ {required final String title,
+ required final String subtitle,
+ required final AppImage image}) = _$OnboardingPageImpl;
+
+ factory _OnboardingPage.fromJson(Map json) =
+ _$OnboardingPageImpl.fromJson;
+
+ @override
+ String get title;
+ @override
+ String get subtitle;
+ @override
+ AppImage get image;
+
+ /// Create a copy of OnboardingPage
+ /// with the given fields replaced by the non-null parameter values.
+ @override
+ @JsonKey(includeFromJson: false, includeToJson: false)
+ _$$OnboardingPageImplCopyWith<_$OnboardingPageImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/lib/modules/onboarding/onboarding_page.g.dart b/lib/modules/onboarding/onboarding_page.g.dart
new file mode 100644
index 0000000..df91ecf
--- /dev/null
+++ b/lib/modules/onboarding/onboarding_page.g.dart
@@ -0,0 +1,22 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'onboarding_page.dart';
+
+// **************************************************************************
+// JsonSerializableGenerator
+// **************************************************************************
+
+_$OnboardingPageImpl _$$OnboardingPageImplFromJson(Map json) =>
+ _$OnboardingPageImpl(
+ title: json['title'] as String,
+ subtitle: json['subtitle'] as String,
+ image: AppImage.fromJson(json['image'] as Map),
+ );
+
+Map _$$OnboardingPageImplToJson(
+ _$OnboardingPageImpl instance) =>
+ {
+ 'title': instance.title,
+ 'subtitle': instance.subtitle,
+ 'image': instance.image,
+ };
diff --git a/lib/modules/onboarding/onboarding_route.dart b/lib/modules/onboarding/onboarding_route.dart
new file mode 100644
index 0000000..b000d32
--- /dev/null
+++ b/lib/modules/onboarding/onboarding_route.dart
@@ -0,0 +1,15 @@
+import 'package:go_router/go_router.dart';
+import 'package:flutter_template/navigation.dart';
+import 'package:flutter_template/modules/onboarding/onboarding_view.dart';
+
+class OnboardingRoute implements AppRoute {
+ static const String path = "/onboarding";
+
+ @override
+ GoRoute createRoute() {
+ return GoRoute(
+ path: path,
+ builder: (context, state) => const OnboardingView(),
+ );
+ }
+}
diff --git a/lib/modules/onboarding/onboarding_view.dart b/lib/modules/onboarding/onboarding_view.dart
new file mode 100644
index 0000000..6b9e8a9
--- /dev/null
+++ b/lib/modules/onboarding/onboarding_view.dart
@@ -0,0 +1,194 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_template/i18n/translations.g.dart';
+import 'package:flutter_template/modules/base/widgets/responsive_layout.dart';
+import 'package:flutter_template/modules/onboarding/onboarding_controller.dart';
+import 'package:flutter_template/modules/base/image/app_image_widget.dart';
+import 'package:flutter_template/theme/app_theme.dart';
+import 'package:context_watch_signals/context_watch_signals.dart';
+
+class OnboardingView extends StatefulWidget {
+ const OnboardingView({super.key});
+
+ @override
+ State createState() => _OnboardingViewState();
+}
+
+class _OnboardingViewState extends State {
+ @override
+ Widget build(BuildContext context) {
+ final controller = onboardingController(context);
+ final pages = controller.pages;
+ final index = controller.currentIndex.watch(context);
+ return Scaffold(
+ body: SafeArea(
+ child: ResponsiveLayout(
+ small: Column(
+ children: [
+ const SizedBox(height: 16),
+ Expanded(
+ child: _withAnimation(
+ AppImageWidget(
+ image: pages[index].image,
+ width: 200,
+ height: 200,
+ ),
+ ),
+ ),
+ Expanded(
+ child: AnimatedSwitcher(
+ duration: Durations.extralong1,
+ child: _withAnimation(
+ _buildHeroText(
+ pages[index].title,
+ pages[index].subtitle,
+ ),
+ ),
+ ),
+ ),
+ Align(
+ alignment: Alignment.bottomCenter,
+ child: Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: _navigationBar(index == pages.length - 1),
+ ),
+ ),
+ ],
+ ),
+ large: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: _withAnimation(
+ AppImageWidget(
+ image: pages[index].image,
+ width: 250,
+ height: 200,
+ ),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(24.0),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ _withAnimation(
+ ConstrainedBox(
+ constraints: const BoxConstraints(maxWidth: 480),
+ child: _buildHeroText(
+ pages[index].title,
+ pages[index].subtitle,
+ ),
+ ),
+ ),
+ const SizedBox(height: 24.0),
+ _navigationBar(index == pages.length - 1),
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHeroText(String title, String text) {
+ return Padding(
+ key: ValueKey("hero-text-$title"),
+ padding: const EdgeInsets.all(16.0),
+ child: Column(
+ children: [
+ Text(
+ title,
+ style: context.textTheme.titleLarge,
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 8),
+ Text(
+ text,
+ textAlign: TextAlign.center,
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _navigationBar(bool isEnd) {
+ return Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ TextButton(
+ onPressed: () => onboardingController(context).skip(),
+ child: Text(
+ isEnd ? context.t.onboarding.not_now : context.t.onboarding.skip,
+ style: TextStyle(
+ color: context.theme.colorScheme.primary.withAlpha(180),
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ FloatingActionButton.extended(
+ onPressed: () => onboardingController(context).next(),
+ label: AnimatedSwitcher(
+ duration: Durations.extralong1,
+ child: isEnd
+ ? Row(
+ key: const ValueKey("nav-button"),
+ children: [
+ Text(onboardingController(context).destinationLabel),
+ const Padding(
+ padding: EdgeInsets.only(left: 4.0),
+ child: Icon(
+ Icons.chevron_right_rounded,
+ ),
+ ),
+ ],
+ )
+ : const Icon(
+ key: ValueKey("nav-button"),
+ Icons.chevron_right_rounded,
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _withAnimation(Widget child) {
+ return AnimatedSwitcher(
+ duration: Durations.extralong1,
+ switchInCurve: Curves.easeInOut,
+ switchOutCurve: Curves.fastOutSlowIn,
+ layoutBuilder: (currentChild, previousChildren) => currentChild!,
+ transitionBuilder: (child, animation) {
+ return SlideTransition(
+ position: Tween(
+ begin: const Offset(0, 0.1),
+ end: const Offset(0, 0),
+ ).animate(
+ CurvedAnimation(
+ parent: animation,
+ curve: Curves.easeInOut,
+ ),
+ ),
+ child: FadeTransition(
+ opacity: Tween(
+ begin: 0.2,
+ end: 1,
+ ).animate(
+ CurvedAnimation(
+ parent: animation,
+ curve: Curves.easeInOut,
+ ),
+ ),
+ child: child,
+ ),
+ );
+ },
+ child: child,
+ );
+ }
+}
diff --git a/lib/navigation.dart b/lib/navigation.dart
new file mode 100644
index 0000000..4cee85c
--- /dev/null
+++ b/lib/navigation.dart
@@ -0,0 +1,92 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:flutter_template/modules/base/logger.dart';
+import 'package:flutter_template/modules/base/widgets/app_scaffold.dart';
+
+final GlobalKey rootNavigatorKey = GlobalKey();
+
+abstract class AppRoute {
+ GoRoute createRoute();
+}
+
+abstract class NavigationRoute extends AppRoute {
+ NavigationDestination createDestination(BuildContext context);
+}
+
+GoRouter createRouter({
+ required String initialLocation,
+ required List navigationRoutes,
+ required List otherRoutes,
+ GoRouterRedirect? redirect,
+}) {
+ return GoRouter(
+ debugLogDiagnostics: kDebugMode,
+ navigatorKey: rootNavigatorKey,
+ initialLocation: initialLocation,
+ observers: [
+ if (kDebugMode) DebugRouterObserver(),
+ ],
+ redirect: (context, state) {
+ // Call redirect function if provided otherwise apply root redirect
+ // Useful for web navigation when the user only goes to the domain name
+ return redirect?.call(context, state) ??
+ (state.matchedLocation == "/" ? initialLocation : null);
+ },
+ routes: [
+ createNavigationShell(navigationRoutes),
+ ...otherRoutes.map((e) => e.createRoute()),
+ ],
+ );
+}
+
+StatefulShellRoute createNavigationShell(List routes) {
+ return StatefulShellRoute.indexedStack(
+ builder: (context, state, navigationShell) {
+ return AppScaffold(
+ currentIndex: navigationShell.currentIndex,
+ body: navigationShell,
+ destinations: routes.map((e) => e.createDestination(context)).toList(),
+ onNavigate: (index) {
+ navigationShell.goBranch(
+ index,
+ initialLocation: index == navigationShell.currentIndex,
+ );
+ },
+ );
+ },
+ branches: routes
+ .map((e) => e.createRoute())
+ .map(
+ (e) => StatefulShellBranch(
+ initialLocation: e.path,
+ navigatorKey: GlobalKey(debugLabel: e.path),
+ routes: [e],
+ ),
+ )
+ .toList(),
+ );
+}
+
+class DebugRouterObserver extends NavigatorObserver {
+ @override
+ void didPush(Route route, Route? previousRoute) {
+ log(d: () => 'did push route $route');
+ }
+
+ @override
+ void didPop(Route route, Route? previousRoute) {
+ log(d: () => 'did pop route $route');
+ }
+
+ @override
+ void didRemove(Route route, Route? previousRoute) {
+ log(d: () => 'did remove route $route');
+ super.didRemove(route, previousRoute);
+ }
+
+ @override
+ void didReplace({Route? newRoute, Route? oldRoute}) {
+ log(d: () => 'did replace route $newRoute for $oldRoute');
+ }
+}
diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart
new file mode 100644
index 0000000..fb4612c
--- /dev/null
+++ b/lib/theme/app_theme.dart
@@ -0,0 +1,25 @@
+import 'package:flutter/material.dart';
+
+extension AppThemeExt on BuildContext {
+ TextTheme get textTheme => Theme.of(this).textTheme;
+
+ ThemeData get theme => Theme.of(this);
+}
+
+final lightTheme = ThemeData(
+ useMaterial3: true,
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: const Color(0xFF22BCF4),
+ brightness: Brightness.light,
+ ),
+
+);
+
+final darkTheme = ThemeData(
+ useMaterial3: true,
+ colorScheme: ColorScheme.fromSeed(
+ seedColor: const Color(0xFF146E8D),
+ brightness: Brightness.dark,
+ ),
+
+);
diff --git a/pubspec.lock b/pubspec.lock
new file mode 100644
index 0000000..016315a
--- /dev/null
+++ b/pubspec.lock
@@ -0,0 +1,1411 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ _fe_analyzer_shared:
+ dependency: transitive
+ description:
+ name: _fe_analyzer_shared
+ sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834
+ url: "https://pub.dev"
+ source: hosted
+ version: "72.0.0"
+ _macros:
+ dependency: transitive
+ description: dart
+ source: sdk
+ version: "0.3.2"
+ analyzer:
+ dependency: transitive
+ description:
+ name: analyzer
+ sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.7.0"
+ analyzer_plugin:
+ dependency: transitive
+ description:
+ name: analyzer_plugin
+ sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.3"
+ another_flushbar:
+ dependency: "direct main"
+ description:
+ name: another_flushbar
+ sha256: "19bf9520230ec40b300aaf9dd2a8fefcb277b25ecd1c4838f530566965befc2a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.12.30"
+ ansicolor:
+ dependency: transitive
+ description:
+ name: ansicolor
+ sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.3"
+ archive:
+ dependency: transitive
+ description:
+ name: archive
+ sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.6.1"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.0"
+ async:
+ dependency: "direct main"
+ description:
+ name: async
+ sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.11.0"
+ async_listenable:
+ dependency: transitive
+ description:
+ name: async_listenable
+ sha256: "8dda06e54834a36685e932d3add50a90d30ef0533ff8cde54ed28955357e3d47"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ basic_interfaces:
+ dependency: transitive
+ description:
+ name: basic_interfaces
+ sha256: c37c8c4ddbc594430eb3817a4edfcc9a0cadbc7ea4c51a5b48785e351f1ba479
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.1"
+ build:
+ dependency: transitive
+ description:
+ name: build
+ sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ build_config:
+ dependency: transitive
+ description:
+ name: build_config
+ sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ build_daemon:
+ dependency: transitive
+ description:
+ name: build_daemon
+ sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.2"
+ build_resolvers:
+ dependency: transitive
+ description:
+ name: build_resolvers
+ sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.2"
+ build_runner:
+ dependency: "direct dev"
+ description:
+ name: build_runner
+ sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.12"
+ build_runner_core:
+ dependency: transitive
+ description:
+ name: build_runner_core
+ sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.3.2"
+ built_collection:
+ dependency: transitive
+ description:
+ name: built_collection
+ sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.1.1"
+ built_value:
+ dependency: transitive
+ description:
+ name: built_value
+ sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb
+ url: "https://pub.dev"
+ source: hosted
+ version: "8.9.2"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ charcode:
+ dependency: transitive
+ description:
+ name: charcode
+ sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.1"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.3"
+ ci:
+ dependency: transitive
+ description:
+ name: ci
+ sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.0"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.1"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ code_builder:
+ dependency: transitive
+ description:
+ name: code_builder
+ sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.10.0"
+ collection:
+ dependency: "direct main"
+ description:
+ name: collection
+ sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.18.0"
+ context_watch:
+ dependency: "direct main"
+ description:
+ name: context_watch
+ sha256: f65024ec7a518c79b0e7ace6ca6c0557ba78c25b3b169c8370d5c59dbd3c263e
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.1"
+ context_watch_base:
+ dependency: transitive
+ description:
+ name: context_watch_base
+ sha256: "3ab04862604010796b4ee85c619029168db0c0f8f6304346b7c41666d33e7540"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.0"
+ context_watch_signals:
+ dependency: "direct main"
+ description:
+ name: context_watch_signals
+ sha256: aca8db99f8c547cb29ac05f8a52cc4d03424fce2b05d7163d6617b0a8c8983be
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ convert:
+ dependency: transitive
+ description:
+ name: convert
+ sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.1"
+ cross_file:
+ dependency: transitive
+ description:
+ name: cross_file
+ sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.4+2"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.5"
+ csslib:
+ dependency: transitive
+ description:
+ name: csslib
+ sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
+ csv:
+ dependency: transitive
+ description:
+ name: csv
+ sha256: c6aa2679b2a18cb57652920f674488d89712efaf4d3fdf2e537215b35fc19d6c
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.0"
+ custom_lint:
+ dependency: "direct dev"
+ description:
+ name: custom_lint
+ sha256: "22bd87a362f433ba6aae127a7bac2838645270737f3721b180916d7c5946cb5d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.11"
+ custom_lint_builder:
+ dependency: transitive
+ description:
+ name: custom_lint_builder
+ sha256: "0d48e002438950f9582e574ef806b2bea5719d8d14c0f9f754fbad729bcf3b19"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.14"
+ custom_lint_core:
+ dependency: transitive
+ description:
+ name: custom_lint_core
+ sha256: "2952837953022de610dacb464f045594854ced6506ac7f76af28d4a6490e189b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.14"
+ dart_style:
+ dependency: transitive
+ description:
+ name: dart_style
+ sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.6"
+ delayed_display:
+ dependency: "direct main"
+ description:
+ name: delayed_display
+ sha256: "8d722bb730071b872cef2bc0b07ef20549ebd269087a16ed1c5696896246c296"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ dio:
+ dependency: "direct main"
+ description:
+ name: dio
+ sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.6.0"
+ dio_cache_interceptor:
+ dependency: "direct main"
+ description:
+ name: dio_cache_interceptor
+ sha256: fb7905c0d12075d8786a6b63bffd64ae062d053f682cfaf28d145a2686507308
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.5.0"
+ dio_web_adapter:
+ dependency: transitive
+ description:
+ name: dio_web_adapter
+ sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ envied:
+ dependency: "direct main"
+ description:
+ name: envied
+ sha256: bbff9c76120e4dc5e2e36a46690cf0a26feb65e7765633f4e8d916bcd173a450
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.4+1"
+ envied_generator:
+ dependency: "direct dev"
+ description:
+ name: envied_generator
+ sha256: "517b70de08d13dcd40e97b4e5347e216a0b1c75c99e704f3c85c0474a392d14a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.4+1"
+ equatable:
+ dependency: transitive
+ description:
+ name: equatable
+ sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.5"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.1"
+ ffi:
+ dependency: transitive
+ description:
+ name: ffi
+ sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.3"
+ file:
+ dependency: transitive
+ description:
+ name: file
+ sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
+ fixnum:
+ dependency: transitive
+ description:
+ name: fixnum
+ sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_adaptive_scaffold:
+ dependency: "direct main"
+ description:
+ name: flutter_adaptive_scaffold
+ sha256: fe16ddead74e728c88aa33e3d0cef611a556b27194639f9e9ec4a0e32400d87e
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.3"
+ flutter_colorpicker:
+ dependency: "direct main"
+ description:
+ name: flutter_colorpicker
+ sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ flutter_driver:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_launcher_icons:
+ dependency: "direct dev"
+ description:
+ name: flutter_launcher_icons
+ sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.13.1"
+ flutter_lints:
+ dependency: "direct dev"
+ description:
+ name: flutter_lints
+ sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0"
+ flutter_localizations:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_native_splash:
+ dependency: "direct dev"
+ description:
+ name: flutter_native_splash
+ sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_web_plugins:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ freezed:
+ dependency: "direct dev"
+ description:
+ name: freezed
+ sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.7"
+ freezed_annotation:
+ dependency: "direct main"
+ description:
+ name: freezed_annotation
+ sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.4"
+ frontend_server_client:
+ dependency: transitive
+ description:
+ name: frontend_server_client
+ sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0"
+ fuchsia_remote_debug_protocol:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ glob:
+ dependency: transitive
+ description:
+ name: glob
+ sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ go_router:
+ dependency: "direct main"
+ description:
+ name: go_router
+ sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
+ url: "https://pub.dev"
+ source: hosted
+ version: "14.2.7"
+ graphs:
+ dependency: transitive
+ description:
+ name: graphs
+ sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ hotreloader:
+ dependency: transitive
+ description:
+ name: hotreloader
+ sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.0"
+ html:
+ dependency: transitive
+ description:
+ name: html
+ sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.15.4"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.2"
+ http_multi_server:
+ dependency: transitive
+ description:
+ name: http_multi_server
+ sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.1"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.2"
+ image:
+ dependency: transitive
+ description:
+ name: image
+ sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.0"
+ in_app_review:
+ dependency: "direct main"
+ description:
+ name: in_app_review
+ sha256: "99869244d09adc76af16bf8fd731dd13cef58ecafd5917847589c49f378cbb30"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.9"
+ in_app_review_platform_interface:
+ dependency: transitive
+ description:
+ name: in_app_review_platform_interface
+ sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.5"
+ integration_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ intl:
+ dependency: transitive
+ description:
+ name: intl
+ sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.19.0"
+ io:
+ dependency: transitive
+ description:
+ name: io
+ sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
+ js:
+ dependency: transitive
+ description:
+ name: js
+ sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.1"
+ json2yaml:
+ dependency: transitive
+ description:
+ name: json2yaml
+ sha256: da94630fbc56079426fdd167ae58373286f603371075b69bf46d848d63ba3e51
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
+ json_annotation:
+ dependency: "direct main"
+ description:
+ name: json_annotation
+ sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.9.0"
+ json_serializable:
+ dependency: "direct dev"
+ description:
+ name: json_serializable
+ sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.8.0"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.0.5"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.5"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
+ lints:
+ dependency: transitive
+ description:
+ name: lints
+ sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0"
+ lite_ref:
+ dependency: "direct main"
+ description:
+ name: lite_ref
+ sha256: "2519ee5c20257ed8b8eec92c967b8ae2a3361895e9e5dd268569c681803df41b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.1"
+ lite_ref_core:
+ dependency: transitive
+ description:
+ name: lite_ref_core
+ sha256: b902af6b90736d6fb7a2c171af37d15f4894a5880e9eb55ffb3fee5f01385849
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.1"
+ logger:
+ dependency: "direct main"
+ description:
+ name: logger
+ sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.0"
+ logging:
+ dependency: transitive
+ description:
+ name: logging
+ sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.0"
+ macros:
+ dependency: transitive
+ description:
+ name: macros
+ sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.2-main.4"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.12.16+1"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.1"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.15.0"
+ mime:
+ dependency: transitive
+ description:
+ name: mime
+ sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.6"
+ package_config:
+ dependency: transitive
+ description:
+ name: package_config
+ sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.9.0"
+ path_parsing:
+ dependency: transitive
+ description:
+ name: path_parsing
+ sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
+ path_provider:
+ dependency: transitive
+ description:
+ name: path_provider
+ sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ path_provider_android:
+ dependency: transitive
+ description:
+ name: path_provider_android
+ sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.10"
+ path_provider_foundation:
+ dependency: transitive
+ description:
+ name: path_provider_foundation
+ sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.0"
+ path_provider_linux:
+ dependency: transitive
+ description:
+ name: path_provider_linux
+ sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.1"
+ path_provider_platform_interface:
+ dependency: transitive
+ description:
+ name: path_provider_platform_interface
+ sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ path_provider_windows:
+ dependency: transitive
+ description:
+ name: path_provider_windows
+ sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.0"
+ permission_handler:
+ dependency: "direct main"
+ description:
+ name: permission_handler
+ sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "11.3.1"
+ permission_handler_android:
+ dependency: transitive
+ description:
+ name: permission_handler_android
+ sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa"
+ url: "https://pub.dev"
+ source: hosted
+ version: "12.0.12"
+ permission_handler_apple:
+ dependency: transitive
+ description:
+ name: permission_handler_apple
+ sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.4.5"
+ permission_handler_html:
+ dependency: transitive
+ description:
+ name: permission_handler_html
+ sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.3+2"
+ permission_handler_platform_interface:
+ dependency: transitive
+ description:
+ name: permission_handler_platform_interface
+ sha256: fe0ffe274d665be8e34f9c59705441a7d248edebbe5d9e3ec2665f88b79358ea
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.2"
+ permission_handler_windows:
+ dependency: transitive
+ description:
+ name: permission_handler_windows
+ sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.2"
+ platform:
+ dependency: transitive
+ description:
+ name: platform
+ sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.5"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.8"
+ pool:
+ dependency: transitive
+ description:
+ name: pool
+ sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.5.1"
+ process:
+ dependency: transitive
+ description:
+ name: process
+ sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.2"
+ pub_semver:
+ dependency: transitive
+ description:
+ name: pub_semver
+ sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ pubspec_parse:
+ dependency: transitive
+ description:
+ name: pubspec_parse
+ sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ recase:
+ dependency: transitive
+ description:
+ name: recase
+ sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.1.0"
+ rive:
+ dependency: "direct main"
+ description:
+ name: rive
+ sha256: daa5394a7d064b4997b39e9afa02f6882c479c38b19fa0dd60f052b99c105400
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.13.13"
+ rive_common:
+ dependency: transitive
+ description:
+ name: rive_common
+ sha256: c7bf0781b1621629361579c300ac2f8aa1a238227a242202a596e82becc244d7
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.11"
+ rxdart:
+ dependency: transitive
+ description:
+ name: rxdart
+ sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.27.7"
+ share_plus:
+ dependency: "direct main"
+ description:
+ name: share_plus
+ sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.0.0"
+ share_plus_platform_interface:
+ dependency: transitive
+ description:
+ name: share_plus_platform_interface
+ sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0"
+ shared_preferences:
+ dependency: "direct main"
+ description:
+ name: shared_preferences
+ sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ shared_preferences_android:
+ dependency: transitive
+ description:
+ name: shared_preferences_android
+ sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ shared_preferences_foundation:
+ dependency: transitive
+ description:
+ name: shared_preferences_foundation
+ sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.2"
+ shared_preferences_linux:
+ dependency: transitive
+ description:
+ name: shared_preferences_linux
+ sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ shared_preferences_platform_interface:
+ dependency: transitive
+ description:
+ name: shared_preferences_platform_interface
+ sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ shared_preferences_web:
+ dependency: transitive
+ description:
+ name: shared_preferences_web
+ sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.2"
+ shared_preferences_windows:
+ dependency: transitive
+ description:
+ name: shared_preferences_windows
+ sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.1"
+ shelf:
+ dependency: transitive
+ description:
+ name: shelf
+ sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.1"
+ shelf_web_socket:
+ dependency: transitive
+ description:
+ name: shelf_web_socket
+ sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.0"
+ signals_core:
+ dependency: transitive
+ description:
+ name: signals_core
+ sha256: "534843854cf805f0a9f58e47ab5c105fc8ef7f61c2f9a4cce6617adb503bc5b0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.4.0"
+ signals_flutter:
+ dependency: transitive
+ description:
+ name: signals_flutter
+ sha256: d6adb1b4ece00191d1448187c2393ca82779fd59c70aa11c8d9af937286f06c7
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.4.0"
+ signals_lint:
+ dependency: "direct dev"
+ description:
+ name: signals_lint
+ sha256: d55e1aa9a9b6d41234a53f9ed8b14f87d32b155711dccebb2e28d068fdee965d
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.0"
+ skeletonizer:
+ dependency: "direct main"
+ description:
+ name: skeletonizer
+ sha256: "3b202e4fa9c49b017d368fb0e570d4952bcd19972b67b2face071bdd68abbfae"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.2"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.99"
+ slang:
+ dependency: "direct main"
+ description:
+ name: slang
+ sha256: a2f704508bf9f209b71c881347bd27de45309651e9bd63570e4dd6ed2a77fbd2
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.31.2"
+ slang_flutter:
+ dependency: "direct main"
+ description:
+ name: slang_flutter
+ sha256: f8400292be49c11697d94af58d7f7d054c91af759f41ffe71e4e5413871ffc62
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.31.0"
+ slang_gpt:
+ dependency: "direct dev"
+ description:
+ name: slang_gpt
+ sha256: "98e6f32f518c038c18fdd6b53923966a97f3a358487bfbe1a6dead4e8c1a3a39"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.10.3"
+ source_gen:
+ dependency: transitive
+ description:
+ name: source_gen
+ sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.5.0"
+ source_helper:
+ dependency: transitive
+ description:
+ name: source_helper
+ sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.4"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.10.0"
+ sprintf:
+ dependency: transitive
+ description:
+ name: sprintf
+ sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.11.1"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ stream_transform:
+ dependency: transitive
+ description:
+ name: stream_transform
+ sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.0"
+ sync_http:
+ dependency: transitive
+ description:
+ name: sync_http
+ sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.1"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.2"
+ timing:
+ dependency: transitive
+ description:
+ name: timing
+ sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.1"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.2"
+ universal_html:
+ dependency: "direct main"
+ description:
+ name: universal_html
+ sha256: "56536254004e24d9d8cfdb7dbbf09b74cf8df96729f38a2f5c238163e3d58971"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.4"
+ universal_io:
+ dependency: "direct main"
+ description:
+ name: universal_io
+ sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.2"
+ url_launcher:
+ dependency: "direct main"
+ description:
+ name: url_launcher
+ sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.0"
+ url_launcher_android:
+ dependency: transitive
+ description:
+ name: url_launcher_android
+ sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.10"
+ url_launcher_ios:
+ dependency: transitive
+ description:
+ name: url_launcher_ios
+ sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.3.1"
+ url_launcher_linux:
+ dependency: transitive
+ description:
+ name: url_launcher_linux
+ sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.0"
+ url_launcher_macos:
+ dependency: transitive
+ description:
+ name: url_launcher_macos
+ sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.0"
+ url_launcher_platform_interface:
+ dependency: transitive
+ description:
+ name: url_launcher_platform_interface
+ sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.2"
+ url_launcher_web:
+ dependency: transitive
+ description:
+ name: url_launcher_web
+ sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.3"
+ url_launcher_windows:
+ dependency: transitive
+ description:
+ name: url_launcher_windows
+ sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.2"
+ uuid:
+ dependency: transitive
+ description:
+ name: uuid
+ sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.4.2"
+ vector_graphics:
+ dependency: "direct main"
+ description:
+ name: vector_graphics
+ sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.11+1"
+ vector_graphics_codec:
+ dependency: transitive
+ description:
+ name: vector_graphics_codec
+ sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.11+1"
+ vector_graphics_compiler:
+ dependency: "direct dev"
+ description:
+ name: vector_graphics_compiler
+ sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.11+1"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
+ url: "https://pub.dev"
+ source: hosted
+ version: "14.2.4"
+ watcher:
+ dependency: transitive
+ description:
+ name: watcher
+ sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.5.1"
+ web_socket:
+ dependency: transitive
+ description:
+ name: web_socket
+ sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.6"
+ web_socket_channel:
+ dependency: transitive
+ description:
+ name: web_socket_channel
+ sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.1"
+ webdriver:
+ dependency: transitive
+ description:
+ name: webdriver
+ sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.3"
+ win32:
+ dependency: transitive
+ description:
+ name: win32
+ sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.5.4"
+ xdg_directories:
+ dependency: transitive
+ description:
+ name: xdg_directories
+ sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.4"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.5.0"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.2"
+sdks:
+ dart: ">=3.5.0 <4.0.0"
+ flutter: ">=3.24.0"
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..6daf87e
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,101 @@
+name: flutter_template
+description: "An open source project to show Flutter base template of ShipFlutter project. It shows best practices and base structure"
+
+# Prevent accidental publishing to pub.dev.
+publish_to: "none"
+
+version: 1.1.0+1
+
+environment:
+ sdk: ">=3.3.3 <4.0.0"
+
+dependencies:
+ # Core dependencies
+ another_flushbar: ^1.12.30
+ async: ^2.11.0
+ collection: ^1.18.0
+ context_watch: ^5.0.0
+ context_watch_signals: ^2.0.0
+ delayed_display: ^2.0.0
+ dio: ^5.6.0
+ dio_cache_interceptor: ^3.5.0
+ envied: ^0.5.4
+ flutter:
+ sdk: flutter
+ flutter_adaptive_scaffold: ^0.2.2
+ flutter_colorpicker: ^1.1.0
+ flutter_localizations:
+ sdk: flutter
+ flutter_web_plugins:
+ sdk: flutter
+ freezed_annotation: ^2.4.4
+ go_router: ^14.2.6
+ in_app_review: ^2.0.9
+ json_annotation: ^4.9.0
+ lite_ref: ^0.8.1
+ logger: ^2.4.0
+ rive: ^0.13.12
+ share_plus: ^9.0.0
+ shared_preferences: ^2.3.2
+ skeletonizer: ^1.4.2
+ slang: ^3.31.1
+ slang_flutter: ^3.31.0
+ universal_html: ^2.2.4
+ universal_io: ^2.2.2
+ url_launcher: ^6.3.0
+ vector_graphics: ^1.1.11
+ permission_handler: ^11.3.1
+ # Modules dependencies
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ flutter_launcher_icons: "^0.13.1"
+ # If you want to control the initialization of your app move this to dependencies
+ # https://pub.dev/packages/flutter_native_splash#3-set-up-app-initialization-optional
+ flutter_native_splash: ^2.4.1
+ flutter_lints: ^4.0.0
+ build_runner: ^2.4.12
+ freezed: ^2.5.7
+ json_serializable: ^6.8.0
+ signals_lint: ^5.0.0
+ custom_lint: ^0.5.11
+ slang_gpt: ^0.10.3
+ vector_graphics_compiler: ^1.1.11
+ envied_generator: ^0.5.4
+ integration_test:
+ sdk: flutter
+
+flutter:
+ uses-material-design: true
+
+ assets:
+ # Add assets from the images directory to the application.
+ - assets/images/
+ - assets/rive/
+ # Add svg to the svg folder to apply the transformation to all of them.
+ # See https://docs.flutter.dev/ui/assets/asset-transformation
+ - path: assets/svg/
+ transformers:
+ - package: vector_graphics_compiler
+
+# Whenever you change and icon, run `flutter pub run flutter_launcher_icons`
+flutter_launcher_icons:
+ android: true
+ ios: true
+ image_path: "assets/images/icon-512-maskable.png"
+ min_sdk_android: 21
+ web:
+ generate: true
+ image_path: "assets/images/icon-512.png"
+
+# Whenever you change the theme, run `dart run flutter_native_splash:create`
+# Read docs for more information: https://pub.dev/packages/flutter_native_splash
+flutter_native_splash:
+ color: "#00B4D8"
+ android_12:
+ color: "#00B4D8"
+ image: "assets/images/icon-512.png"
+ # The web splash is done manually in the index.html
+ web: false
+ fullscreen: true
diff --git a/setup.sh b/setup.sh
new file mode 100644
index 0000000..ac36e27
--- /dev/null
+++ b/setup.sh
@@ -0,0 +1,6 @@
+#!/bin/bash
+set -e
+
+flutter pub get
+dart run build_runner build --delete-conflicting-outputs
+dart run slang build
\ No newline at end of file
diff --git a/slang.yaml b/slang.yaml
new file mode 100644
index 0000000..f943b6f
--- /dev/null
+++ b/slang.yaml
@@ -0,0 +1,13 @@
+base_locale: en
+fallback_strategy: base_locale
+input_directory: lib/i18n
+input_file_pattern: .i18n.json
+output_directory: lib/i18n
+output_file_name: translations.g.dart
+output_format: single_file
+translation_overrides: true
+
+gpt:
+ model: gpt-4
+ description: |
+ An open source project to show Flutter base template of ShipFlutter project. It shows best practices and base structure
diff --git a/test/unit_test.dart b/test/unit_test.dart
new file mode 100644
index 0000000..e100eb0
--- /dev/null
+++ b/test/unit_test.dart
@@ -0,0 +1,15 @@
+// This is an example unit test.
+//
+// A unit test tests a single function, method, or class. To learn more about
+// writing unit tests, visit
+// https://flutter.dev/docs/cookbook/testing/unit/introduction
+
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ group('Plus Operator', () {
+ test('should add two numbers together', () {
+ expect(1 + 1, 2);
+ });
+ });
+}
diff --git a/test/widget_test.dart b/test/widget_test.dart
new file mode 100644
index 0000000..13ad8b9
--- /dev/null
+++ b/test/widget_test.dart
@@ -0,0 +1,31 @@
+// This is an example Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+//
+// Visit https://flutter.dev/docs/cookbook/testing/widget/introduction for
+// more information about Widget testing.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+ group('MyWidget', () {
+ testWidgets('should display a string of text', (WidgetTester tester) async {
+ // Define a Widget
+ const myWidget = MaterialApp(
+ home: Scaffold(
+ body: Text('Hello'),
+ ),
+ );
+
+ // Build myWidget and trigger a frame.
+ await tester.pumpWidget(myWidget);
+
+ // Verify myWidget shows some text
+ expect(find.byType(Text), findsOneWidget);
+ });
+ });
+}
diff --git a/web/favicon.png b/web/favicon.png
new file mode 100644
index 0000000..1383ea7
Binary files /dev/null and b/web/favicon.png differ
diff --git a/web/firebase-messaging-sw.js b/web/firebase-messaging-sw.js
new file mode 100644
index 0000000..31d926b
--- /dev/null
+++ b/web/firebase-messaging-sw.js
@@ -0,0 +1,30 @@
+importScripts("https://www.gstatic.com/firebasejs/10.12.3/firebase-app-compat.js");
+importScripts("https://www.gstatic.com/firebasejs/10.12.3/firebase-messaging-compat.js");
+
+firebase.initializeApp({
+ apiKey: 'AIzaSyDA3gME8m1E1dJrutfz0kQBxbVQ6UITIw8',
+ appId: '1:349377624153:web:53bcccf302664e9ceb1948',
+ messagingSenderId: '349377624153',
+ projectId: 'Flutter-template',
+ authDomain: 'Flutter-template.firebaseapp.com',
+ storageBucket: 'Flutter-template.appspot.com',
+ measurementId: 'G-WG5KQTSGW9',
+});
+// TODO do we need the real ids?
+// firebase.initializeApp({
+// apiKey: "...",
+// authDomain: "...",
+// databaseURL: "...",
+// projectId: "...",
+// storageBucket: "...",
+// messagingSenderId: "...",
+// appId: "...",
+// });
+
+const messaging = firebase.messaging();
+
+// Optional:
+messaging.onBackgroundMessage((message) => {
+ console.log("onBackgroundMessage", message);
+ // TODO either show a notification or pass it to the app somehow?
+});
diff --git a/web/flutter_bootstrap.js b/web/flutter_bootstrap.js
new file mode 100644
index 0000000..402ea42
--- /dev/null
+++ b/web/flutter_bootstrap.js
@@ -0,0 +1,22 @@
+{{flutter_js}}
+{{flutter_build_config}}
+
+const loadingDiv = document.createElement("div");
+loadingDiv.id = "loading";
+document.body.appendChild(loadingDiv);
+const icon = document.createElement("img");
+icon.src = "icons/Icon-192.png";
+loadingDiv.appendChild(icon);
+
+// Custom bootstrap script to create a loading animation
+_flutter.loader.load({
+ onEntrypointLoaded: async function (engineInitializer) {
+ const appRunner = await engineInitializer.initializeEngine();
+
+ // Remove the loading spinner when the app runner is ready
+ if (document.body.contains(loadingDiv)) {
+ document.body.removeChild(loadingDiv);
+ }
+ await appRunner.runApp();
+ },
+});
diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png
new file mode 100644
index 0000000..5abe692
Binary files /dev/null and b/web/icons/Icon-192.png differ
diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png
new file mode 100644
index 0000000..69af94a
Binary files /dev/null and b/web/icons/Icon-512.png differ
diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png
new file mode 100644
index 0000000..5abe692
Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ
diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png
new file mode 100644
index 0000000..69af94a
Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ
diff --git a/web/icons/apple-touch-icon.png b/web/icons/apple-touch-icon.png
new file mode 100644
index 0000000..4352940
Binary files /dev/null and b/web/icons/apple-touch-icon.png differ
diff --git a/web/icons/favicon.ico b/web/icons/favicon.ico
new file mode 100644
index 0000000..f269e8c
Binary files /dev/null and b/web/icons/favicon.ico differ
diff --git a/web/icons/favicon.svg b/web/icons/favicon.svg
new file mode 100644
index 0000000..5d90b0e
--- /dev/null
+++ b/web/icons/favicon.svg
@@ -0,0 +1,57 @@
+
+
+
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..110be5b
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Flutter template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/manifest.json b/web/manifest.json
new file mode 100644
index 0000000..ff087da
--- /dev/null
+++ b/web/manifest.json
@@ -0,0 +1,46 @@
+{
+ "name": "ShipFlutter Builder",
+ "short_name": "ShipFlutter",
+ "start_url": ".",
+ "display": "standalone",
+ "background_color": "#000000",
+ "theme_color": "#00B4D8",
+ "description": "Build Flutter apps Fast with ShipFlutter.com",
+ "orientation": "portrait-primary",
+ "prefer_related_applications": false,
+ "related_applications": [
+ {
+ "platform": "itunes",
+ "url": "TODO: app store link"
+ },
+ {
+ "id": "com.shipflutter.android",
+ "platform": "play",
+ "url": "https://play.google.com/store/apps/details?id=com.shipflutter.android"
+ }
+ ],
+ "icons": [
+ {
+ "src": "icons/Icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png"
+ },
+ {
+ "src": "icons/Icon-maskable-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "icons/Icon-maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ]
+}
\ No newline at end of file