diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..2bb3073
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,23 @@
+image: ghcr.io/cirruslabs/flutter:latest
+
+stages:
+ - build
+
+variables:
+ SECURE_FILES_DOWNLOAD_PATH: './'
+
+build_android_apk:
+ stage: build
+ before_script:
+ - flutter doctor -v
+ script:
+ - cd mobile
+ - flutter pub get
+ - flutter build apk --release
+ artifacts:
+ name: "ForkFolio-Mobile-APK-$CI_COMMIT_SHORT_SHA"
+ paths:
+ - mobile/build/app/outputs/flutter-apk/app-release.apk
+ expire_in: 30 days
+ rules:
+ - if: $CI_COMMIT_BRANCH == "main"
diff --git a/mobile/README.md b/mobile/README.md
new file mode 100644
index 0000000..e49d9c8
--- /dev/null
+++ b/mobile/README.md
@@ -0,0 +1,59 @@
+# ForkFolio Mobile 📱🍳
+
+Cross-platform mobile application for **ForkFolio** built with Flutter, featuring deep **Android Share Target** ingestion, multimodal **Gemini Vision Camera Scanning**, and distraction-free **Cook Mode**.
+
+---
+
+## ✨ Features
+
+* **Interactive Share Target (`ACTION_SEND`)**:
+ * Tap "Share" from Chrome, Instagram, TikTok, Reddit, or YouTube directly to ForkFolio.
+ * Pops an interactive bottom sheet that sanitizes URLs, extracts captions, scrapes details, and lets you preview & tag before saving.
+* **Camera Recipe Scanner**:
+ * Point the camera at physical cookbooks, magazine pages, or handwritten recipe cards.
+ * Direct ingestion into ForkFolio's `POST /api/recipes/scan-photo` powered by server-side Gemini 2.5 Flash Vision.
+* **Kitchen Cook Mode**:
+ * Automatic `Wakelock` keeps the screen awake so you don't touch your phone with flour or messy hands.
+ * Large, counter-readable typography with swipeable step cards.
+ * Live serving size scaling (0.5x, 1x, 1.5x, 2x) recalculating ingredient amounts on the fly.
+ * Integrated cooking timers with audible alerts.
+* **Hardware-Backed Persistent Authentication**:
+ * Connects using `ff_m_...` device tokens stored in **Android Keystore** (`EncryptedSharedPreferences`) or **iOS Keychain**.
+ * Sliding 90-day inactivity expiration.
+ * One-click remote device revocation via ForkFolio web settings.
+
+---
+
+## 🚀 Getting Started
+
+### 1. Prerequisites
+* [Flutter SDK](https://flutter.dev/docs/get-started/install) (version `>= 3.2.0`)
+* Android Studio with Android SDK (API 34+) or VS Code with Flutter extension
+* An active ForkFolio instance accessible over HTTPS
+
+### 2. Install Dependencies
+```bash
+cd forkfolio-mobile
+flutter pub get
+```
+
+### 3. Run in Development
+Connect an Android device or launch an emulator, then run:
+```bash
+flutter run
+```
+
+### 4. Build Release APK
+```bash
+flutter build apk --release
+```
+The resulting APK will be located at:
+`build/app/outputs/flutter-apk/app-release.apk`
+
+---
+
+## 🔒 Security & Networking
+
+* **Server Connection**: Enter your public HTTPS domain (e.g. `https://recipes.yourdomain.com`).
+* **Credentials**: Log in once with your standard username/password. The app automatically registers with your device model name (e.g. "Google Pixel 8 Pro") and receives an encrypted persistent token.
+* **Revocation**: At any time, you can visit the ForkFolio web app, open **Settings -> Linked Mobile Devices**, and tap **"Revoke"** to immediately cut off mobile access.
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
new file mode 100644
index 0000000..cc57a97
--- /dev/null
+++ b/mobile/android/app/build.gradle
@@ -0,0 +1,64 @@
+plugins {
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+android {
+ namespace "com.forkfolio.mobile"
+ compileSdk 34
+ ndkVersion flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_17
+ targetCompatibility JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = '17'
+ }
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ applicationId "com.forkfolio.mobile"
+ minSdkVersion 24
+ targetSdkVersion 34
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ signingConfig signingConfigs.debug
+ minifyEnabled false
+ shrinkResources false
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {}
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ef29f89
--- /dev/null
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/mobile/android/app/src/main/kotlin/com/forkfolio/mobile/MainActivity.kt b/mobile/android/app/src/main/kotlin/com/forkfolio/mobile/MainActivity.kt
new file mode 100644
index 0000000..31d7d73
--- /dev/null
+++ b/mobile/android/app/src/main/kotlin/com/forkfolio/mobile/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.forkfolio.mobile
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle
new file mode 100644
index 0000000..bc157bd
--- /dev/null
+++ b/mobile/android/build.gradle
@@ -0,0 +1,18 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+tasks.register("clean", Delete) {
+ delete rootProject.buildDir
+}
diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle
new file mode 100644
index 0000000..b9e43bd
--- /dev/null
+++ b/mobile/android/settings.gradle
@@ -0,0 +1,25 @@
+pluginManagement {
+ def flutterSdkPath = {
+ def properties = new Properties()
+ file("local.properties").withInputStream { properties.load(it) }
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ return flutterSdkPath
+ }()
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id "dev.flutter.flutter-plugin-loader" version "1.0.0"
+ id "com.android.application" version "8.1.0" apply false
+ id "org.jetbrains.kotlin.android" version "1.8.22" apply false
+}
+
+include ":app"
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
new file mode 100644
index 0000000..1704f72
--- /dev/null
+++ b/mobile/ios/Runner/Info.plist
@@ -0,0 +1,48 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ ForkFolio
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ forkfolio_mobile
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
+ NSCameraUsageDescription
+ ForkFolio needs camera access to scan recipe photos from cookbooks, recipe cards, and packaging.
+ NSPhotoLibraryUsageDescription
+ ForkFolio needs photo library access to import recipe screenshots and food photos.
+
+
+ AppGroupId
+ group.com.forkfolio.mobile
+
+
diff --git a/mobile/lib/core/api/api_client.dart b/mobile/lib/core/api/api_client.dart
new file mode 100644
index 0000000..6aacb52
--- /dev/null
+++ b/mobile/lib/core/api/api_client.dart
@@ -0,0 +1,205 @@
+import 'dart:convert';
+import 'package:dio/dio.dart';
+import '../models/recipe.dart';
+import '../storage/secure_storage_service.dart';
+
+class ApiException implements Exception {
+ final String message;
+ final int? statusCode;
+ ApiException(this.message, [this.statusCode]);
+
+ @override
+ String toString() => message;
+}
+
+class ApiClient {
+ final SecureStorageService _storage;
+ final Dio _dio;
+
+ ApiClient(this._storage)
+ : _dio = Dio(
+ BaseOptions(
+ connectTimeout: const Duration(seconds: 15),
+ receiveTimeout: const Duration(seconds: 45),
+ headers: {'Accept': 'application/json'},
+ ),
+ ) {
+ // Interceptor to auto-inject Bearer mobile token and base URL
+ _dio.interceptors.add(
+ InterceptorsWrapper(
+ onRequest: (options, handler) async {
+ final serverUrl = await _storage.getServerUrl();
+ final token = await _storage.getToken();
+
+ if (serverUrl != null && !options.path.startsWith('http')) {
+ options.baseUrl = serverUrl;
+ }
+
+ if (token != null && token.isNotEmpty) {
+ options.headers['Authorization'] = 'Bearer $token';
+ }
+
+ return handler.next(options);
+ },
+ onError: (DioException e, handler) {
+ String message = 'Network communication error.';
+ if (e.response != null) {
+ final data = e.response?.data;
+ if (data is Map && data['error'] != null) {
+ message = data['error'].toString();
+ } else if (e.response?.statusCode == 401) {
+ message = 'Session expired or invalid. Please log in again.';
+ } else {
+ message = 'Server returned HTTP ${e.response?.statusCode}.';
+ }
+ } else if (e.type == DioExceptionType.connectionTimeout) {
+ message = 'Connection timed out. Please check your internet or server URL.';
+ } else if (e.type == DioExceptionType.connectionError) {
+ message = 'Could not connect to ForkFolio server. Check domain or VPN status.';
+ }
+ return handler.reject(
+ DioException(
+ requestOptions: e.requestOptions,
+ error: ApiException(message, e.response?.statusCode),
+ response: e.response,
+ type: e.type,
+ ),
+ );
+ },
+ ),
+ );
+ }
+
+ // 1. Mobile App Login
+ Future