瀏覽代碼

Transfer repository from Gitlab

Tran, Alex 3 年之前
父節點
當前提交
568cc243f0
共有 100 個文件被更改,包括 3716 次插入0 次删除
  1. 46 0
      mobile/.gitignore
  2. 10 0
      mobile/.metadata
  3. 16 0
      mobile/README.md
  4. 29 0
      mobile/analysis_options.yaml
  5. 13 0
      mobile/android/.gitignore
  6. 68 0
      mobile/android/app/build.gradle
  7. 7 0
      mobile/android/app/src/debug/AndroidManifest.xml
  8. 38 0
      mobile/android/app/src/main/AndroidManifest.xml
  9. 6 0
      mobile/android/app/src/main/kotlin/com/example/immich_mobile/MainActivity.kt
  10. 12 0
      mobile/android/app/src/main/res/drawable-v21/launch_background.xml
  11. 12 0
      mobile/android/app/src/main/res/drawable/launch_background.xml
  12. 二進制
      mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  13. 二進制
      mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  14. 二進制
      mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  15. 二進制
      mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  16. 二進制
      mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  17. 18 0
      mobile/android/app/src/main/res/values-night/styles.xml
  18. 18 0
      mobile/android/app/src/main/res/values/styles.xml
  19. 7 0
      mobile/android/app/src/profile/AndroidManifest.xml
  20. 31 0
      mobile/android/build.gradle
  21. 3 0
      mobile/android/gradle.properties
  22. 6 0
      mobile/android/gradle/wrapper/gradle-wrapper.properties
  23. 11 0
      mobile/android/settings.gradle
  24. 二進制
      mobile/assets/immich-logo-no-outline.png
  25. 二進制
      mobile/assets/immich-logo.png
  26. 98 0
      mobile/assets/immich-logo.svg
  27. 34 0
      mobile/ios/.gitignore
  28. 26 0
      mobile/ios/Flutter/AppFrameworkInfo.plist
  29. 2 0
      mobile/ios/Flutter/Debug.xcconfig
  30. 2 0
      mobile/ios/Flutter/Release.xcconfig
  31. 41 0
      mobile/ios/Podfile
  32. 50 0
      mobile/ios/Podfile.lock
  33. 551 0
      mobile/ios/Runner.xcodeproj/project.pbxproj
  34. 7 0
      mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  35. 8 0
      mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  36. 8 0
      mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  37. 87 0
      mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  38. 10 0
      mobile/ios/Runner.xcworkspace/contents.xcworkspacedata
  39. 8 0
      mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  40. 8 0
      mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  41. 13 0
      mobile/ios/Runner/AppDelegate.swift
  42. 122 0
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  43. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  44. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  45. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  46. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
  47. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  48. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
  49. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
  50. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  51. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
  52. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
  53. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
  54. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
  55. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
  56. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
  57. 二進制
      mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
  58. 23 0
      mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
  59. 二進制
      mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
  60. 二進制
      mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
  61. 二進制
      mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
  62. 5 0
      mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
  63. 37 0
      mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard
  64. 26 0
      mobile/ios/Runner/Base.lproj/Main.storyboard
  65. 49 0
      mobile/ios/Runner/Info.plist
  66. 1 0
      mobile/ios/Runner/Runner-Bridging-Header.h
  67. 11 0
      mobile/lib/constants/hive_box.dart
  68. 92 0
      mobile/lib/main.dart
  69. 0 0
      mobile/lib/module_template/models/store_model_here.txt
  70. 0 0
      mobile/lib/module_template/providers/store_providers_here.txt
  71. 0 0
      mobile/lib/module_template/services/store_services_here.txt
  72. 0 0
      mobile/lib/module_template/ui/store_ui_here.txt
  73. 0 0
      mobile/lib/module_template/views/store_views_here.txt
  74. 113 0
      mobile/lib/modules/home/models/get_all_asset_respose.model.dart
  75. 60 0
      mobile/lib/modules/home/providers/asset.provider.dart
  76. 38 0
      mobile/lib/modules/home/services/asset.service.dart
  77. 26 0
      mobile/lib/modules/home/ui/image_grid.dart
  78. 105 0
      mobile/lib/modules/home/ui/immich_sliver_appbar.dart
  79. 72 0
      mobile/lib/modules/home/ui/profile_drawer.dart
  80. 52 0
      mobile/lib/modules/home/ui/thumbnail_image.dart
  81. 165 0
      mobile/lib/modules/home/views/home_page.dart
  82. 93 0
      mobile/lib/modules/login/models/authentication_state.model.dart
  83. 61 0
      mobile/lib/modules/login/models/login_response.model.dart
  84. 127 0
      mobile/lib/modules/login/providers/authentication.provider.dart
  85. 124 0
      mobile/lib/modules/login/ui/login_form.dart
  86. 16 0
      mobile/lib/modules/login/views/login_page.dart
  87. 22 0
      mobile/lib/routing/auth_guard.dart
  88. 22 0
      mobile/lib/routing/router.dart
  89. 122 0
      mobile/lib/routing/router.gr.dart
  90. 77 0
      mobile/lib/shared/models/backup_state.model.dart
  91. 100 0
      mobile/lib/shared/models/device_info.model.dart
  92. 11 0
      mobile/lib/shared/models/image_viewer_page_data.model.dart
  93. 131 0
      mobile/lib/shared/models/immich_asset.model.dart
  94. 98 0
      mobile/lib/shared/models/server_info.model.dart
  95. 13 0
      mobile/lib/shared/providers/app_state.provider.dart
  96. 137 0
      mobile/lib/shared/providers/backup.provider.dart
  97. 124 0
      mobile/lib/shared/services/backup.service.dart
  98. 30 0
      mobile/lib/shared/services/device_info.service.dart
  99. 18 0
      mobile/lib/shared/services/local_storage.service.dart
  100. 89 0
      mobile/lib/shared/services/network.service.dart

+ 46 - 0
mobile/.gitignore

@@ -0,0 +1,46 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# 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

+ 10 - 0
mobile/.metadata

@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: 77d935af4db863f6abd0b9c31c7e6df2a13de57b
+  channel: stable
+
+project_type: app

+ 16 - 0
mobile/README.md

@@ -0,0 +1,16 @@
+# immich_mobile
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+Few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
+
+For help getting started with Flutter, view our
+[online documentation](https://flutter.dev/docs), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.

+ 29 - 0
mobile/analysis_options.yaml

@@ -0,0 +1,29 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+  # The lint rules applied to this project can be customized in the
+  # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+  # included above or to enable additional rules. A list of all available lints
+  # and their documentation is published at
+  # https://dart-lang.github.io/linter/lints/index.html.
+  #
+  # Instead of disabling a lint rule for the entire project in the
+  # section below, it can also be suppressed for a single line of code
+  # or a specific dart file by using the `// ignore: name_of_lint` and
+  # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+  # producing the lint.
+  rules:
+    # avoid_print: false  # Uncomment to disable the `avoid_print` rule
+    # prefer_single_quotes: true  # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options

+ 13 - 0
mobile/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

+ 68 - 0
mobile/android/app/build.gradle

@@ -0,0 +1,68 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withReader('UTF-8') { reader ->
+        localProperties.load(reader)
+    }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+    flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+    flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+    compileSdkVersion flutter.compileSdkVersion
+
+    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.example.immich_mobile"
+        minSdkVersion flutter.minSdkVersion
+        targetSdkVersion flutter.targetSdkVersion
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+    }
+
+    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
+        }
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}

+ 7 - 0
mobile/android/app/src/debug/AndroidManifest.xml

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

+ 38 - 0
mobile/android/app/src/main/AndroidManifest.xml

@@ -0,0 +1,38 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.example.immich_mobile">
+    <application
+            android:label="immich_mobile"
+            android:name="${applicationName}"
+            android:icon="@mipmap/ic_launcher">
+        <activity
+                android:name=".MainActivity"
+                android:exported="true"
+                android:launchMode="singleTop"
+                android:theme="@style/LaunchTheme"
+                android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
+                android:hardwareAccelerated="true"
+                android:windowSoftInputMode="adjustResize">
+            <!-- Specifies an Android theme to apply to this Activity as soon as
+                 the Android process has started. This theme is visible to the user
+                 while the Flutter UI initializes. After that, this theme continues
+                 to determine the Window background behind the Flutter UI. -->
+            <meta-data
+                    android:name="io.flutter.embedding.android.NormalTheme"
+                    android:resource="@style/NormalTheme"
+            />
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN"/>
+                <category android:name="android.intent.category.LAUNCHER"/>
+            </intent-filter>
+             
+        </activity>
+        <!-- Don't delete the meta-data below.
+             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
+        <meta-data
+                android:name="flutterEmbedding"
+                android:value="2"/>
+
+       
+    </application>
+    <uses-permission android:name="android.permission.INTERNET"/>
+</manifest>

+ 6 - 0
mobile/android/app/src/main/kotlin/com/example/immich_mobile/MainActivity.kt

@@ -0,0 +1,6 @@
+package com.example.immich_mobile
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}

+ 12 - 0
mobile/android/app/src/main/res/drawable-v21/launch_background.xml

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

+ 12 - 0
mobile/android/app/src/main/res/drawable/launch_background.xml

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

二進制
mobile/android/app/src/main/res/mipmap-hdpi/ic_launcher.png


二進制
mobile/android/app/src/main/res/mipmap-mdpi/ic_launcher.png


二進制
mobile/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png


二進制
mobile/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png


二進制
mobile/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


+ 18 - 0
mobile/android/app/src/main/res/values-night/styles.xml

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

+ 18 - 0
mobile/android/app/src/main/res/values/styles.xml

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

+ 7 - 0
mobile/android/app/src/profile/AndroidManifest.xml

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

+ 31 - 0
mobile/android/build.gradle

@@ -0,0 +1,31 @@
+buildscript {
+    ext.kotlin_version = '1.6.10'
+    repositories {
+        google()
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:4.1.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+    project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+    project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+    delete rootProject.buildDir
+}

+ 3 - 0
mobile/android/gradle.properties

@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true

+ 6 - 0
mobile/android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip

+ 11 - 0
mobile/android/settings.gradle

@@ -0,0 +1,11 @@
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"

二進制
mobile/assets/immich-logo-no-outline.png


二進制
mobile/assets/immich-logo.png


+ 98 - 0
mobile/assets/immich-logo.svg

@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="svg2781" xmlns:svg="http://www.w3.org/2000/svg"
+	 xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 564.2 553.5"
+	 style="enable-background:new 0 0 564.2 553.5;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#4081EF;stroke:#512D8C;stroke-miterlimit:10;}
+	.st1{fill:#31A452;stroke:#512D8C;stroke-miterlimit:10;}
+	.st2{fill:#DE7FB3;stroke:#512D8C;stroke-miterlimit:10;}
+	.st3{fill:#FFB800;stroke:#512D8C;stroke-miterlimit:10;}
+	.st4{fill:#E64132;stroke:#512D8C;stroke-miterlimit:10;}
+	.st5{fill:#F2F5FB;stroke:#512D8C;stroke-miterlimit:10;}
+</style>
+<path class="st0" d="M210.5,549.6c-2.2-0.2-5.5-1-9.7-2.2c-52.4-15.7-99-46.5-133.8-88.5c-8.8-10.7-17.2-22.4-19.4-27.5
+	c-8.1-18.1-6.3-38.7,4.8-55.4c5-7.5,13.2-15,20.5-18.7c1.2-0.6,54.1-20,55.8-20.4c0.5-0.1,0.5,0.2-0.3,2.1c-0.7,1.7-1,3.1-1.1,5.5
+	l-0.1,3.2l2.8,5.8c8.7,17.9,19.2,32.7,33.2,46.4c6.3,6.2,7.8,7.6,13.8,12.3c22.7,18.1,52,30.7,79.9,34.3c2.5,0.3,5,0.8,5.7,1
+	c2.8,0.9,7.7-0.8,11-3.7l1.8-1.6l-0.2,4.8c-0.1,2.7-0.6,15.4-1,28.3c-0.6,20.3-0.8,24-1.5,27.5c-3.9,20.7-18.6,37.5-38.4,44.1
+	c-4.6,1.5-8,2.2-13.1,2.7C216.6,550.1,215.3,550,210.5,549.6z"/>
+<path class="st1" d="M339.8,549.4c-4-0.4-9.4-1.6-13.2-2.9c-3.4-1.2-10-4.4-12.5-6.1c-10.9-7.4-19-17.9-23.1-30
+	c-2.2-6.7-2.3-7.5-3.3-36.9c-0.5-14.9-0.9-27.9-0.9-28.9l0-1.9l2.3,1.8c2.6,2,6.6,3.4,8.5,3.1c0.6-0.1,3-0.5,5.3-0.8
+	c37.7-5.3,71.2-22.2,97.4-49.1c12.2-12.5,21.4-25.5,29.9-42.4l3.5-7l0-3.6c0-3.1-0.1-3.8-1-5.7c-0.5-1.2-0.9-2.1-0.9-2.2
+	c0.2-0.2,55.3,20.1,56.9,20.9c2.6,1.3,6.6,4.1,9.9,7c9.2,7.7,16.1,19.4,18.8,31.8c0.7,3.1,0.8,4.8,0.8,11.3c0,8.6-0.5,11.7-2.9,18.7
+	c-1.7,5-2.9,7.2-7.1,13.1c-7.6,11-15.3,20.5-25.2,31.2c-32.8,35.4-76.5,62.5-123.4,76.3C351.6,549.6,347.2,550.1,339.8,549.4z"/>
+<path class="st2" d="M255.6,438c-25.9-4.2-50.7-14.9-71.7-31c-5.2-4-8.7-7.1-14.1-12.4c-12.7-12.5-21.9-24.9-30.5-41.4
+	c-2.3-4.4-2.4-4.7-2.4-7.1c0-8.8,8.5-15.2,16.9-12.7c5.6,1.7,9.6,6.8,9.7,12.2c0,2.6-0.8,4.6-2.6,6.2c-1.2,1.1-3.2,1.9-4.6,1.9
+	c-1.2,0-3.3-0.8-4.3-1.6c-2.1-1.8-2-1,0.4,3.2c19.3,33.8,52.3,59.1,90,69.1c5.7,1.5,11.5,2.7,11.8,2.4c0.1-0.1-0.4-0.8-1.3-1.6
+	c-5.1-4.5-2.3-11.7,5-12.8c5.4-0.8,11.4,2.7,13.9,8c0.8,1.7,1,2.5,1,5.3s-0.1,3.5-1,5.3c-2,4.3-6.8,7.9-10.3,7.8
+	C260.6,438.7,257.9,438.3,255.6,438z"/>
+<path class="st0" d="M297.6,438.2c-3.4-1.3-6.4-4.3-7.8-8.1c-1.1-2.9-0.9-7.3,0.5-10.2c2.6-5.3,8.7-8.5,14.4-7.5
+	c2.9,0.5,4.7,1.9,6,4.3c0.8,1.6,1,2.2,0.8,3.6c-0.3,2.2-0.9,3.3-2.7,4.8c-0.8,0.7-1.4,1.4-1.3,1.5c0.5,0.5,13.4-2.7,21.3-5.4
+	c33.6-11.3,62.5-35.1,80.4-66.1c2.5-4.4,2.6-5,0.5-3.2c-2.8,2.4-7,1.9-9.6-1c-4-4.6-0.7-13.8,6.1-16.9c2-0.9,2.7-1,5.5-1
+	c2.9,0,3.5,0.1,5.6,1.1c4.4,2.1,7.4,6.4,7.8,11c0.2,2.2,0.1,2.3-2.2,6.9c-23,45.9-67,78.1-117.2,85.9
+	C300.2,438.8,299.4,438.9,297.6,438.2z"/>
+<path class="st1" d="M211.1,398.5c-4.7-0.9-8.7-2.7-12.9-5.9c-10.8-8.1-13.5-22.3-6.6-33.7c0.7-1.2,1.1-2.2,1-2.4
+	c-0.2-0.2-1.2-0.6-2.3-1.1c-7.6-3-13-10.6-13.5-19.1c-0.5-7.4,3.1-15,9-19.4c1-0.7,2.2-1.5,2.6-1.8c0.8-0.4,68.9-22.7,69.4-22.7
+	c0.2,0,0.7,0.7,1.2,1.5c0.5,0.8,1.6,2.3,2.4,3.3c1.2,1.4,1.5,1.9,1.2,2.3c-0.2,0.3-6.9,9.5-14.8,20.5
+	c-15.9,21.9-15.5,21.3-13.4,23.4c1.3,1.3,2.9,1.4,4.4,0.3c0.6-0.4,7.5-9.7,15.5-20.7c11.2-15.4,14.6-19.9,15-19.7
+	c0.9,0.4,5.5,1.9,6.6,2.1l1,0.2l0,35.3c0,39.7,0,38.8-2.5,44c-2.6,5.3-7.2,9.3-12.7,11.2c-3.7,1.3-6.8,1.6-10.2,1
+	c-5.5-0.9-9.8-3.2-13.7-7.4l-2.2-2.4l-0.6,0.9c-3,4.3-8.6,8.1-14,9.5C218.2,398.6,213.2,398.9,211.1,398.5z"/>
+<path class="st3" d="M342.9,398.5c-5.5-0.9-9.9-3.2-14.3-7.6l-3.2-3.2l-0.7,1c-2.3,3.3-6.8,6.5-11.1,7.9c-3.7,1.2-9.2,1.4-12.6,0.3
+	c-7.1-2.1-12.7-7.4-15.2-14.3l-0.9-2.6v-37.1v-37.1l1.8-0.4c1-0.2,2.7-0.8,3.9-1.2c1.1-0.5,2.1-0.8,2.2-0.7c0.1,0.1,6.5,9,14.4,19.9
+	c7.8,10.9,14.7,20.1,15.2,20.5c2.2,1.9,5.4,0.4,5.4-2.6c0-1.4-1-2.9-13.8-20.5c-7.6-10.5-14.2-19.6-14.7-20.4l-0.9-1.3l1.4-1.7
+	c0.8-0.9,1.9-2.5,2.5-3.4l1-1.6l34.4,11.2c18.9,6.2,35.1,11.6,35.9,12.1c6.8,4,11.1,11.3,11.1,19.1c0,4.1-0.5,6.4-2.4,10.2
+	c-2,4.1-5.5,7.6-9.6,9.7c-1.6,0.8-3.2,1.5-3.4,1.5c-1,0-0.9,0.7,0.3,2.6c2.8,4.3,4,8.5,3.9,13.7c0,8.1-3.7,15.2-10.6,20.3
+	C356.4,397.6,349.5,399.5,342.9,398.5z"/>
+<path class="st2" d="M53.9,341.9c-0.5-0.1-2.3-0.4-3.9-0.7c-15.6-2.6-30.4-12.6-38.8-26.2c-3.5-5.7-6.4-13.2-7.8-19.9
+	c-1.2-6.1-0.8-28.1,0.8-43.1c4.5-43,19-84.3,42.2-120.7c6.5-10.2,14.9-21.5,18.2-24.6c17.8-16.6,43.1-20.5,64.8-10
+	c4.3,2.1,8.8,5.1,12.7,8.6c2.8,2.4,5.8,6.1,20.9,25.5c9.7,12.5,17.8,22.8,17.9,23c0.2,0.2-0.9,0.4-3.2,0.4c-2.5,0-4.1,0.2-5.7,0.7
+	c-2.1,0.7-2.6,1.1-7.9,6.3c-8.2,8.1-14.4,15.3-20.3,23.9c-15.5,22.2-25.4,47.7-28.8,74.8c-2.2,16.9-1.6,37.5,1.6,52.3
+	c0.3,1.4,0.5,2.8,0.4,3c-0.1,0.2,0.2,1.3,0.8,2.4c1.1,2.4,4.3,5.7,6.5,6.8l1.5,0.8l-1.2,0.4c-0.7,0.2-13.1,3.8-27.6,8
+	c-16.4,4.7-27.7,7.8-29.8,8.1C64.1,342.1,56.1,342.3,53.9,341.9z"/>
+<path class="st3" d="M494.7,341.7c-2.1-0.3-33.8-9.1-56.5-15.8l-2.5-0.7l1.6-0.8c3.4-1.7,7.2-6.6,7.3-9.6c0-0.7,0.4-3.3,0.8-5.8
+	c3.9-22.7,3.1-46.1-2.5-68.4c-6.4-25.5-18.6-49.2-35.8-69.1c-4.6-5.3-14.8-15.4-16.4-16.1c-2.4-1.1-5.1-1.6-8-1.4l-2.7,0.2l1.2-1.5
+	c0.7-0.8,8.5-10.8,17.5-22.3c8.9-11.5,17.2-21.8,18.5-23.1c2.6-2.7,7-6.2,10.3-8.2c19.3-11.6,43-11.1,61.6,1.2
+	c5.4,3.6,8.2,6.2,12.3,11.7c26.4,34.5,44,73.7,52.3,116.2c3.4,17.6,4.9,33.3,5,52.4c0,13-0.2,14.8-2.5,21.8
+	C547.8,328.6,521.7,345.2,494.7,341.7z"/>
+<path class="st4" d="M133.9,318.5c-2-0.5-4.6-1.9-6-3.3c-2.5-2.4-3.1-3.5-3.7-7.3c-4.4-27.3-2.2-54,6.7-79.3
+	c5.3-15.1,13.5-30.5,23-43.1c5.8-7.8,16.6-19.5,19-20.7c4.7-2.4,11.3-1.2,15.2,2.7c5.4,5.4,5.2,13.9-0.3,19.1
+	c-4.3,4-9.4,4.4-12.6,0.9c-1.7-1.9-2.2-3.9-1.7-6.4c0.2-1.1,0.3-2,0.2-2.2c-0.3-0.3-3.6,3.3-8.3,9.1c-17.6,21.8-28.5,48-31.9,76.5
+	c-1.1,9.3-1,26.4,0.1,34.6c0.3,1.8,0.8,1.9,1.4,0.1c0.9-2.6,4-4.7,6.8-4.7c3,0,5.9,2.2,7.5,5.7c0.6,1.3,0.8,2.3,0.8,5.2
+	c0,3.3-0.1,3.8-1.1,5.7c-1.4,2.7-4.6,5.7-7.1,6.6C139.4,318.6,135.8,318.9,133.9,318.5z"/>
+<path class="st1" d="M422.6,318.5c-3.7-0.6-7.7-3.6-9.4-7.1c-3.8-7.5,0.1-16.9,6.9-16.9c3.1,0,5.8,2,6.9,5.2
+	c0.4,1.2,0.5,1.3,0.7,0.7c1.3-3.7,1.7-26.4,0.6-35.7c-3.6-29.6-14.5-55.3-33-77.9c-5.5-6.7-8.4-9.4-7.1-6.6c0.7,1.4,0.5,4.3-0.3,5.9
+	c-0.9,1.7-3.2,3.5-5,3.8c-3.2,0.6-7.9-1.6-10.2-4.8c-6.5-8.8-0.5-21.2,10.4-21.4c4.6-0.1,5.2,0.3,11.2,6.4
+	c12.1,12.3,21.1,24.9,28.8,40.3c13.2,26.3,18.6,54.9,16.1,84.5c-0.5,5.6-2,15.7-2.6,17.1c-1.3,2.8-4.8,5.5-8.4,6.5
+	C425.9,318.9,425.1,318.9,422.6,318.5z"/>
+<path class="st0" d="M178.2,307.2c-6-1.3-12.2-6.2-14.9-11.7c-3.4-7-3.1-15.1,0.9-21.6c0.7-1.2,1.2-2.3,1.1-2.4
+	c-0.1-0.1-1.1-0.6-2.1-1c-3.9-1.5-8.1-4.8-10.7-8.3c-4.6-6.2-6.1-14.6-3.9-22.1c2.9-10.3,9.4-16.8,19.1-19.3c2.8-0.7,9-0.8,11.7,0
+	c1.1,0.3,2.2,0.5,2.4,0.5c0.2,0,0.3-0.7,0.3-1.5c0-2.9,0.8-5.8,2.4-9.2c5.2-10.8,18.1-15.5,29-10.5c2.7,1.2,6.2,3.8,7.8,5.8
+	c0.7,0.8,10.3,14,21.5,29.4l20.3,27.9l-1.5,1.8c-0.8,1-1.9,2.6-2.5,3.5c-0.6,1-1.2,1.7-1.5,1.6c-4.5-1.7-46.7-15-47.7-15
+	c-1.9,0-3.1,1.3-3.1,3.2c0,1,0.2,1.7,0.8,2.3c0.6,0.6,7.8,3.1,24.5,8.5l23.7,7.7l-0.1,4.3l-0.1,4.3L223,295.9
+	c-18,5.9-33.9,10.9-35.2,11.2C184.7,307.8,181.2,307.8,178.2,307.2z"/>
+<path class="st4" d="M372.5,306.8c-1.8-0.5-17.5-5.6-35-11.3l-31.8-10.4l1-4.3v-4.3l22.6-7.7c15-4.9,24-8,24.6-8.5
+	c0.7-0.6,0.9-1.1,0.9-2.2c0-2-1.2-3.3-3.1-3.3c-0.9,0-10.5,2.9-24.7,7.5c-12.8,4.1-23.4,7.5-23.6,7.5c-0.1,0-0.7-0.8-1.3-1.9
+	c-0.6-1-1.6-2.5-2.2-3.2c-0.7-0.7-1.2-1.5-1.2-1.6c0-0.2,9.6-13.5,21.4-29.6c18.9-26,21.6-29.6,23.6-31.1c5.7-4.4,13.1-5.8,19.7-3.9
+	c9,2.7,16.1,11.6,16.1,20.3c0,2.3-0.1,2.3,3.1,1.5c4.7-1.1,11.5-0.5,16,1.5c4.6,2,9,6,11.5,10.2c2.1,3.6,3.9,9.4,4.2,13.2
+	c0.3,5.2-1.1,10.7-4,15.3c-2.6,4.1-7.8,8.3-12.1,9.8c-0.9,0.3-1.7,0.8-1.7,1c0,0.2,0.4,1,0.9,1.7c2.4,3.6,3.6,7.7,3.5,12.7
+	c0,5.8-2.1,10.7-6.4,15.1c-4,4.1-8.9,6.3-14.9,6.5C376.3,307.7,375.3,307.6,372.5,306.8z"/>
+<path class="st5" d="M276.2,298.9c-6.1-1.6-11.4-6.8-13.2-12.9c-0.7-2.4-0.7-7.5,0-9.9c1.7-5.8,6.6-10.8,12.3-12.5
+	c2.7-0.8,7.2-0.9,10-0.2c6.2,1.6,11.6,7.1,13.2,13.3c1.6,6-0.3,12.6-5,17.3C288.9,298.6,282.2,300.5,276.2,298.9z"/>
+<path class="st2" d="M248.3,229.8c-13.3-18.3-21.2-29.6-22-31.1c-1.4-3-1.9-5.5-1.9-9.4c0-14.1,13.1-24.4,27.1-21.4
+	c1.4,0.3,2.6,0.5,2.7,0.5s0.3-1.3,0.4-2.8c0.8-10.7,8.4-19.6,18.9-22.4c3.9-1,10.6-1,14.5,0c8.9,2.3,15.9,9.3,18.2,18.2
+	c0.4,1.5,0.7,3.7,0.7,4.9c0,1.2,0.1,2.1,0.3,2.1s1.5-0.3,3-0.6c7.4-1.6,15.2,0.7,20.5,6c4.3,4.3,6.6,9.6,6.6,15.6
+	c0,4-0.6,6.5-2.4,10c-0.6,1.2-10.4,15-21.7,30.7c-17.8,24.5-20.8,28.5-21.4,28.3c-0.4-0.1-1.9-0.6-3.4-1.1c-1.5-0.5-2.9-0.9-3.3-0.9
+	c-0.7,0-0.7-0.8-0.3-25.5v-25.5l-1.4-0.9c-1-1.1-2.5-1.5-3.8-0.9c-2,0.8-2-0.5-1.8,27.2v25.8h-1.2c-0.5-0.2-2.4,0.3-4,0.9
+	s-3.1,1.1-3.2,1.1C269.2,258.5,259.8,245.6,248.3,229.8z"/>
+<path class="st3" d="M210.9,164.8c-4.1-0.9-7.7-3.6-9.6-7.4c-1.4-2.8-1.7-7.3-0.5-10.3c1.7-4.5,3.9-6.1,15.6-11.2
+	c15.8-7,31.4-11.1,49.2-12.9c7.3-0.8,23.2-0.8,30.6,0c17.4,1.8,33.3,6,49.1,13c7.3,3.2,12.5,6.1,13.6,7.5c4.3,5.6,3.8,12.7-1.1,17.6
+	c-5.1,5.1-12.9,5.4-18.1,0.7c-2-1.8-3-3.5-3.4-5.6c-0.7-4,2.9-8.1,7.3-8.2c1.4,0,1.5-0.1,1.1-0.5c-0.3-0.3-2.2-1.2-4.3-2.1
+	c-33.2-14.5-70.5-16.4-105-5.4c-7.5,2.4-19,7.2-18.6,7.7c0.1,0.2,0.8,0.3,1.6,0.3c5.6,0,9.1,6.2,6.1,10.8
+	C221.6,163.3,215.9,165.9,210.9,164.8z"/>
+<path class="st4" d="M174.7,123.4c-8.9-13.1-16.8-25.1-17.5-26.6c-1.6-3.3-3.6-9.2-4.4-13c-2.6-12.5-0.9-25.8,5-37.5
+	c4.2-8.3,11.2-16.3,18.6-21.3c5-3.4,6.1-3.9,12.8-6.3c23.1-8.2,47.2-13.1,73.4-15c7.5-0.6,28.5-0.6,36.3,0
+	c25.5,1.8,50.6,6.9,73,14.8c6.4,2.2,8.2,3.1,13.1,6.5c9.8,6.6,18.1,17.5,22,29.2c2.2,6.5,2.7,10,2.7,17.9c0,7.9-0.5,11.3-2.7,17.9
+	c-2.3,6.8-3.7,9.1-20.3,33.6l-16.1,23.8l-0.4-2.2c-0.2-1.2-0.9-3-1.4-4c-1-1.8-4.4-5.6-4.7-5.2c-0.1,0.1-1.2-0.4-2.4-1.1
+	c-9.1-5.2-21.9-10.5-33.2-13.9c-37-11-77.2-8.8-113,6.1c-4.9,2.1-17.7,8.4-19.2,9.5c-2.2,1.6-5.1,6.8-5.1,9c0,0.4-0.1,1-0.3,1.2
+	C191,147,184.7,138,174.7,123.4z"/>
+</svg>

+ 34 - 0
mobile/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

+ 26 - 0
mobile/ios/Flutter/AppFrameworkInfo.plist

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

+ 2 - 0
mobile/ios/Flutter/Debug.xcconfig

@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"

+ 2 - 0
mobile/ios/Flutter/Release.xcconfig

@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"

+ 41 - 0
mobile/ios/Podfile

@@ -0,0 +1,41 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.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__))
+end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    flutter_additional_ios_build_settings(target)
+  end
+end

+ 50 - 0
mobile/ios/Podfile.lock

@@ -0,0 +1,50 @@
+PODS:
+  - device_info_plus (0.0.1):
+    - Flutter
+  - Flutter (1.0.0)
+  - FMDB (2.7.5):
+    - FMDB/standard (= 2.7.5)
+  - FMDB/standard (2.7.5)
+  - path_provider_ios (0.0.1):
+    - Flutter
+  - photo_manager (1.0.0):
+    - Flutter
+    - FlutterMacOS
+  - sqflite (0.0.2):
+    - Flutter
+    - FMDB (>= 2.7.5)
+
+DEPENDENCIES:
+  - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
+  - Flutter (from `Flutter`)
+  - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
+  - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
+  - sqflite (from `.symlinks/plugins/sqflite/ios`)
+
+SPEC REPOS:
+  trunk:
+    - FMDB
+
+EXTERNAL SOURCES:
+  device_info_plus:
+    :path: ".symlinks/plugins/device_info_plus/ios"
+  Flutter:
+    :path: Flutter
+  path_provider_ios:
+    :path: ".symlinks/plugins/path_provider_ios/ios"
+  photo_manager:
+    :path: ".symlinks/plugins/photo_manager/ios"
+  sqflite:
+    :path: ".symlinks/plugins/sqflite/ios"
+
+SPEC CHECKSUMS:
+  device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
+  Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a
+  FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
+  path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
+  photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
+  sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
+
+PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
+
+COCOAPODS: 1.10.1

+ 551 - 0
mobile/ios/Runner.xcodeproj/project.pbxproj

@@ -0,0 +1,551 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 51;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+		3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+		74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+		97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+		97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+		97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+		D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+		9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 10;
+			files = (
+			);
+			name = "Embed Frameworks";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+		1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
+		1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
+		2E3441B73560D0F6FD25E04F /* 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 = "<group>"; };
+		3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
+		74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
+		74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
+		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
+		886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
+		9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
+		97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		E0E99CDC17B3EB7FA8BA2332 /* 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 = "<group>"; };
+		F7101BB0391A314774615E89 /* 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 = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		97C146EB1CF9000F007C117D /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		0FB772A5B9601143383626CA /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */,
+				E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */,
+				F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */,
+			);
+			path = Pods;
+			sourceTree = "<group>";
+		};
+		1754452DD81DA6620E279E51 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		9740EEB11CF90186004384FC /* Flutter */ = {
+			isa = PBXGroup;
+			children = (
+				3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+				9740EEB21CF90195004384FC /* Debug.xcconfig */,
+				7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+				9740EEB31CF90195004384FC /* Generated.xcconfig */,
+			);
+			name = Flutter;
+			sourceTree = "<group>";
+		};
+		97C146E51CF9000F007C117D = {
+			isa = PBXGroup;
+			children = (
+				9740EEB11CF90186004384FC /* Flutter */,
+				97C146F01CF9000F007C117D /* Runner */,
+				97C146EF1CF9000F007C117D /* Products */,
+				0FB772A5B9601143383626CA /* Pods */,
+				1754452DD81DA6620E279E51 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		97C146EF1CF9000F007C117D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				97C146EE1CF9000F007C117D /* Runner.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		97C146F01CF9000F007C117D /* Runner */ = {
+			isa = PBXGroup;
+			children = (
+				97C146FA1CF9000F007C117D /* Main.storyboard */,
+				97C146FD1CF9000F007C117D /* Assets.xcassets */,
+				97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+				97C147021CF9000F007C117D /* Info.plist */,
+				1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+				1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+				74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+				74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+			);
+			path = Runner;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		97C146ED1CF9000F007C117D /* Runner */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+			buildPhases = (
+				4044AF030EF7D8721844FFBA /* [CP] Check Pods Manifest.lock */,
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+				D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */,
+			);
+			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 = {
+				LastUpgradeCheck = 1300;
+				ORGANIZATIONNAME = "";
+				TargetAttributes = {
+					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 */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		97C146EC1CF9000F007C117D /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+				3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+				97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+				97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+			);
+			name = "Thin Binary";
+			outputPaths = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+		};
+		4044AF030EF7D8721844FFBA /* [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;
+		};
+		9740EEB61CF901F6004384FC /* Run Script */ = {
+			isa = PBXShellScriptBuildPhase;
+			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";
+		};
+		D218A34AEE62BC1EF119F5B0 /* [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 */
+		97C146EA1CF9000F007C117D /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+				1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+		97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C146FB1CF9000F007C117D /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "<group>";
+		};
+		97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				97C147001CF9000F007C117D /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		249021D3217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				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;
+				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 = 9.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SUPPORTED_PLATFORMS = iphoneos;
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Profile;
+		};
+		249021D4217E4FDB00AE95B9 /* Profile */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				DEVELOPMENT_TEAM = C24486LLLU;
+				ENABLE_BITCODE = NO;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.immichMobile;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Profile;
+		};
+		97C147031CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				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;
+				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 = 9.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;
+				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;
+				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 = 9.0;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				SUPPORTED_PLATFORMS = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				SWIFT_OPTIMIZATION_LEVEL = "-O";
+				TARGETED_DEVICE_FAMILY = "1,2";
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		97C147061CF9000F007C117D /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				DEVELOPMENT_TEAM = C24486LLLU;
+				ENABLE_BITCODE = NO;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.immichMobile;
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				VERSIONING_SYSTEM = "apple-generic";
+			};
+			name = Debug;
+		};
+		97C147071CF9000F007C117D /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CLANG_ENABLE_MODULES = YES;
+				CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+				DEVELOPMENT_TEAM = C24486LLLU;
+				ENABLE_BITCODE = NO;
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.immichMobile;
+				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 */
+		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 */;
+}

+ 7 - 0
mobile/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata

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

+ 8 - 0
mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

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

+ 8 - 0
mobile/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

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

+ 87 - 0
mobile/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1300"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+               BuildableName = "Runner.app"
+               BlueprintName = "Runner"
+               ReferencedContainer = "container:Runner.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <Testables>
+      </Testables>
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      launchStyle = "0"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Profile"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 10 - 0
mobile/ios/Runner.xcworkspace/contents.xcworkspacedata

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

+ 8 - 0
mobile/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist

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

+ 8 - 0
mobile/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings

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

+ 13 - 0
mobile/ios/Runner/AppDelegate.swift

@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@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)
+  }
+}

+ 122 - 0
mobile/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"
+  }
+}

二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png


二進制
mobile/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png


+ 23 - 0
mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json

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

二進制
mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png


二進制
mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png


二進制
mobile/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png


+ 5 - 0
mobile/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.

+ 37 - 0
mobile/ios/Runner/Base.lproj/LaunchScreen.storyboard

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

+ 26 - 0
mobile/ios/Runner/Base.lproj/Main.storyboard

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

+ 49 - 0
mobile/ios/Runner/Info.plist

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

+ 1 - 0
mobile/ios/Runner/Runner-Bridging-Header.h

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

+ 11 - 0
mobile/lib/constants/hive_box.dart

@@ -0,0 +1,11 @@
+// Access token
+const String userInfoBox = "immichBoxUserInfo"; // Box
+const String accessTokenKey = "immichBoxAccessTokenKey"; // Key 1
+const String deviceIdKey = 'immichBoxDeviceIdKey'; // Key 2
+
+// SERVER ENDPOINT
+const String serverEndpointKey = 'immichBoxServerEndpoint';
+
+// KEY
+const String hiveAllAsssetKey = "allAssets";
+const String hiveBackupProgressKey = "backupProgressAssets";

+ 92 - 0
mobile/lib/main.dart

@@ -0,0 +1,92 @@
+import 'package:flutter/material.dart';
+import 'package:hive_flutter/hive_flutter.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/shared/providers/app_state.provider.dart';
+import 'constants/hive_box.dart';
+import 'package:google_fonts/google_fonts.dart';
+
+void main() async {
+  await Hive.initFlutter();
+  await Hive.openBox(userInfoBox);
+  // Hive.registerAdapter(ImmichBackUpAssetAdapter());
+  // Hive.deleteBoxFromDisk(hiveImmichBox);
+
+  runApp(const ProviderScope(child: ImmichApp()));
+}
+
+class ImmichApp extends ConsumerStatefulWidget {
+  const ImmichApp({Key? key}) : super(key: key);
+
+  @override
+  _ImmichAppState createState() => _ImmichAppState();
+}
+
+class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserver {
+  @override
+  void didChangeAppLifecycleState(AppLifecycleState state) {
+    switch (state) {
+      case AppLifecycleState.resumed:
+        debugPrint("[APP STATE] resumed");
+        ref.read(appStateProvider.notifier).state = AppStateEnum.resumed;
+        break;
+      case AppLifecycleState.inactive:
+        debugPrint("[APP STATE] inactive");
+        ref.read(appStateProvider.notifier).state = AppStateEnum.inactive;
+        break;
+      case AppLifecycleState.paused:
+        debugPrint("[APP STATE] paused");
+        ref.read(appStateProvider.notifier).state = AppStateEnum.paused;
+        break;
+      case AppLifecycleState.detached:
+        debugPrint("[APP STATE] detached");
+        ref.read(appStateProvider.notifier).state = AppStateEnum.detached;
+        break;
+    }
+  }
+
+  Future<void> initApp() async {
+    // ! TOBE DELETE
+    // Simulate Sign In And Register/Get Device ID
+    // await ref.read(authenticationProvider.notifier).login();
+    // ref.read(backupProvider.notifier).getBackupInfo();
+    // WidgetsBinding.instance?.addObserver(this);
+  }
+
+  @override
+  initState() {
+    super.initState();
+    initApp().then((_) => debugPrint("App Init Completed"));
+  }
+
+  @override
+  void dispose() {
+    WidgetsBinding.instance?.removeObserver(this);
+    super.dispose();
+  }
+
+  final _immichRouter = AppRouter();
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp.router(
+      title: 'Immich',
+      debugShowCheckedModeBanner: false,
+      theme: ThemeData(
+        primarySwatch: Colors.indigo,
+        textTheme: GoogleFonts.workSansTextTheme(
+          Theme.of(context).textTheme.apply(fontSizeFactor: 1.0),
+        ),
+        scaffoldBackgroundColor: const Color(0xFFf6f8fe),
+        appBarTheme: const AppBarTheme(
+          backgroundColor: Colors.white,
+          foregroundColor: Colors.indigo,
+          elevation: 1,
+          centerTitle: true,
+        ),
+      ),
+      routeInformationParser: _immichRouter.defaultRouteParser(),
+      routerDelegate: _immichRouter.delegate(),
+    );
+  }
+}

+ 0 - 0
mobile/lib/module_template/models/store_model_here.txt


+ 0 - 0
mobile/lib/module_template/providers/store_providers_here.txt


+ 0 - 0
mobile/lib/module_template/services/store_services_here.txt


+ 0 - 0
mobile/lib/module_template/ui/store_ui_here.txt


+ 0 - 0
mobile/lib/module_template/views/store_views_here.txt


+ 113 - 0
mobile/lib/modules/home/models/get_all_asset_respose.model.dart

@@ -0,0 +1,113 @@
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
+import 'package:immich_mobile/shared/models/immich_asset.model.dart';
+
+class ImmichAssetGroupByDate {
+  final String date;
+  List<ImmichAsset> assets;
+  ImmichAssetGroupByDate({
+    required this.date,
+    required this.assets,
+  });
+
+  ImmichAssetGroupByDate copyWith({
+    String? date,
+    List<ImmichAsset>? assets,
+  }) {
+    return ImmichAssetGroupByDate(
+      date: date ?? this.date,
+      assets: assets ?? this.assets,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'date': date,
+      'assets': assets.map((x) => x.toMap()).toList(),
+    };
+  }
+
+  factory ImmichAssetGroupByDate.fromMap(Map<String, dynamic> map) {
+    return ImmichAssetGroupByDate(
+      date: map['date'] ?? '',
+      assets: List<ImmichAsset>.from(map['assets']?.map((x) => ImmichAsset.fromMap(x))),
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory ImmichAssetGroupByDate.fromJson(String source) => ImmichAssetGroupByDate.fromMap(json.decode(source));
+
+  @override
+  String toString() => 'ImmichAssetGroupByDate(date: $date, assets: $assets)';
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is ImmichAssetGroupByDate && other.date == date && listEquals(other.assets, assets);
+  }
+
+  @override
+  int get hashCode => date.hashCode ^ assets.hashCode;
+}
+
+class GetAllAssetResponse {
+  final int count;
+  final List<ImmichAssetGroupByDate> data;
+  final String nextPageKey;
+  GetAllAssetResponse({
+    required this.count,
+    required this.data,
+    required this.nextPageKey,
+  });
+
+  GetAllAssetResponse copyWith({
+    int? count,
+    List<ImmichAssetGroupByDate>? data,
+    String? nextPageKey,
+  }) {
+    return GetAllAssetResponse(
+      count: count ?? this.count,
+      data: data ?? this.data,
+      nextPageKey: nextPageKey ?? this.nextPageKey,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'count': count,
+      'data': data.map((x) => x.toMap()).toList(),
+      'nextPageKey': nextPageKey,
+    };
+  }
+
+  factory GetAllAssetResponse.fromMap(Map<String, dynamic> map) {
+    return GetAllAssetResponse(
+      count: map['count']?.toInt() ?? 0,
+      data: List<ImmichAssetGroupByDate>.from(map['data']?.map((x) => ImmichAssetGroupByDate.fromMap(x))),
+      nextPageKey: map['nextPageKey'] ?? '',
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory GetAllAssetResponse.fromJson(String source) => GetAllAssetResponse.fromMap(json.decode(source));
+
+  @override
+  String toString() => 'GetAllAssetResponse(count: $count, data: $data, nextPageKey: $nextPageKey)';
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is GetAllAssetResponse &&
+        other.count == count &&
+        listEquals(other.data, data) &&
+        other.nextPageKey == nextPageKey;
+  }
+
+  @override
+  int get hashCode => count.hashCode ^ data.hashCode ^ nextPageKey.hashCode;
+}

+ 60 - 0
mobile/lib/modules/home/providers/asset.provider.dart

@@ -0,0 +1,60 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
+import 'package:immich_mobile/modules/home/services/asset.service.dart';
+
+class AssetNotifier extends StateNotifier<List<ImmichAssetGroupByDate>> {
+  final imagePerPage = 100;
+  final AssetService _assetService = AssetService();
+
+  AssetNotifier() : super([]);
+  late String? nextPageKey = "";
+  bool isFetching = false;
+
+  getImmichAssets() async {
+    GetAllAssetResponse? res = await _assetService.getAllAsset();
+    nextPageKey = res?.nextPageKey;
+
+    if (res != null) {
+      for (var assets in res.data) {
+        state = [...state, assets];
+      }
+    }
+  }
+
+  getMoreAsset() async {
+    if (nextPageKey != null && !isFetching) {
+      isFetching = true;
+      GetAllAssetResponse? res = await _assetService.getMoreAsset(nextPageKey);
+
+      if (res != null) {
+        nextPageKey = res.nextPageKey;
+
+        List<ImmichAssetGroupByDate> previousState = state;
+        List<ImmichAssetGroupByDate> currentState = [];
+
+        for (var assets in res.data) {
+          currentState = [...currentState, assets];
+        }
+
+        if (previousState.last.date == currentState.first.date) {
+          previousState.last.assets = [...previousState.last.assets, ...currentState.first.assets];
+          state = [...previousState, ...currentState.sublist(1)];
+        } else {
+          state = [...previousState, ...currentState];
+        }
+      }
+
+      isFetching = false;
+    }
+  }
+
+  clearAllAsset() {
+    state = [];
+  }
+}
+
+final currentLocalPageProvider = StateProvider<int>((ref) => 0);
+
+final assetProvider = StateNotifierProvider<AssetNotifier, List<ImmichAssetGroupByDate>>((ref) {
+  return AssetNotifier();
+});

+ 38 - 0
mobile/lib/modules/home/services/asset.service.dart

@@ -0,0 +1,38 @@
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
+import 'package:immich_mobile/shared/services/network.service.dart';
+
+class AssetService {
+  final NetworkService _networkService = NetworkService();
+
+  Future<GetAllAssetResponse?> getAllAsset() async {
+    var res = await _networkService.getRequest(url: "asset/all");
+    try {
+      Map<String, dynamic> decodedData = jsonDecode(res.toString());
+
+      GetAllAssetResponse result = GetAllAssetResponse.fromMap(decodedData);
+      return result;
+    } catch (e) {
+      debugPrint("Error getAllAsset  ${e.toString()}");
+    }
+  }
+
+  Future<GetAllAssetResponse?> getMoreAsset(String? nextPageKey) async {
+    try {
+      var res = await _networkService.getRequest(
+        url: "asset/all?nextPageKey=$nextPageKey",
+      );
+
+      Map<String, dynamic> decodedData = jsonDecode(res.toString());
+
+      GetAllAssetResponse result = GetAllAssetResponse.fromMap(decodedData);
+      if (result.count != 0) {
+        return result;
+      }
+    } catch (e) {
+      debugPrint("Error getAllAsset  ${e.toString()}");
+    }
+  }
+}

+ 26 - 0
mobile/lib/modules/home/ui/image_grid.dart

@@ -0,0 +1,26 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/modules/home/ui/thumbnail_image.dart';
+import 'package:immich_mobile/shared/models/immich_asset.model.dart';
+
+class ImageGrid extends StatelessWidget {
+  final List<ImmichAsset> assetGroup;
+
+  const ImageGrid({Key? key, required this.assetGroup}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return SliverGrid(
+      gridDelegate:
+          const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 3, crossAxisSpacing: 5.0, mainAxisSpacing: 5),
+      delegate: SliverChildBuilderDelegate(
+        (BuildContext context, int index) {
+          return GestureDetector(
+            onTap: () {},
+            child: ThumbnailImage(asset: assetGroup[index]),
+          );
+        },
+        childCount: assetGroup.length,
+      ),
+    );
+  }
+}

+ 105 - 0
mobile/lib/modules/home/ui/immich_sliver_appbar.dart

@@ -0,0 +1,105 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
+
+import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/shared/models/backup_state.model.dart';
+import 'package:immich_mobile/shared/providers/backup.provider.dart';
+
+class ImmichSliverAppBar extends ConsumerWidget {
+  const ImmichSliverAppBar({
+    Key? key,
+    required this.imageGridGroup,
+  }) : super(key: key);
+
+  final List<Widget> imageGridGroup;
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final BackUpState _backupState = ref.watch(backupProvider);
+
+    return SliverPadding(
+      padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 5),
+      sliver: SliverAppBar(
+        centerTitle: true,
+        floating: true,
+        pinned: false,
+        snap: false,
+        backgroundColor: Colors.grey[200],
+        shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
+        leading: Builder(
+          builder: (BuildContext context) {
+            return IconButton(
+              icon: const Icon(Icons.account_circle_rounded),
+              onPressed: () {
+                Scaffold.of(context).openDrawer();
+              },
+              tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
+            );
+          },
+        ),
+        title: Text(
+          'IMMICH',
+          style: GoogleFonts.snowburstOne(
+            textStyle: TextStyle(
+              fontWeight: FontWeight.bold,
+              fontSize: 18,
+              color: Theme.of(context).primaryColor,
+            ),
+          ),
+        ),
+        actions: [
+          Stack(
+            alignment: AlignmentDirectional.center,
+            children: [
+              _backupState.backupProgress == BackUpProgressEnum.inProgress
+                  ? Positioned(
+                      top: 10,
+                      right: 12,
+                      child: SizedBox(
+                        height: 8,
+                        width: 8,
+                        child: CircularProgressIndicator(
+                          strokeWidth: 1,
+                          valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
+                        ),
+                      ),
+                    )
+                  : Container(),
+              IconButton(
+                icon: const Icon(Icons.backup_rounded),
+                tooltip: 'Backup Controller',
+                onPressed: () async {
+                  var onPop = await AutoRouter.of(context).push(const BackupControllerRoute());
+
+                  // Fetch new image
+                  if (onPop == true) {
+                    // Remove and force getting new widget again
+                    if (imageGridGroup.isNotEmpty) {
+                      ref.read(assetProvider.notifier).getMoreAsset();
+                    } else {
+                      ref.read(assetProvider.notifier).getImmichAssets();
+                    }
+                  }
+                },
+              ),
+              _backupState.backupProgress == BackUpProgressEnum.inProgress
+                  ? Positioned(
+                      bottom: 5,
+                      child: Text(
+                        _backupState.backingUpAssetCount.toString(),
+                        style: const TextStyle(fontSize: 9, fontWeight: FontWeight.bold),
+                      ),
+                    )
+                  : Container()
+            ],
+          ),
+        ],
+        systemOverlayStyle: SystemUiOverlayStyle.dark,
+      ),
+    );
+  }
+}

+ 72 - 0
mobile/lib/modules/home/ui/profile_drawer.dart

@@ -0,0 +1,72 @@
+import 'package:auto_route/annotations.dart';
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/src/widgets/framework.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
+import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
+import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
+import 'package:immich_mobile/routing/router.dart';
+
+class ProfileDrawer extends ConsumerWidget {
+  const ProfileDrawer({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    AuthenticationState _authState = ref.watch(authenticationProvider);
+
+    return Drawer(
+      shape: const RoundedRectangleBorder(
+        borderRadius: BorderRadius.only(
+          topRight: Radius.circular(5),
+          bottomRight: Radius.circular(5),
+        ),
+      ),
+      child: ListView(
+        padding: EdgeInsets.zero,
+        children: [
+          DrawerHeader(
+            decoration: BoxDecoration(
+              color: Colors.grey[200],
+            ),
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.center,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                const Image(
+                  image: AssetImage('assets/immich-logo-no-outline.png'),
+                  width: 50,
+                  filterQuality: FilterQuality.high,
+                ),
+                const Padding(padding: EdgeInsets.all(8)),
+                Text(
+                  _authState.userEmail,
+                  style: TextStyle(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold),
+                )
+              ],
+            ),
+          ),
+          ListTile(
+            tileColor: Colors.grey[100],
+            leading: const Icon(
+              Icons.logout_rounded,
+              color: Colors.black54,
+            ),
+            title: const Text(
+              "Sign Out",
+              style: TextStyle(color: Colors.black54, fontSize: 14),
+            ),
+            onTap: () async {
+              bool res = await ref.read(authenticationProvider.notifier).logout();
+              ref.read(assetProvider.notifier).clearAllAsset();
+
+              if (res) {
+                AutoRouter.of(context).popUntilRoot();
+              }
+            },
+          )
+        ],
+      ),
+    );
+  }
+}

+ 52 - 0
mobile/lib/modules/home/ui/thumbnail_image.dart

@@ -0,0 +1,52 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:flutter/material.dart';
+import 'package:hive_flutter/hive_flutter.dart';
+import 'package:immich_mobile/constants/hive_box.dart';
+import 'package:immich_mobile/shared/models/immich_asset.model.dart';
+import 'package:immich_mobile/routing/router.dart';
+import 'package:transparent_image/transparent_image.dart';
+
+class ThumbnailImage extends StatelessWidget {
+  final ImmichAsset asset;
+
+  const ThumbnailImage({Key? key, required this.asset}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    var box = Hive.box(userInfoBox);
+    var thumbnailRequestUrl =
+        '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=true';
+
+    return GestureDetector(
+      onTap: () {
+        AutoRouter.of(context).push(
+          ImageViewerRoute(
+            imageUrl:
+                '${box.get(serverEndpointKey)}/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}&isThumb=false',
+            heroTag: asset.id,
+            thumbnailUrl: thumbnailRequestUrl,
+          ),
+        );
+      },
+      onLongPress: () {},
+      child: Hero(
+        tag: asset.id,
+        child: CachedNetworkImage(
+          width: 300,
+          height: 300,
+          memCacheHeight: 250,
+          fit: BoxFit.cover,
+          imageUrl: thumbnailRequestUrl,
+          httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
+          fadeInDuration: const Duration(milliseconds: 250),
+          progressIndicatorBuilder: (context, url, downloadProgress) => Transform.scale(
+            scale: 0.2,
+            child: CircularProgressIndicator(value: downloadProgress.progress),
+          ),
+          errorWidget: (context, url, error) => const Icon(Icons.error),
+        ),
+      ),
+    );
+  }
+}

+ 165 - 0
mobile/lib/modules/home/views/home_page.dart

@@ -0,0 +1,165 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
+import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
+import 'package:immich_mobile/shared/models/backup_state.model.dart';
+import 'package:immich_mobile/modules/home/models/get_all_asset_respose.model.dart';
+import 'package:immich_mobile/modules/home/ui/image_grid.dart';
+import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
+import 'package:immich_mobile/shared/providers/backup.provider.dart';
+import 'package:intl/intl.dart';
+
+class HomePage extends HookConsumerWidget {
+  const HomePage({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final ValueNotifier<bool> _showBackToTopBtn = useState(false);
+    ScrollController _scrollController = useScrollController();
+    List<ImmichAssetGroupByDate> assetGroup = ref.watch(assetProvider);
+    BackUpState _backupState = ref.watch(backupProvider);
+    List<Widget> imageGridGroup = [];
+    List<GlobalKey> monthGroupKey = [];
+
+    _scrollControllerCallback() {
+      var endOfPage = _scrollController.position.maxScrollExtent;
+
+      if (_scrollController.offset >= endOfPage - (endOfPage * 0.1) && !_scrollController.position.outOfRange) {
+        ref.read(assetProvider.notifier).getMoreAsset();
+      }
+
+      if (_scrollController.offset >= 400) {
+        _showBackToTopBtn.value = true;
+      } else {
+        _showBackToTopBtn.value = false;
+      }
+    }
+
+    useEffect(() {
+      ref.read(assetProvider.notifier).getImmichAssets();
+
+      _scrollController.addListener(_scrollControllerCallback);
+
+      return () => _scrollController.removeListener(_scrollControllerCallback);
+    }, [_scrollController, key]);
+
+    Widget _buildBody() {
+      if (assetGroup.isNotEmpty) {
+        String lastGroupDate = assetGroup[0].date;
+
+        for (var group in assetGroup) {
+          var dateTitle = group.date;
+          var assetGroup = group.assets;
+
+          int? currentMonth = DateTime.tryParse(dateTitle)?.month;
+          int? previousMonth = DateTime.tryParse(lastGroupDate)?.month;
+
+          if ((currentMonth! - previousMonth!) != 0) {
+            var myKey = GlobalKey();
+            monthGroupKey.add(myKey);
+            // debugPrint("Group Key $myKey");
+
+            imageGridGroup.add(
+              SliverToBoxAdapter(
+                key: myKey,
+                child: Padding(
+                  padding: const EdgeInsets.only(left: 10.0, top: 32),
+                  child: Text(
+                    DateFormat('MMMM, y').format(
+                      DateTime.parse(dateTitle),
+                    ),
+                    style: TextStyle(
+                      fontSize: 24,
+                      fontWeight: FontWeight.bold,
+                      color: Theme.of(context).primaryColor,
+                    ),
+                  ),
+                ),
+              ),
+            );
+          }
+
+          imageGridGroup.add(
+            _buildDateGroupTitle(dateTitle),
+          );
+
+          imageGridGroup.add(ImageGrid(assetGroup: assetGroup));
+
+          lastGroupDate = dateTitle;
+        }
+
+        return SafeArea(
+          child: CustomScrollView(
+            controller: _scrollController,
+            slivers: [
+              ImmichSliverAppBar(imageGridGroup: imageGridGroup),
+              ...imageGridGroup,
+            ],
+          ),
+        );
+      } else {
+        return Container();
+      }
+    }
+
+    return Scaffold(
+      drawer: const ProfileDrawer(),
+      body: _buildBody(),
+      bottomNavigationBar: BottomAppBar(
+        child: IconButton(
+          onPressed: () {
+            if (monthGroupKey.isNotEmpty) {
+              var targetContext = monthGroupKey.last.currentContext;
+              if (targetContext != null) {
+                Scrollable.ensureVisible(
+                  targetContext,
+                  duration: const Duration(milliseconds: 400),
+                  curve: Curves.easeInOut,
+                );
+              }
+            }
+          },
+          icon: const Icon(Icons.ac_unit_outlined),
+        ),
+      ),
+      floatingActionButton: _showBackToTopBtn.value
+          ? FloatingActionButton.small(
+              enableFeedback: true,
+              backgroundColor: Theme.of(context).secondaryHeaderColor,
+              foregroundColor: Theme.of(context).primaryColor,
+              onPressed: () {
+                _scrollController.animateTo(0, duration: const Duration(seconds: 1), curve: Curves.easeOutExpo);
+              },
+              child: const Icon(Icons.keyboard_arrow_up_rounded),
+            )
+          : null,
+    );
+  }
+
+  SliverToBoxAdapter _buildDateGroupTitle(String dateTitle) {
+    var currentYear = DateTime.now().year;
+    var groupYear = DateTime.parse(dateTitle).year;
+    var formatDateTemplate = currentYear == groupYear ? 'E, MMM dd' : 'E, MMM dd, yyyy';
+    return SliverToBoxAdapter(
+      child: Padding(
+        padding: const EdgeInsets.only(top: 24.0, bottom: 24.0, left: 3.0),
+        child: Row(
+          children: [
+            Padding(
+              padding: const EdgeInsets.only(left: 8.0, bottom: 5.0, top: 5.0),
+              child: Text(
+                DateFormat(formatDateTemplate).format(DateTime.parse(dateTitle)),
+                style: const TextStyle(
+                  fontSize: 14,
+                  fontWeight: FontWeight.bold,
+                  color: Colors.black87,
+                ),
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 93 - 0
mobile/lib/modules/login/models/authentication_state.model.dart

@@ -0,0 +1,93 @@
+import 'dart:convert';
+
+import 'package:immich_mobile/shared/models/device_info.model.dart';
+
+class AuthenticationState {
+  final String deviceId;
+  final String deviceType;
+  final String userId;
+  final String userEmail;
+  final bool isAuthenticated;
+  final DeviceInfoRemote deviceInfo;
+
+  AuthenticationState({
+    required this.deviceId,
+    required this.deviceType,
+    required this.userId,
+    required this.userEmail,
+    required this.isAuthenticated,
+    required this.deviceInfo,
+  });
+
+  AuthenticationState copyWith({
+    String? deviceId,
+    String? deviceType,
+    String? userId,
+    String? userEmail,
+    bool? isAuthenticated,
+    DeviceInfoRemote? deviceInfo,
+  }) {
+    return AuthenticationState(
+      deviceId: deviceId ?? this.deviceId,
+      deviceType: deviceType ?? this.deviceType,
+      userId: userId ?? this.userId,
+      userEmail: userEmail ?? this.userEmail,
+      isAuthenticated: isAuthenticated ?? this.isAuthenticated,
+      deviceInfo: deviceInfo ?? this.deviceInfo,
+    );
+  }
+
+  @override
+  String toString() {
+    return 'AuthenticationState(deviceId: $deviceId, deviceType: $deviceType, userId: $userId, userEmail: $userEmail, isAuthenticated: $isAuthenticated, deviceInfo: $deviceInfo)';
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'deviceId': deviceId,
+      'deviceType': deviceType,
+      'userId': userId,
+      'userEmail': userEmail,
+      'isAuthenticated': isAuthenticated,
+      'deviceInfo': deviceInfo.toMap(),
+    };
+  }
+
+  factory AuthenticationState.fromMap(Map<String, dynamic> map) {
+    return AuthenticationState(
+      deviceId: map['deviceId'] ?? '',
+      deviceType: map['deviceType'] ?? '',
+      userId: map['userId'] ?? '',
+      userEmail: map['userEmail'] ?? '',
+      isAuthenticated: map['isAuthenticated'] ?? false,
+      deviceInfo: DeviceInfoRemote.fromMap(map['deviceInfo']),
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory AuthenticationState.fromJson(String source) => AuthenticationState.fromMap(json.decode(source));
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is AuthenticationState &&
+        other.deviceId == deviceId &&
+        other.deviceType == deviceType &&
+        other.userId == userId &&
+        other.userEmail == userEmail &&
+        other.isAuthenticated == isAuthenticated &&
+        other.deviceInfo == deviceInfo;
+  }
+
+  @override
+  int get hashCode {
+    return deviceId.hashCode ^
+        deviceType.hashCode ^
+        userId.hashCode ^
+        userEmail.hashCode ^
+        isAuthenticated.hashCode ^
+        deviceInfo.hashCode;
+  }
+}

+ 61 - 0
mobile/lib/modules/login/models/login_response.model.dart

@@ -0,0 +1,61 @@
+import 'dart:convert';
+
+class LogInReponse {
+  final String accessToken;
+  final String userId;
+  final String userEmail;
+
+  LogInReponse({
+    required this.accessToken,
+    required this.userId,
+    required this.userEmail,
+  });
+
+  LogInReponse copyWith({
+    String? accessToken,
+    String? userId,
+    String? userEmail,
+  }) {
+    return LogInReponse(
+      accessToken: accessToken ?? this.accessToken,
+      userId: userId ?? this.userId,
+      userEmail: userEmail ?? this.userEmail,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'accessToken': accessToken,
+      'userId': userId,
+      'userEmail': userEmail,
+    };
+  }
+
+  factory LogInReponse.fromMap(Map<String, dynamic> map) {
+    return LogInReponse(
+      accessToken: map['accessToken'] ?? '',
+      userId: map['userId'] ?? '',
+      userEmail: map['userEmail'] ?? '',
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory LogInReponse.fromJson(String source) => LogInReponse.fromMap(json.decode(source));
+
+  @override
+  String toString() => 'LogInReponse(accessToken: $accessToken, userId: $userId, userEmail: $userEmail)';
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is LogInReponse &&
+        other.accessToken == accessToken &&
+        other.userId == userId &&
+        other.userEmail == userEmail;
+  }
+
+  @override
+  int get hashCode => accessToken.hashCode ^ userId.hashCode ^ userEmail.hashCode;
+}

+ 127 - 0
mobile/lib/modules/login/providers/authentication.provider.dart

@@ -0,0 +1,127 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:hive/hive.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/hive_box.dart';
+import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
+import 'package:immich_mobile/modules/login/models/login_response.model.dart';
+import 'package:immich_mobile/shared/services/backup.service.dart';
+import 'package:immich_mobile/shared/services/device_info.service.dart';
+import 'package:immich_mobile/shared/services/network.service.dart';
+import 'package:immich_mobile/shared/models/device_info.model.dart';
+import 'package:immich_mobile/utils/dio_http_interceptor.dart';
+
+class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
+  AuthenticationNotifier()
+      : super(
+          AuthenticationState(
+            deviceId: "",
+            deviceType: "",
+            isAuthenticated: false,
+            userId: "",
+            userEmail: "",
+            deviceInfo: DeviceInfoRemote(
+              id: 0,
+              userId: "",
+              deviceId: "",
+              deviceType: "",
+              notificationToken: "",
+              createdAt: "",
+              isAutoBackup: false,
+            ),
+          ),
+        );
+
+  final DeviceInfoService _deviceInfoService = DeviceInfoService();
+  final BackupService _backupService = BackupService();
+  final NetworkService _networkService = NetworkService();
+
+  Future<bool> login(String email, String password, String serverEndpoint) async {
+    // Store server endpoint to Hive and test endpoint
+    if (serverEndpoint[serverEndpoint.length - 1] == "/") {
+      var validUrl = serverEndpoint.substring(0, serverEndpoint.length - 1);
+      Hive.box(userInfoBox).put(serverEndpointKey, validUrl);
+    } else {
+      Hive.box(userInfoBox).put(serverEndpointKey, serverEndpoint);
+    }
+
+    bool isServerEndpointVerified = await _networkService.pingServer();
+    if (!isServerEndpointVerified) {
+      return false;
+    }
+
+    // Store device id to local storage
+    var deviceInfo = await _deviceInfoService.getDeviceInfo();
+    Hive.box(userInfoBox).put(deviceIdKey, deviceInfo["deviceId"]);
+
+    state = state.copyWith(
+      deviceId: deviceInfo["deviceId"],
+      deviceType: deviceInfo["deviceType"],
+    );
+
+    // Make sign-in request
+    try {
+      Response res = await _networkService.postRequest(url: 'auth/login', data: {'email': email, 'password': password});
+
+      var payload = LogInReponse.fromJson(res.toString());
+
+      Hive.box(userInfoBox).put(accessTokenKey, payload.accessToken);
+
+      state = state.copyWith(
+        isAuthenticated: true,
+        userId: payload.userId,
+        userEmail: payload.userEmail,
+      );
+    } catch (e) {
+      return false;
+    }
+
+    // Register device info
+    try {
+      Response res = await _networkService
+          .postRequest(url: 'device-info', data: {'deviceId': state.deviceId, 'deviceType': state.deviceType});
+
+      DeviceInfoRemote deviceInfo = DeviceInfoRemote.fromJson(res.toString());
+      state = state.copyWith(deviceInfo: deviceInfo);
+    } catch (e) {
+      debugPrint("ERROR Register Device Info: $e");
+    }
+
+    return true;
+  }
+
+  Future<bool> logout() async {
+    Hive.box(userInfoBox).delete(accessTokenKey);
+    state = AuthenticationState(
+      deviceId: "",
+      deviceType: "",
+      isAuthenticated: false,
+      userId: "",
+      userEmail: "",
+      deviceInfo: DeviceInfoRemote(
+        id: 0,
+        userId: "",
+        deviceId: "",
+        deviceType: "",
+        notificationToken: "",
+        createdAt: "",
+        isAutoBackup: false,
+      ),
+    );
+
+    return true;
+  }
+
+  setAutoBackup(bool backupState) async {
+    var deviceInfo = await _deviceInfoService.getDeviceInfo();
+    var deviceId = deviceInfo["deviceId"];
+    var deviceType = deviceInfo["deviceType"];
+
+    DeviceInfoRemote deviceInfoRemote = await _backupService.setAutoBackup(backupState, deviceId, deviceType);
+    state = state.copyWith(deviceInfo: deviceInfoRemote);
+  }
+}
+
+final authenticationProvider = StateNotifierProvider<AuthenticationNotifier, AuthenticationState>((ref) {
+  return AuthenticationNotifier();
+});

+ 124 - 0
mobile/lib/modules/login/ui/login_form.dart

@@ -0,0 +1,124 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
+
+class LoginForm extends HookConsumerWidget {
+  const LoginForm({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final usernameController = useTextEditingController(text: 'testuser@email.com');
+    final passwordController = useTextEditingController(text: 'password');
+    final serverEndpointController = useTextEditingController(text: 'http://192.168.1.216');
+
+    return Center(
+      child: ConstrainedBox(
+        constraints: const BoxConstraints(maxWidth: 300),
+        child: Wrap(
+          spacing: 32,
+          runSpacing: 32,
+          alignment: WrapAlignment.center,
+          children: [
+            const Image(
+              image: AssetImage('assets/immich-logo-no-outline.png'),
+              width: 128,
+              filterQuality: FilterQuality.high,
+            ),
+            Text(
+              'IMMICH',
+              style: GoogleFonts.snowburstOne(
+                  textStyle:
+                      TextStyle(fontWeight: FontWeight.bold, fontSize: 48, color: Theme.of(context).primaryColor)),
+            ),
+            EmailInput(controller: usernameController),
+            PasswordInput(controller: passwordController),
+            ServerEndpointInput(controller: serverEndpointController),
+            LoginButton(
+              emailController: usernameController,
+              passwordController: passwordController,
+              serverEndpointController: serverEndpointController,
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class ServerEndpointInput extends StatelessWidget {
+  final TextEditingController controller;
+
+  const ServerEndpointInput({Key? key, required this.controller}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return TextFormField(
+      controller: controller,
+      decoration: const InputDecoration(
+          labelText: 'Server Endpoint URL', border: OutlineInputBorder(), hintText: 'http://your-server-ip:port'),
+    );
+  }
+}
+
+class EmailInput extends StatelessWidget {
+  final TextEditingController controller;
+
+  const EmailInput({Key? key, required this.controller}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return TextFormField(
+      controller: controller,
+      decoration:
+          const InputDecoration(labelText: 'email', border: OutlineInputBorder(), hintText: 'youremail@email.com'),
+    );
+  }
+}
+
+class PasswordInput extends StatelessWidget {
+  final TextEditingController controller;
+
+  const PasswordInput({Key? key, required this.controller}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return TextFormField(
+      obscureText: true,
+      controller: controller,
+      decoration: const InputDecoration(labelText: 'Password', border: OutlineInputBorder(), hintText: 'password'),
+    );
+  }
+}
+
+class LoginButton extends ConsumerWidget {
+  final TextEditingController emailController;
+  final TextEditingController passwordController;
+  final TextEditingController serverEndpointController;
+
+  const LoginButton(
+      {Key? key,
+      required this.emailController,
+      required this.passwordController,
+      required this.serverEndpointController})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return ElevatedButton(
+        onPressed: () async {
+          var isAuthenicated = await ref
+              .read(authenticationProvider.notifier)
+              .login(emailController.text, passwordController.text, serverEndpointController.text);
+
+          if (isAuthenicated) {
+            AutoRouter.of(context).pushNamed("/home-page");
+          } else {
+            debugPrint("BAD LOGIN TRY AGAIN - Show UI Here");
+          }
+        },
+        child: const Text("Login"));
+  }
+}

+ 16 - 0
mobile/lib/modules/login/views/login_page.dart

@@ -0,0 +1,16 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
+import 'package:immich_mobile/modules/login/ui/login_form.dart';
+
+class LoginPage extends HookConsumerWidget {
+  const LoginPage({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    return const Scaffold(
+      body: LoginForm(),
+    );
+  }
+}

+ 22 - 0
mobile/lib/routing/auth_guard.dart

@@ -0,0 +1,22 @@
+import 'dart:convert';
+
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:immich_mobile/shared/services/network.service.dart';
+
+class AuthGuard extends AutoRouteGuard {
+  final NetworkService _networkService = NetworkService();
+
+  @override
+  void onNavigation(NavigationResolver resolver, StackRouter router) async {
+    try {
+      var res = await _networkService.postRequest(url: 'auth/validateToken');
+      var jsonReponse = jsonDecode(res.toString());
+      if (jsonReponse['authStatus']) {
+        resolver.next(true);
+      }
+    } catch (e) {
+      router.removeUntil((route) => route.name == "LoginRoute");
+    }
+  }
+}

+ 22 - 0
mobile/lib/routing/router.dart

@@ -0,0 +1,22 @@
+import 'package:auto_route/auto_route.dart';
+import 'package:flutter/widgets.dart';
+import 'package:immich_mobile/modules/login/views/login_page.dart';
+import 'package:immich_mobile/modules/home/views/home_page.dart';
+import 'package:immich_mobile/routing/auth_guard.dart';
+import 'package:immich_mobile/shared/views/backup_controller_page.dart';
+import 'package:immich_mobile/shared/views/image_viewer_page.dart';
+
+part 'router.gr.dart';
+
+@MaterialAutoRouter(
+  replaceInRouteName: 'Page,Route',
+  routes: <AutoRoute>[
+    AutoRoute(page: LoginPage, initial: true),
+    AutoRoute(page: HomePage, guards: [AuthGuard]),
+    AutoRoute(page: BackupControllerPage, guards: [AuthGuard]),
+    AutoRoute(page: ImageViewerPage, guards: [AuthGuard]),
+  ],
+)
+class AppRouter extends _$AppRouter {
+  AppRouter() : super(authGuard: AuthGuard());
+}

+ 122 - 0
mobile/lib/routing/router.gr.dart

@@ -0,0 +1,122 @@
+// **************************************************************************
+// AutoRouteGenerator
+// **************************************************************************
+
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+// **************************************************************************
+// AutoRouteGenerator
+// **************************************************************************
+//
+// ignore_for_file: type=lint
+
+part of 'router.dart';
+
+class _$AppRouter extends RootStackRouter {
+  _$AppRouter(
+      {GlobalKey<NavigatorState>? navigatorKey, required this.authGuard})
+      : super(navigatorKey);
+
+  final AuthGuard authGuard;
+
+  @override
+  final Map<String, PageFactory> pagesMap = {
+    LoginRoute.name: (routeData) {
+      return MaterialPageX<dynamic>(
+          routeData: routeData, child: const LoginPage());
+    },
+    HomeRoute.name: (routeData) {
+      return MaterialPageX<dynamic>(
+          routeData: routeData, child: const HomePage());
+    },
+    BackupControllerRoute.name: (routeData) {
+      return MaterialPageX<dynamic>(
+          routeData: routeData, child: const BackupControllerPage());
+    },
+    ImageViewerRoute.name: (routeData) {
+      final args = routeData.argsAs<ImageViewerRouteArgs>();
+      return MaterialPageX<dynamic>(
+          routeData: routeData,
+          child: ImageViewerPage(
+              key: args.key,
+              imageUrl: args.imageUrl,
+              heroTag: args.heroTag,
+              thumbnailUrl: args.thumbnailUrl));
+    }
+  };
+
+  @override
+  List<RouteConfig> get routes => [
+        RouteConfig(LoginRoute.name, path: '/'),
+        RouteConfig(HomeRoute.name, path: '/home-page', guards: [authGuard]),
+        RouteConfig(BackupControllerRoute.name,
+            path: '/backup-controller-page', guards: [authGuard]),
+        RouteConfig(ImageViewerRoute.name,
+            path: '/image-viewer-page', guards: [authGuard])
+      ];
+}
+
+/// generated route for
+/// [LoginPage]
+class LoginRoute extends PageRouteInfo<void> {
+  const LoginRoute() : super(LoginRoute.name, path: '/');
+
+  static const String name = 'LoginRoute';
+}
+
+/// generated route for
+/// [HomePage]
+class HomeRoute extends PageRouteInfo<void> {
+  const HomeRoute() : super(HomeRoute.name, path: '/home-page');
+
+  static const String name = 'HomeRoute';
+}
+
+/// generated route for
+/// [BackupControllerPage]
+class BackupControllerRoute extends PageRouteInfo<void> {
+  const BackupControllerRoute()
+      : super(BackupControllerRoute.name, path: '/backup-controller-page');
+
+  static const String name = 'BackupControllerRoute';
+}
+
+/// generated route for
+/// [ImageViewerPage]
+class ImageViewerRoute extends PageRouteInfo<ImageViewerRouteArgs> {
+  ImageViewerRoute(
+      {Key? key,
+      required String imageUrl,
+      required String heroTag,
+      required String thumbnailUrl})
+      : super(ImageViewerRoute.name,
+            path: '/image-viewer-page',
+            args: ImageViewerRouteArgs(
+                key: key,
+                imageUrl: imageUrl,
+                heroTag: heroTag,
+                thumbnailUrl: thumbnailUrl));
+
+  static const String name = 'ImageViewerRoute';
+}
+
+class ImageViewerRouteArgs {
+  const ImageViewerRouteArgs(
+      {this.key,
+      required this.imageUrl,
+      required this.heroTag,
+      required this.thumbnailUrl});
+
+  final Key? key;
+
+  final String imageUrl;
+
+  final String heroTag;
+
+  final String thumbnailUrl;
+
+  @override
+  String toString() {
+    return 'ImageViewerRouteArgs{key: $key, imageUrl: $imageUrl, heroTag: $heroTag, thumbnailUrl: $thumbnailUrl}';
+  }
+}

+ 77 - 0
mobile/lib/shared/models/backup_state.model.dart

@@ -0,0 +1,77 @@
+import 'dart:convert';
+
+import 'package:dio/dio.dart';
+
+import 'package:immich_mobile/shared/models/server_info.model.dart';
+
+enum BackUpProgressEnum { idle, inProgress, done }
+
+class BackUpState {
+  final BackUpProgressEnum backupProgress;
+  final int totalAssetCount;
+  final int assetOnDatabase;
+  final int backingUpAssetCount;
+  final double progressInPercentage;
+  final CancelToken cancelToken;
+  final ServerInfo serverInfo;
+
+  BackUpState({
+    required this.backupProgress,
+    required this.totalAssetCount,
+    required this.assetOnDatabase,
+    required this.backingUpAssetCount,
+    required this.progressInPercentage,
+    required this.cancelToken,
+    required this.serverInfo,
+  });
+
+  BackUpState copyWith({
+    BackUpProgressEnum? backupProgress,
+    int? totalAssetCount,
+    int? assetOnDatabase,
+    int? backingUpAssetCount,
+    double? progressInPercentage,
+    CancelToken? cancelToken,
+    ServerInfo? serverInfo,
+  }) {
+    return BackUpState(
+      backupProgress: backupProgress ?? this.backupProgress,
+      totalAssetCount: totalAssetCount ?? this.totalAssetCount,
+      assetOnDatabase: assetOnDatabase ?? this.assetOnDatabase,
+      backingUpAssetCount: backingUpAssetCount ?? this.backingUpAssetCount,
+      progressInPercentage: progressInPercentage ?? this.progressInPercentage,
+      cancelToken: cancelToken ?? this.cancelToken,
+      serverInfo: serverInfo ?? this.serverInfo,
+    );
+  }
+
+  @override
+  String toString() {
+    return 'BackUpState(backupProgress: $backupProgress, totalAssetCount: $totalAssetCount, assetOnDatabase: $assetOnDatabase, backingUpAssetCount: $backingUpAssetCount, progressInPercentage: $progressInPercentage, cancelToken: $cancelToken, serverInfo: $serverInfo)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is BackUpState &&
+        other.backupProgress == backupProgress &&
+        other.totalAssetCount == totalAssetCount &&
+        other.assetOnDatabase == assetOnDatabase &&
+        other.backingUpAssetCount == backingUpAssetCount &&
+        other.progressInPercentage == progressInPercentage &&
+        other.cancelToken == cancelToken &&
+        other.serverInfo == serverInfo;
+  }
+
+  @override
+  int get hashCode {
+    return backupProgress.hashCode ^
+        totalAssetCount.hashCode ^
+        assetOnDatabase.hashCode ^
+        backingUpAssetCount.hashCode ^
+        progressInPercentage.hashCode ^
+        cancelToken.hashCode ^
+        serverInfo.hashCode;
+  }
+}

+ 100 - 0
mobile/lib/shared/models/device_info.model.dart

@@ -0,0 +1,100 @@
+import 'dart:convert';
+import 'dart:ffi';
+
+class DeviceInfoRemote {
+  final int id;
+  final String userId;
+  final String deviceId;
+  final String deviceType;
+  final String notificationToken;
+  final String createdAt;
+  final bool isAutoBackup;
+
+  DeviceInfoRemote({
+    required this.id,
+    required this.userId,
+    required this.deviceId,
+    required this.deviceType,
+    required this.notificationToken,
+    required this.createdAt,
+    required this.isAutoBackup,
+  });
+
+  DeviceInfoRemote copyWith({
+    int? id,
+    String? userId,
+    String? deviceId,
+    String? deviceType,
+    String? notificationToken,
+    String? createdAt,
+    bool? isAutoBackup,
+  }) {
+    return DeviceInfoRemote(
+      id: id ?? this.id,
+      userId: userId ?? this.userId,
+      deviceId: deviceId ?? this.deviceId,
+      deviceType: deviceType ?? this.deviceType,
+      notificationToken: notificationToken ?? this.notificationToken,
+      createdAt: createdAt ?? this.createdAt,
+      isAutoBackup: isAutoBackup ?? this.isAutoBackup,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'id': id,
+      'userId': userId,
+      'deviceId': deviceId,
+      'deviceType': deviceType,
+      'notificationToken': notificationToken,
+      'createdAt': createdAt,
+      'isAutoBackup': isAutoBackup,
+    };
+  }
+
+  factory DeviceInfoRemote.fromMap(Map<String, dynamic> map) {
+    return DeviceInfoRemote(
+      id: map['id']?.toInt() ?? 0,
+      userId: map['userId'] ?? '',
+      deviceId: map['deviceId'] ?? '',
+      deviceType: map['deviceType'] ?? '',
+      notificationToken: map['notificationToken'] ?? '',
+      createdAt: map['createdAt'] ?? '',
+      isAutoBackup: map['isAutoBackup'] ?? false,
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory DeviceInfoRemote.fromJson(String source) => DeviceInfoRemote.fromMap(json.decode(source));
+
+  @override
+  String toString() {
+    return 'DeviceInfo(id: $id, userId: $userId, deviceId: $deviceId, deviceType: $deviceType, notificationToken: $notificationToken, createdAt: $createdAt, isAutoBackup: $isAutoBackup)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is DeviceInfoRemote &&
+        other.id == id &&
+        other.userId == userId &&
+        other.deviceId == deviceId &&
+        other.deviceType == deviceType &&
+        other.notificationToken == notificationToken &&
+        other.createdAt == createdAt &&
+        other.isAutoBackup == isAutoBackup;
+  }
+
+  @override
+  int get hashCode {
+    return id.hashCode ^
+        userId.hashCode ^
+        deviceId.hashCode ^
+        deviceType.hashCode ^
+        notificationToken.hashCode ^
+        createdAt.hashCode ^
+        isAutoBackup.hashCode;
+  }
+}

+ 11 - 0
mobile/lib/shared/models/image_viewer_page_data.model.dart

@@ -0,0 +1,11 @@
+class ImageViewerPageData {
+  final String heroTag;
+  final String imageUrl;
+  final String thumbnailUrl;
+
+  ImageViewerPageData({
+    required this.heroTag,
+    required this.imageUrl,
+    required this.thumbnailUrl,
+  });
+}

+ 131 - 0
mobile/lib/shared/models/immich_asset.model.dart

@@ -0,0 +1,131 @@
+import 'dart:convert';
+
+class ImmichAsset {
+  final String id;
+  final String deviceAssetId;
+  final String userId;
+  final String deviceId;
+  final String assetType;
+  final String localPath;
+  final String remotePath;
+  final String createdAt;
+  final String modifiedAt;
+  final bool isFavorite;
+  final String? description;
+
+  ImmichAsset({
+    required this.id,
+    required this.deviceAssetId,
+    required this.userId,
+    required this.deviceId,
+    required this.assetType,
+    required this.localPath,
+    required this.remotePath,
+    required this.createdAt,
+    required this.modifiedAt,
+    required this.isFavorite,
+    this.description,
+  });
+
+  ImmichAsset copyWith({
+    String? id,
+    String? deviceAssetId,
+    String? userId,
+    String? deviceId,
+    String? assetType,
+    String? localPath,
+    String? remotePath,
+    String? createdAt,
+    String? modifiedAt,
+    bool? isFavorite,
+    String? description,
+  }) {
+    return ImmichAsset(
+      id: id ?? this.id,
+      deviceAssetId: deviceAssetId ?? this.deviceAssetId,
+      userId: userId ?? this.userId,
+      deviceId: deviceId ?? this.deviceId,
+      assetType: assetType ?? this.assetType,
+      localPath: localPath ?? this.localPath,
+      remotePath: remotePath ?? this.remotePath,
+      createdAt: createdAt ?? this.createdAt,
+      modifiedAt: modifiedAt ?? this.modifiedAt,
+      isFavorite: isFavorite ?? this.isFavorite,
+      description: description ?? this.description,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'id': id,
+      'deviceAssetId': deviceAssetId,
+      'userId': userId,
+      'deviceId': deviceId,
+      'assetType': assetType,
+      'localPath': localPath,
+      'remotePath': remotePath,
+      'createdAt': createdAt,
+      'modifiedAt': modifiedAt,
+      'isFavorite': isFavorite,
+      'description': description,
+    };
+  }
+
+  factory ImmichAsset.fromMap(Map<String, dynamic> map) {
+    return ImmichAsset(
+      id: map['id'] ?? '',
+      deviceAssetId: map['deviceAssetId'] ?? '',
+      userId: map['userId'] ?? '',
+      deviceId: map['deviceId'] ?? '',
+      assetType: map['assetType'] ?? '',
+      localPath: map['localPath'] ?? '',
+      remotePath: map['remotePath'] ?? '',
+      createdAt: map['createdAt'] ?? '',
+      modifiedAt: map['modifiedAt'] ?? '',
+      isFavorite: map['isFavorite'] ?? false,
+      description: map['description'],
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory ImmichAsset.fromJson(String source) => ImmichAsset.fromMap(json.decode(source));
+
+  @override
+  String toString() {
+    return 'ImmichAsset(id: $id, deviceAssetId: $deviceAssetId, userId: $userId, deviceId: $deviceId, assetType: $assetType, localPath: $localPath, remotePath: $remotePath, createdAt: $createdAt, modifiedAt: $modifiedAt, isFavorite: $isFavorite, description: $description)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is ImmichAsset &&
+        other.id == id &&
+        other.deviceAssetId == deviceAssetId &&
+        other.userId == userId &&
+        other.deviceId == deviceId &&
+        other.assetType == assetType &&
+        other.localPath == localPath &&
+        other.remotePath == remotePath &&
+        other.createdAt == createdAt &&
+        other.modifiedAt == modifiedAt &&
+        other.isFavorite == isFavorite &&
+        other.description == description;
+  }
+
+  @override
+  int get hashCode {
+    return id.hashCode ^
+        deviceAssetId.hashCode ^
+        userId.hashCode ^
+        deviceId.hashCode ^
+        assetType.hashCode ^
+        localPath.hashCode ^
+        remotePath.hashCode ^
+        createdAt.hashCode ^
+        modifiedAt.hashCode ^
+        isFavorite.hashCode ^
+        description.hashCode;
+  }
+}

+ 98 - 0
mobile/lib/shared/models/server_info.model.dart

@@ -0,0 +1,98 @@
+import 'dart:convert';
+
+class ServerInfo {
+  final String diskSize;
+  final String diskUse;
+  final String diskAvailable;
+  final int diskSizeRaw;
+  final int diskUseRaw;
+  final int diskAvailableRaw;
+  final double diskUsagePercentage;
+  ServerInfo({
+    required this.diskSize,
+    required this.diskUse,
+    required this.diskAvailable,
+    required this.diskSizeRaw,
+    required this.diskUseRaw,
+    required this.diskAvailableRaw,
+    required this.diskUsagePercentage,
+  });
+
+  ServerInfo copyWith({
+    String? diskSize,
+    String? diskUse,
+    String? diskAvailable,
+    int? diskSizeRaw,
+    int? diskUseRaw,
+    int? diskAvailableRaw,
+    double? diskUsagePercentage,
+  }) {
+    return ServerInfo(
+      diskSize: diskSize ?? this.diskSize,
+      diskUse: diskUse ?? this.diskUse,
+      diskAvailable: diskAvailable ?? this.diskAvailable,
+      diskSizeRaw: diskSizeRaw ?? this.diskSizeRaw,
+      diskUseRaw: diskUseRaw ?? this.diskUseRaw,
+      diskAvailableRaw: diskAvailableRaw ?? this.diskAvailableRaw,
+      diskUsagePercentage: diskUsagePercentage ?? this.diskUsagePercentage,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'diskSize': diskSize,
+      'diskUse': diskUse,
+      'diskAvailable': diskAvailable,
+      'diskSizeRaw': diskSizeRaw,
+      'diskUseRaw': diskUseRaw,
+      'diskAvailableRaw': diskAvailableRaw,
+      'diskUsagePercentage': diskUsagePercentage,
+    };
+  }
+
+  factory ServerInfo.fromMap(Map<String, dynamic> map) {
+    return ServerInfo(
+      diskSize: map['diskSize'] ?? '',
+      diskUse: map['diskUse'] ?? '',
+      diskAvailable: map['diskAvailable'] ?? '',
+      diskSizeRaw: map['diskSizeRaw']?.toInt() ?? 0,
+      diskUseRaw: map['diskUseRaw']?.toInt() ?? 0,
+      diskAvailableRaw: map['diskAvailableRaw']?.toInt() ?? 0,
+      diskUsagePercentage: map['diskUsagePercentage']?.toDouble() ?? 0.0,
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory ServerInfo.fromJson(String source) => ServerInfo.fromMap(json.decode(source));
+
+  @override
+  String toString() {
+    return 'ServerInfo(diskSize: $diskSize, diskUse: $diskUse, diskAvailable: $diskAvailable, diskSizeRaw: $diskSizeRaw, diskUseRaw: $diskUseRaw, diskAvailableRaw: $diskAvailableRaw, diskUsagePercentage: $diskUsagePercentage)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is ServerInfo &&
+        other.diskSize == diskSize &&
+        other.diskUse == diskUse &&
+        other.diskAvailable == diskAvailable &&
+        other.diskSizeRaw == diskSizeRaw &&
+        other.diskUseRaw == diskUseRaw &&
+        other.diskAvailableRaw == diskAvailableRaw &&
+        other.diskUsagePercentage == diskUsagePercentage;
+  }
+
+  @override
+  int get hashCode {
+    return diskSize.hashCode ^
+        diskUse.hashCode ^
+        diskAvailable.hashCode ^
+        diskSizeRaw.hashCode ^
+        diskUseRaw.hashCode ^
+        diskAvailableRaw.hashCode ^
+        diskUsagePercentage.hashCode;
+  }
+}

+ 13 - 0
mobile/lib/shared/providers/app_state.provider.dart

@@ -0,0 +1,13 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+enum AppStateEnum {
+  active,
+  inactive,
+  paused,
+  resumed,
+  detached,
+}
+
+final appStateProvider = StateProvider<AppStateEnum>((ref) {
+  return AppStateEnum.active;
+});

+ 137 - 0
mobile/lib/shared/providers/backup.provider.dart

@@ -0,0 +1,137 @@
+import 'package:dio/dio.dart';
+import 'package:flutter/foundation.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/shared/services/server_info.service.dart';
+import 'package:immich_mobile/shared/models/backup_state.model.dart';
+import 'package:immich_mobile/shared/models/server_info.model.dart';
+import 'package:immich_mobile/shared/services/backup.service.dart';
+import 'package:photo_manager/photo_manager.dart';
+
+class BackupNotifier extends StateNotifier<BackUpState> {
+  BackupNotifier()
+      : super(
+          BackUpState(
+            backupProgress: BackUpProgressEnum.idle,
+            backingUpAssetCount: 0,
+            assetOnDatabase: 0,
+            totalAssetCount: 0,
+            progressInPercentage: 0,
+            cancelToken: CancelToken(),
+            serverInfo: ServerInfo(
+              diskAvailable: "0",
+              diskAvailableRaw: 0,
+              diskSize: "0",
+              diskSizeRaw: 0,
+              diskUsagePercentage: 0.0,
+              diskUse: "0",
+              diskUseRaw: 0,
+            ),
+          ),
+        );
+
+  final BackupService _backupService = BackupService();
+  final ServerInfoService _serverInfoService = ServerInfoService();
+
+  void getBackupInfo() async {
+    _updateServerInfo();
+
+    List<AssetPathEntity> list = await PhotoManager.getAssetPathList(onlyAll: true, type: RequestType.image);
+
+    if (list.isEmpty) {
+      debugPrint("No Asset On Device");
+      return;
+    }
+
+    int totalAsset = list[0].assetCount;
+    List<String> didBackupAsset = await _backupService.getDeviceBackupAsset();
+
+    state = state.copyWith(totalAssetCount: totalAsset, assetOnDatabase: didBackupAsset.length);
+  }
+
+  void startBackupProcess() async {
+    _updateServerInfo();
+
+    state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
+
+    var authResult = await PhotoManager.requestPermissionExtend();
+    if (authResult.isAuth) {
+      await PhotoManager.clearFileCache();
+      // await PhotoManager.presentLimited();
+      // Gather assets info
+      List<AssetPathEntity> list =
+          await PhotoManager.getAssetPathList(hasAll: true, onlyAll: true, type: RequestType.image);
+
+      if (list.isEmpty) {
+        debugPrint("No Asset On Device - Abort Backup Process");
+        return;
+      }
+
+      int totalAsset = list[0].assetCount;
+      List<AssetEntity> currentAssets = await list[0].getAssetListRange(start: 0, end: totalAsset);
+
+      // Get device assets info from database
+      // Compare and find different assets that has not been backing up
+      // Backup those assets
+      List<String> backupAsset = await _backupService.getDeviceBackupAsset();
+
+      state = state.copyWith(totalAssetCount: totalAsset, assetOnDatabase: backupAsset.length);
+      // Remove item that has already been backed up
+      for (var backupAssetId in backupAsset) {
+        currentAssets.removeWhere((e) => e.id == backupAssetId);
+      }
+
+      if (currentAssets.isEmpty) {
+        state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
+      }
+
+      state = state.copyWith(backingUpAssetCount: currentAssets.length);
+
+      // Perform Packup
+      state = state.copyWith(cancelToken: CancelToken());
+      _backupService.backupAsset(currentAssets, state.cancelToken, _onAssetUploaded, _onUploadProgress);
+    } else {
+      PhotoManager.openSetting();
+    }
+  }
+
+  void cancelBackup() {
+    state.cancelToken.cancel('Cancel Backup');
+    state = state.copyWith(backupProgress: BackUpProgressEnum.idle);
+  }
+
+  void _onAssetUploaded() {
+    state =
+        state.copyWith(backingUpAssetCount: state.backingUpAssetCount - 1, assetOnDatabase: state.assetOnDatabase + 1);
+
+    if (state.backingUpAssetCount == 0) {
+      state = state.copyWith(backupProgress: BackUpProgressEnum.done, progressInPercentage: 0.0);
+    }
+
+    _updateServerInfo();
+  }
+
+  void _onUploadProgress(int sent, int total) {
+    state = state.copyWith(progressInPercentage: (sent.toDouble() / total.toDouble() * 100));
+  }
+
+  void _updateServerInfo() async {
+    var serverInfo = await _serverInfoService.getServerInfo();
+
+    // Update server info
+    state = state.copyWith(
+      serverInfo: ServerInfo(
+        diskSize: serverInfo.diskSize,
+        diskUse: serverInfo.diskUse,
+        diskAvailable: serverInfo.diskAvailable,
+        diskSizeRaw: serverInfo.diskSizeRaw,
+        diskUseRaw: serverInfo.diskUseRaw,
+        diskAvailableRaw: serverInfo.diskAvailableRaw,
+        diskUsagePercentage: serverInfo.diskUsagePercentage,
+      ),
+    );
+  }
+}
+
+final backupProvider = StateNotifierProvider<BackupNotifier, BackUpState>((ref) {
+  return BackupNotifier();
+});

+ 124 - 0
mobile/lib/shared/services/backup.service.dart

@@ -0,0 +1,124 @@
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:hive/hive.dart';
+import 'package:immich_mobile/constants/hive_box.dart';
+import 'package:immich_mobile/shared/services/network.service.dart';
+import 'package:immich_mobile/shared/models/device_info.model.dart';
+import 'package:immich_mobile/utils/dio_http_interceptor.dart';
+import 'package:immich_mobile/utils/files_helper.dart';
+import 'package:photo_manager/photo_manager.dart';
+import 'package:http_parser/http_parser.dart';
+import 'package:path/path.dart' as p;
+import 'package:exif/exif.dart';
+
+class BackupService {
+  final NetworkService _networkService = NetworkService();
+
+  Future<List<String>> getDeviceBackupAsset() async {
+    String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
+
+    Response response = await _networkService.getRequest(url: "asset/$deviceId");
+    List<dynamic> result = jsonDecode(response.toString());
+
+    return result.cast<String>();
+  }
+
+  backupAsset(List<AssetEntity> assetList, CancelToken cancelToken, Function singleAssetDoneCb,
+      Function(int, int) uploadProgress) async {
+    var dio = Dio();
+    dio.interceptors.add(AuthenticatedRequestInterceptor());
+    String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
+    String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
+    File? file;
+
+    for (var entity in assetList) {
+      try {
+        file = await entity.file.timeout(const Duration(seconds: 5));
+
+        if (file != null) {
+          // reading exif
+          // var exifData = await readExifFromFile(file);
+
+          // for (String key in exifData.keys) {
+          //   debugPrint("- $key (${exifData[key]?.tagType}): ${exifData[key]}");
+          // }
+
+          // debugPrint("------------------");
+          String originalFileName = await entity.titleAsync;
+          String fileNameWithoutPath = originalFileName.toString().split(".")[0];
+          var fileExtension = p.extension(file.path);
+          LatLng coordinate = await entity.latlngAsync();
+          var mimeType = FileHelper.getMimeType(file.path);
+          var formData = FormData.fromMap({
+            'deviceAssetId': entity.id,
+            'deviceId': deviceId,
+            'assetType': _getAssetType(entity.type),
+            'createdAt': entity.createDateTime.toIso8601String(),
+            'modifiedAt': entity.modifiedDateTime.toIso8601String(),
+            'isFavorite': entity.isFavorite,
+            'fileExtension': fileExtension,
+            'lat': coordinate.latitude,
+            'lon': coordinate.longitude,
+            'files': [
+              await MultipartFile.fromFile(
+                file.path,
+                filename: fileNameWithoutPath,
+                contentType: MediaType(
+                  mimeType["type"],
+                  mimeType["subType"],
+                ),
+              ),
+            ]
+          });
+
+          Response res = await dio.post(
+            '$savedEndpoint/asset/upload',
+            data: formData,
+            cancelToken: cancelToken,
+            onSendProgress: (sent, total) => uploadProgress(sent, total),
+          );
+
+          if (res.statusCode == 201) {
+            singleAssetDoneCb();
+          }
+        }
+      } on DioError catch (e) {
+        debugPrint("DioError backupAsset: ${e.response}");
+        break;
+      } catch (e) {
+        debugPrint("ERROR backupAsset: ${e.toString()}");
+        continue;
+      } finally {
+        if (Platform.isIOS) {
+          file?.deleteSync();
+        }
+      }
+    }
+  }
+
+  String _getAssetType(AssetType assetType) {
+    switch (assetType) {
+      case AssetType.audio:
+        return "AUDIO";
+      case AssetType.image:
+        return "IMAGE";
+      case AssetType.video:
+        return "VIDEO";
+      case AssetType.other:
+        return "OTHER";
+    }
+  }
+
+  Future<DeviceInfoRemote> setAutoBackup(bool status, String deviceId, String deviceType) async {
+    var res = await _networkService.patchRequest(url: 'device-info', data: {
+      "isAutoBackup": status,
+      "deviceId": deviceId,
+      "deviceType": deviceType,
+    });
+
+    return DeviceInfoRemote.fromJson(res.toString());
+  }
+}

+ 30 - 0
mobile/lib/shared/services/device_info.service.dart

@@ -0,0 +1,30 @@
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:flutter/material.dart';
+
+class DeviceInfoService {
+  Future<Map<String, dynamic>> getDeviceInfo() async {
+    // Get device info
+    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
+    String? deviceId = "";
+    String deviceType = "";
+
+    try {
+      AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
+      deviceId = androidInfo.androidId;
+      deviceType = "ANDROID";
+    } catch (e) {
+      debugPrint("Not an android device");
+    }
+
+    try {
+      IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
+      deviceId = iosInfo.identifierForVendor;
+      deviceType = "IOS";
+      debugPrint("Device ID: $deviceId");
+    } catch (e) {
+      debugPrint("Not an ios device");
+    }
+
+    return {"deviceId": deviceId, "deviceType": deviceType};
+  }
+}

+ 18 - 0
mobile/lib/shared/services/local_storage.service.dart

@@ -0,0 +1,18 @@
+import 'package:hive/hive.dart';
+import 'package:immich_mobile/constants/hive_box.dart';
+
+class LocalStorageService {
+  late Box _box;
+
+  LocalStorageService() {
+    _box = Hive.box(userInfoBox);
+  }
+
+  T get<T>(String key) {
+    return _box.get(key);
+  }
+
+  put<T>(String key, T value) {
+    return _box.put(key, value);
+  }
+}

+ 89 - 0
mobile/lib/shared/services/network.service.dart

@@ -0,0 +1,89 @@
+import 'dart:convert';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:hive/hive.dart';
+import 'package:immich_mobile/constants/hive_box.dart';
+import 'package:immich_mobile/utils/dio_http_interceptor.dart';
+
+class NetworkService {
+  Future<dynamic> getRequest({required String url}) async {
+    try {
+      var dio = Dio();
+      dio.interceptors.add(AuthenticatedRequestInterceptor());
+
+      var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
+      Response res = await dio.get('$savedEndpoint/$url');
+
+      if (res.statusCode == 200) {
+        return res;
+      }
+    } on DioError catch (e) {
+      debugPrint("DioError: ${e.response}");
+    } catch (e) {
+      debugPrint("ERROR getRequest: ${e.toString()}");
+    }
+  }
+
+  Future<dynamic> postRequest({required String url, dynamic data}) async {
+    try {
+      var dio = Dio();
+      dio.interceptors.add(AuthenticatedRequestInterceptor());
+
+      var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
+      String validUrl = Uri.parse('$savedEndpoint/$url').toString();
+      Response res = await dio.post(validUrl, data: data);
+
+      return res;
+    } on DioError catch (e) {
+      debugPrint("DioError: ${e.response}");
+      return false;
+    } catch (e) {
+      debugPrint("ERROR BackupService: $e");
+    }
+  }
+
+  Future<dynamic> patchRequest({required String url, dynamic data}) async {
+    try {
+      var dio = Dio();
+      dio.interceptors.add(AuthenticatedRequestInterceptor());
+
+      var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
+
+      String validUrl = Uri.parse('$savedEndpoint/$url').toString();
+      Response res = await dio.patch(validUrl, data: data);
+
+      return res;
+    } on DioError catch (e) {
+      debugPrint("DioError: ${e.response}");
+    } catch (e) {
+      debugPrint("ERROR BackupService: $e");
+    }
+  }
+
+  Future<bool> pingServer() async {
+    try {
+      var dio = Dio();
+
+      var savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
+
+      String validUrl = Uri.parse('$savedEndpoint/server-info/ping').toString();
+
+      debugPrint("pint server at url $validUrl");
+      Response res = await dio.get(validUrl);
+      var jsonRespsonse = jsonDecode(res.toString());
+
+      if (jsonRespsonse["res"] == "pong") {
+        return true;
+      } else {
+        return false;
+      }
+    } on DioError catch (e) {
+      debugPrint("[PING SERVER] DioError: ${e.response} - $e");
+      return false;
+    } catch (e) {
+      debugPrint("ERROR BackupService: $e");
+      return false;
+    }
+  }
+}

部分文件因文件數量過多而無法顯示