From 745ff019549af37a143e6f5f07aa5b238446d968 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Sun, 7 Dec 2025 12:26:16 +0530 Subject: [PATCH] feat(widget): Add home screen widget with quick access actions --- android/app/src/main/AndroidManifest.xml | 13 + .../cogwheel/conduit/ConduitWidgetProvider.kt | 107 +++++ .../res/drawable-v31/ic_widget_camera.xml | 15 + .../res/drawable-v31/ic_widget_clipboard.xml | 12 + .../main/res/drawable-v31/ic_widget_mic.xml | 13 + .../res/drawable-v31/ic_widget_mic_accent.xml | 13 + .../res/drawable-v31/ic_widget_photos.xml | 12 + .../res/drawable-v31/ic_widget_waveform.xml | 12 + .../res/drawable-v31/widget_background.xml | 8 + .../res/drawable-v31/widget_button_circle.xml | 11 + .../res/drawable-v31/widget_button_mic.xml | 11 + .../res/drawable-v31/widget_button_pill.xml | 12 + .../drawable-v31/widget_button_primary.xml | 12 + .../drawable-v31/widget_button_secondary.xml | 12 + android/app/src/main/res/drawable/ic_hub.xml | 13 + .../app/src/main/res/drawable/ic_sparkle.xml | 13 - .../main/res/drawable/ic_widget_camera.xml | 15 + .../main/res/drawable/ic_widget_clipboard.xml | 12 + .../src/main/res/drawable/ic_widget_mic.xml | 13 + .../res/drawable/ic_widget_mic_accent.xml | 13 + .../main/res/drawable/ic_widget_photos.xml | 12 + .../main/res/drawable/ic_widget_waveform.xml | 12 + .../main/res/drawable/widget_background.xml | 8 + .../res/drawable/widget_button_circle.xml | 11 + .../main/res/drawable/widget_button_mic.xml | 11 + .../main/res/drawable/widget_button_pill.xml | 12 + .../res/drawable/widget_button_primary.xml | 12 + .../res/drawable/widget_button_secondary.xml | 12 + .../src/main/res/drawable/widget_preview.xml | 54 +++ .../src/main/res/layout/conduit_widget.xml | 130 ++++++ .../app/src/main/res/values-night/colors.xml | 31 ++ .../app/src/main/res/values-night/dimens.xml | 10 + android/app/src/main/res/values/colors.xml | 31 ++ android/app/src/main/res/values/dimens.xml | 10 + android/app/src/main/res/values/strings.xml | 14 + .../src/main/res/xml/conduit_widget_info.xml | 18 + ...M-32423289-c69e-46b7-b4e9-78a19a092179.png | 0 ...e-8935b981-4568-4aa2-9e64-756e03aa6aaf.png | 0 ...5-d9c39cb9-8c69-4f4d-9c5b-8308d6beaa9d.png | 0 docs/android-widget-setup.md | 133 ++++++ docs/ios-widget-setup.md | 120 +++++ .../AccentColor.colorset/Contents.json | 21 + .../AppIcon.appiconset/Contents.json | 35 ++ .../Assets.xcassets/Contents.json | 7 + .../HubIcon.imageset/Contents.json | 17 + .../Assets.xcassets/HubIcon.imageset/hub.svg | 2 + .../WidgetBackground.colorset/Contents.json | 39 ++ ios/ConduitWidget/ConduitWidget.entitlements | 11 + ios/ConduitWidget/ConduitWidget.swift | 129 ++++++ ios/ConduitWidget/ConduitWidgetBundle.swift | 16 + ios/ConduitWidget/Info.plist | 12 + ios/ConduitWidgetExtension.entitlements | 10 + ios/Podfile.lock | 6 + ios/Runner.xcodeproj/project.pbxproj | 236 +++++++++- lib/core/providers/app_startup_providers.dart | 2 + lib/core/services/home_widget_service.dart | 409 ++++++++++++++++++ lib/l10n/app_en.arb | 20 + pubspec.lock | 8 + pubspec.yaml | 1 + 59 files changed, 1950 insertions(+), 14 deletions(-) create mode 100644 android/app/src/main/kotlin/app/cogwheel/conduit/ConduitWidgetProvider.kt create mode 100644 android/app/src/main/res/drawable-v31/ic_widget_camera.xml create mode 100644 android/app/src/main/res/drawable-v31/ic_widget_clipboard.xml create mode 100644 android/app/src/main/res/drawable-v31/ic_widget_mic.xml create mode 100644 android/app/src/main/res/drawable-v31/ic_widget_mic_accent.xml create mode 100644 android/app/src/main/res/drawable-v31/ic_widget_photos.xml create mode 100644 android/app/src/main/res/drawable-v31/ic_widget_waveform.xml create mode 100644 android/app/src/main/res/drawable-v31/widget_background.xml create mode 100644 android/app/src/main/res/drawable-v31/widget_button_circle.xml create mode 100644 android/app/src/main/res/drawable-v31/widget_button_mic.xml create mode 100644 android/app/src/main/res/drawable-v31/widget_button_pill.xml create mode 100644 android/app/src/main/res/drawable-v31/widget_button_primary.xml create mode 100644 android/app/src/main/res/drawable-v31/widget_button_secondary.xml create mode 100644 android/app/src/main/res/drawable/ic_hub.xml delete mode 100644 android/app/src/main/res/drawable/ic_sparkle.xml create mode 100644 android/app/src/main/res/drawable/ic_widget_camera.xml create mode 100644 android/app/src/main/res/drawable/ic_widget_clipboard.xml create mode 100644 android/app/src/main/res/drawable/ic_widget_mic.xml create mode 100644 android/app/src/main/res/drawable/ic_widget_mic_accent.xml create mode 100644 android/app/src/main/res/drawable/ic_widget_photos.xml create mode 100644 android/app/src/main/res/drawable/ic_widget_waveform.xml create mode 100644 android/app/src/main/res/drawable/widget_background.xml create mode 100644 android/app/src/main/res/drawable/widget_button_circle.xml create mode 100644 android/app/src/main/res/drawable/widget_button_mic.xml create mode 100644 android/app/src/main/res/drawable/widget_button_pill.xml create mode 100644 android/app/src/main/res/drawable/widget_button_primary.xml create mode 100644 android/app/src/main/res/drawable/widget_button_secondary.xml create mode 100644 android/app/src/main/res/drawable/widget_preview.xml create mode 100644 android/app/src/main/res/layout/conduit_widget.xml create mode 100644 android/app/src/main/res/values-night/colors.xml create mode 100644 android/app/src/main/res/values-night/dimens.xml create mode 100644 android/app/src/main/res/values/colors.xml create mode 100644 android/app/src/main/res/values/dimens.xml create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/xml/conduit_widget_info.xml create mode 100644 assets/Screenshot_2025-12-07_at_2.33.10_PM-32423289-c69e-46b7-b4e9-78a19a092179.png create mode 100644 assets/image-8935b981-4568-4aa2-9e64-756e03aa6aaf.png create mode 100644 assets/share_1567790370950729575-d9c39cb9-8c69-4f4d-9c5b-8308d6beaa9d.png create mode 100644 docs/android-widget-setup.md create mode 100644 docs/ios-widget-setup.md create mode 100644 ios/ConduitWidget/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 ios/ConduitWidget/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/ConduitWidget/Assets.xcassets/Contents.json create mode 100644 ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/Contents.json create mode 100644 ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/hub.svg create mode 100644 ios/ConduitWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 ios/ConduitWidget/ConduitWidget.entitlements create mode 100644 ios/ConduitWidget/ConduitWidget.swift create mode 100644 ios/ConduitWidget/ConduitWidgetBundle.swift create mode 100644 ios/ConduitWidget/Info.plist create mode 100644 ios/ConduitWidgetExtension.entitlements create mode 100644 lib/core/services/home_widget_service.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 0b0d227..e958c00 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -119,6 +119,19 @@ android:exported="false" android:foregroundServiceType="dataSync|microphone"/> + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable-v31/ic_widget_clipboard.xml b/android/app/src/main/res/drawable-v31/ic_widget_clipboard.xml new file mode 100644 index 0000000..699d217 --- /dev/null +++ b/android/app/src/main/res/drawable-v31/ic_widget_clipboard.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/drawable-v31/ic_widget_mic.xml b/android/app/src/main/res/drawable-v31/ic_widget_mic.xml new file mode 100644 index 0000000..2c46624 --- /dev/null +++ b/android/app/src/main/res/drawable-v31/ic_widget_mic.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/android/app/src/main/res/drawable-v31/ic_widget_mic_accent.xml b/android/app/src/main/res/drawable-v31/ic_widget_mic_accent.xml new file mode 100644 index 0000000..f43d1aa --- /dev/null +++ b/android/app/src/main/res/drawable-v31/ic_widget_mic_accent.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/android/app/src/main/res/drawable-v31/ic_widget_photos.xml b/android/app/src/main/res/drawable-v31/ic_widget_photos.xml new file mode 100644 index 0000000..0c0eb38 --- /dev/null +++ b/android/app/src/main/res/drawable-v31/ic_widget_photos.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/drawable-v31/ic_widget_waveform.xml b/android/app/src/main/res/drawable-v31/ic_widget_waveform.xml new file mode 100644 index 0000000..7cd19ac --- /dev/null +++ b/android/app/src/main/res/drawable-v31/ic_widget_waveform.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/drawable-v31/widget_background.xml b/android/app/src/main/res/drawable-v31/widget_background.xml new file mode 100644 index 0000000..7b3ea2d --- /dev/null +++ b/android/app/src/main/res/drawable-v31/widget_background.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable-v31/widget_button_circle.xml b/android/app/src/main/res/drawable-v31/widget_button_circle.xml new file mode 100644 index 0000000..fcd6a6f --- /dev/null +++ b/android/app/src/main/res/drawable-v31/widget_button_circle.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/android/app/src/main/res/drawable-v31/widget_button_mic.xml b/android/app/src/main/res/drawable-v31/widget_button_mic.xml new file mode 100644 index 0000000..7406ccc --- /dev/null +++ b/android/app/src/main/res/drawable-v31/widget_button_mic.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/android/app/src/main/res/drawable-v31/widget_button_pill.xml b/android/app/src/main/res/drawable-v31/widget_button_pill.xml new file mode 100644 index 0000000..ed12d5b --- /dev/null +++ b/android/app/src/main/res/drawable-v31/widget_button_pill.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable-v31/widget_button_primary.xml b/android/app/src/main/res/drawable-v31/widget_button_primary.xml new file mode 100644 index 0000000..bd3a856 --- /dev/null +++ b/android/app/src/main/res/drawable-v31/widget_button_primary.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable-v31/widget_button_secondary.xml b/android/app/src/main/res/drawable-v31/widget_button_secondary.xml new file mode 100644 index 0000000..4d5da13 --- /dev/null +++ b/android/app/src/main/res/drawable-v31/widget_button_secondary.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_hub.xml b/android/app/src/main/res/drawable/ic_hub.xml new file mode 100644 index 0000000..f9edfb9 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_hub.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/ic_sparkle.xml b/android/app/src/main/res/drawable/ic_sparkle.xml deleted file mode 100644 index 7700658..0000000 --- a/android/app/src/main/res/drawable/ic_sparkle.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_widget_camera.xml b/android/app/src/main/res/drawable/ic_widget_camera.xml new file mode 100644 index 0000000..7c3bfe4 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_widget_camera.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/ic_widget_clipboard.xml b/android/app/src/main/res/drawable/ic_widget_clipboard.xml new file mode 100644 index 0000000..c218dfe --- /dev/null +++ b/android/app/src/main/res/drawable/ic_widget_clipboard.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_widget_mic.xml b/android/app/src/main/res/drawable/ic_widget_mic.xml new file mode 100644 index 0000000..606827e --- /dev/null +++ b/android/app/src/main/res/drawable/ic_widget_mic.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/ic_widget_mic_accent.xml b/android/app/src/main/res/drawable/ic_widget_mic_accent.xml new file mode 100644 index 0000000..a67a4e1 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_widget_mic_accent.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/android/app/src/main/res/drawable/ic_widget_photos.xml b/android/app/src/main/res/drawable/ic_widget_photos.xml new file mode 100644 index 0000000..374226b --- /dev/null +++ b/android/app/src/main/res/drawable/ic_widget_photos.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_widget_waveform.xml b/android/app/src/main/res/drawable/ic_widget_waveform.xml new file mode 100644 index 0000000..bd11201 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_widget_waveform.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/android/app/src/main/res/drawable/widget_background.xml b/android/app/src/main/res/drawable/widget_background.xml new file mode 100644 index 0000000..0fc15eb --- /dev/null +++ b/android/app/src/main/res/drawable/widget_background.xml @@ -0,0 +1,8 @@ + + + + + + + diff --git a/android/app/src/main/res/drawable/widget_button_circle.xml b/android/app/src/main/res/drawable/widget_button_circle.xml new file mode 100644 index 0000000..a8e1010 --- /dev/null +++ b/android/app/src/main/res/drawable/widget_button_circle.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/widget_button_mic.xml b/android/app/src/main/res/drawable/widget_button_mic.xml new file mode 100644 index 0000000..b9caa78 --- /dev/null +++ b/android/app/src/main/res/drawable/widget_button_mic.xml @@ -0,0 +1,11 @@ + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/widget_button_pill.xml b/android/app/src/main/res/drawable/widget_button_pill.xml new file mode 100644 index 0000000..0f9edc9 --- /dev/null +++ b/android/app/src/main/res/drawable/widget_button_pill.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/widget_button_primary.xml b/android/app/src/main/res/drawable/widget_button_primary.xml new file mode 100644 index 0000000..08becdf --- /dev/null +++ b/android/app/src/main/res/drawable/widget_button_primary.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/widget_button_secondary.xml b/android/app/src/main/res/drawable/widget_button_secondary.xml new file mode 100644 index 0000000..192210e --- /dev/null +++ b/android/app/src/main/res/drawable/widget_button_secondary.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/widget_preview.xml b/android/app/src/main/res/drawable/widget_preview.xml new file mode 100644 index 0000000..792160e --- /dev/null +++ b/android/app/src/main/res/drawable/widget_preview.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/layout/conduit_widget.xml b/android/app/src/main/res/layout/conduit_widget.xml new file mode 100644 index 0000000..1bae06a --- /dev/null +++ b/android/app/src/main/res/layout/conduit_widget.xml @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/values-night/colors.xml b/android/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..24212ae --- /dev/null +++ b/android/app/src/main/res/values-night/colors.xml @@ -0,0 +1,31 @@ + + + + + + @android:color/system_accent1_200 + @android:color/system_accent1_400 + @android:color/system_accent1_800 + + + @android:color/system_accent1_200 + @android:color/system_accent1_800 + + + @android:color/system_accent2_700 + @android:color/system_accent2_100 + + + @android:color/system_neutral1_900 + @android:color/system_neutral2_700 + + + #D0BCFF + #9A82DB + #D0BCFF + #4A3880 + #4A4458 + #E8DEF8 + #1C1B1F + + diff --git a/android/app/src/main/res/values-night/dimens.xml b/android/app/src/main/res/values-night/dimens.xml new file mode 100644 index 0000000..81b2448 --- /dev/null +++ b/android/app/src/main/res/values-night/dimens.xml @@ -0,0 +1,10 @@ + + + + 12dp + 6dp + 24dp + 16dp + 12dp + + diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..5c0c84a --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,31 @@ + + + + + + @android:color/system_accent1_600 + @android:color/system_accent1_800 + @android:color/white + + + @android:color/system_accent1_100 + @android:color/system_accent1_700 + + + @android:color/system_accent2_100 + @android:color/system_accent1_700 + + + @android:color/system_neutral1_10 + @android:color/system_neutral2_100 + + + #6750A4 + #4A3880 + #E8DEF8 + #6750A4 + #E8DEF8 + #1D192B + #FFFBFE + + diff --git a/android/app/src/main/res/values/dimens.xml b/android/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..cfc92cc --- /dev/null +++ b/android/app/src/main/res/values/dimens.xml @@ -0,0 +1,10 @@ + + + + 12dp + 6dp + 24dp + 16dp + 12dp + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..34b5320 --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,14 @@ + + + Conduit + + + Conduit + Quick access to Conduit chat with camera, photos, and clipboard shortcuts + Ask Conduit + Camera + Photos + Clipboard + Voice + + diff --git a/android/app/src/main/res/xml/conduit_widget_info.xml b/android/app/src/main/res/xml/conduit_widget_info.xml new file mode 100644 index 0000000..b28dfbb --- /dev/null +++ b/android/app/src/main/res/xml/conduit_widget_info.xml @@ -0,0 +1,18 @@ + + + diff --git a/assets/Screenshot_2025-12-07_at_2.33.10_PM-32423289-c69e-46b7-b4e9-78a19a092179.png b/assets/Screenshot_2025-12-07_at_2.33.10_PM-32423289-c69e-46b7-b4e9-78a19a092179.png new file mode 100644 index 0000000..e69de29 diff --git a/assets/image-8935b981-4568-4aa2-9e64-756e03aa6aaf.png b/assets/image-8935b981-4568-4aa2-9e64-756e03aa6aaf.png new file mode 100644 index 0000000..e69de29 diff --git a/assets/share_1567790370950729575-d9c39cb9-8c69-4f4d-9c5b-8308d6beaa9d.png b/assets/share_1567790370950729575-d9c39cb9-8c69-4f4d-9c5b-8308d6beaa9d.png new file mode 100644 index 0000000..e69de29 diff --git a/docs/android-widget-setup.md b/docs/android-widget-setup.md new file mode 100644 index 0000000..99f0a4f --- /dev/null +++ b/docs/android-widget-setup.md @@ -0,0 +1,133 @@ +# Android Widget Setup + +The Android home screen widget is automatically included in the app build. This document describes the implementation details. + +## Overview + +The widget uses **Material 3 / Material You** design with dynamic colors on Android 12+ (API 31+). + +## Widget Design (Native Android) + +``` +┌─────────────────────────────────────┐ +│ ✨ Ask Conduit │ ← Primary color (dynamic) +│ │ +├───────────┬───────────┬─────────────┤ +│ 📷 │ 🖼️ │ 📋 │ +│ Camera │ Photos │ Clipboard │ ← Secondary container +└───────────┴───────────┴─────────────┘ +``` + +### Material You (Android 12+) + +- **Primary button**: System accent color (`system_accent1_600`) +- **Secondary buttons**: Secondary container color (`system_accent2_100`) +- **Background**: Neutral surface color (`system_neutral1_10`) +- **Icons**: Tinted with `system_accent1_700` + +### Fallback (Android 11 and below) + +- **Primary button**: Material 3 default purple (`#6750A4`) +- **Secondary buttons**: Light purple container (`#E8DEF8`) +- **Background**: Near-white surface (`#FFFBFE`) + +## Files Structure + +``` +android/app/src/main/ +├── kotlin/.../ConduitWidgetProvider.kt # Widget logic +├── res/ +│ ├── layout/ +│ │ └── conduit_widget.xml # Widget layout +│ ├── drawable/ +│ │ ├── widget_background.xml # Surface background +│ │ ├── widget_button_primary.xml # Primary button +│ │ ├── widget_button_secondary.xml # Secondary buttons +│ │ ├── ic_widget_camera.xml # Camera icon +│ │ ├── ic_widget_photos.xml # Photos icon +│ │ ├── ic_widget_clipboard.xml # Clipboard icon +│ │ └── widget_preview.xml # Widget picker preview +│ ├── drawable-v31/ # Material You overrides +│ │ └── (same files with dynamic colors) +│ ├── values/ +│ │ ├── colors.xml # Light mode colors +│ │ ├── dimens.xml # Widget dimensions +│ │ └── strings.xml # Widget strings +│ ├── values-night/ +│ │ └── colors.xml # Dark mode colors +│ └── xml/ +│ └── conduit_widget_info.xml # Widget metadata +└── AndroidManifest.xml # Widget receiver registration +``` + +## Deep Link Handling + +The widget uses `homewidget://` URL scheme: + +| Action | URL | +|--------|-----| +| New Chat | `homewidget://new_chat` | +| Camera | `homewidget://camera` | +| Photos | `homewidget://photos` | +| Clipboard | `homewidget://clipboard` | + +## Widget Configuration + +The widget is configured in `res/xml/conduit_widget_info.xml`: + +- **Min size**: 250x110dp (3x2 cells) +- **Resizable**: Horizontal and vertical +- **Category**: Home screen +- **Update period**: Never (static widget) + +## Testing + +1. Build and install the debug APK: + ```bash + flutter build apk --debug + flutter install + ``` + +2. Long press on home screen +3. Tap "Widgets" +4. Search for "Conduit" +5. Drag widget to home screen + +## Customization + +### Changing the accent color + +The widget automatically picks up the system's Material You palette on Android 12+. On older versions, modify the fallback colors in `values/colors.xml`: + +```xml +#YOUR_COLOR +``` + +### Changing widget size + +Modify `res/xml/conduit_widget_info.xml`: + +```xml + +``` + +## Troubleshooting + +### Widget not appearing + +- Ensure the app was installed (not just built) +- Try restarting the home launcher +- Check that `ConduitWidgetProvider` is registered in AndroidManifest.xml + +### Colors not updating on theme change + +- Widget colors are set at creation time +- User needs to re-add widget after theme change +- Or trigger update via `HomeWidget.updateWidget()` from Flutter + diff --git a/docs/ios-widget-setup.md b/docs/ios-widget-setup.md new file mode 100644 index 0000000..8c8bf35 --- /dev/null +++ b/docs/ios-widget-setup.md @@ -0,0 +1,120 @@ +# iOS Widget Extension Setup + +This document describes how the ConduitWidget extension was added and how to configure it. + +## Overview + +The widget extension was created via Xcode and provides quick access buttons: + +- **Ask Conduit** - Opens app with new chat, focuses composer +- **Camera** - Opens app, creates new chat, launches camera +- **Photos** - Opens app, creates new chat, opens photo picker +- **Clipboard** - Opens app with clipboard contents as prompt + +## Files Structure + +``` +ios/ConduitWidget/ +├── Assets.xcassets/ # Asset catalog +│ ├── AccentColor.colorset/ # Theme accent color +│ ├── AppIcon.appiconset/ # Widget icon (uses app icon) +│ └── WidgetBackground.colorset/ # Light/dark backgrounds +├── ConduitWidget.entitlements # App group for data sharing +├── ConduitWidget.swift # Main widget implementation +├── ConduitWidgetBundle.swift # Widget bundle entry point +└── Info.plist # Extension configuration +``` + +## App Group Configuration + +Both the main app and widget share data via the app group `group.app.cogwheel.conduit`. + +**Important:** Ensure both targets have this app group in their capabilities: + +1. Select the **Runner** target → Signing & Capabilities → App Groups +2. Verify `group.app.cogwheel.conduit` is listed + +3. Select the **ConduitWidget** target → Signing & Capabilities → App Groups +4. Verify `group.app.cogwheel.conduit` is listed + +## Deep Link Handling + +The widget uses `homewidget://` URL scheme to communicate with the Flutter app: + +| Action | URL | +|--------|-----| +| New Chat | `homewidget://new_chat` | +| Camera | `homewidget://camera` | +| Photos | `homewidget://photos` | +| Clipboard | `homewidget://clipboard` | + +These are handled by `HomeWidgetCoordinator` in the Flutter code. + +## Widget Design (Native iOS) + +``` +┌─────────────────────────────────────┐ +│ ✨ Ask Conduit │ ← System tint color +│ │ +├───────────┬───────────┬─────────────┤ +│ 📷 │ 🖼️ │ 📋 │ +│ Camera │ Photos │ Clipboard │ ← Secondary system bg +└───────────┴───────────┴─────────────┘ +``` + +- **Size**: Medium widget (systemMedium) +- **Primary button**: System tint color (follows app accent) +- **Secondary buttons**: `secondarySystemGroupedBackground` +- **Icons**: SF Symbols with hierarchical rendering +- **Typography**: SF Rounded font +- **Supports**: Light/dark mode, Dynamic Type + +## Building + +The widget extension is built automatically when you build the main app: + +```bash +flutter build ios +``` + +Or build from Xcode: + +1. Open `ios/Runner.xcworkspace` +2. Select the **Runner** scheme +3. Build (⌘B) + +## Testing + +1. Build and run the main app on a device/simulator +2. Go to home screen +3. Long press → tap **+** to add widgets +4. Search for "Conduit" +5. Add the medium widget + +## Troubleshooting + +### Widget not appearing in picker + +- Ensure the widget extension builds without errors +- Check deployment target is iOS 17.0+ +- Clean build folder (⇧⌘K) and rebuild + +### Widget actions don't work + +- Verify the `homewidget://` URL scheme is handled +- Check `HomeWidgetCoordinator` is initialized in app startup +- Ensure app group is configured on both targets + +### Widget doesn't update + +- The widget uses `.never` refresh policy (static content) +- Call `HomeWidget.updateWidget()` from Flutter to trigger refresh + +## Adding to a Fresh Clone + +If cloning the repo fresh, the widget extension should already be configured in the Xcode project. Just ensure: + +1. Team/signing is set for both targets +2. App groups capability is enabled +3. Pod install has been run + diff --git a/ios/ConduitWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/ios/ConduitWidget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..e1e799d --- /dev/null +++ b/ios/ConduitWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,21 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.929", + "green" : "0.227", + "red" : "0.486" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} + diff --git a/ios/ConduitWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/ConduitWidget/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/ios/ConduitWidget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/ConduitWidget/Assets.xcassets/Contents.json b/ios/ConduitWidget/Assets.xcassets/Contents.json new file mode 100644 index 0000000..6cc1226 --- /dev/null +++ b/ios/ConduitWidget/Assets.xcassets/Contents.json @@ -0,0 +1,7 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} + diff --git a/ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/Contents.json b/ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/Contents.json new file mode 100644 index 0000000..546f556 --- /dev/null +++ b/ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/Contents.json @@ -0,0 +1,17 @@ +{ + "images" : [ + { + "filename" : "hub.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} + diff --git a/ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/hub.svg b/ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/hub.svg new file mode 100644 index 0000000..349ae5b --- /dev/null +++ b/ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/hub.svg @@ -0,0 +1,2 @@ + + diff --git a/ios/ConduitWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/ios/ConduitWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..e9810e6 --- /dev/null +++ b/ios/ConduitWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,39 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.961", + "green" : "0.961", + "red" : "0.961" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.118", + "green" : "0.118", + "red" : "0.118" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} + diff --git a/ios/ConduitWidget/ConduitWidget.entitlements b/ios/ConduitWidget/ConduitWidget.entitlements new file mode 100644 index 0000000..3d1b11d --- /dev/null +++ b/ios/ConduitWidget/ConduitWidget.entitlements @@ -0,0 +1,11 @@ + + + + + com.apple.security.application-groups + + group.app.cogwheel.conduit + + + + diff --git a/ios/ConduitWidget/ConduitWidget.swift b/ios/ConduitWidget/ConduitWidget.swift new file mode 100644 index 0000000..5c97c16 --- /dev/null +++ b/ios/ConduitWidget/ConduitWidget.swift @@ -0,0 +1,129 @@ +// +// ConduitWidget.swift +// ConduitWidget +// +// Created by cogwheel on 07/12/25. +// + +import WidgetKit +import SwiftUI + +// MARK: - Timeline Entry + +struct ConduitEntry: TimelineEntry { + let date: Date +} + +// MARK: - Timeline Provider + +struct ConduitProvider: TimelineProvider { + func placeholder(in context: Context) -> ConduitEntry { + ConduitEntry(date: Date()) + } + + func getSnapshot(in context: Context, completion: @escaping (ConduitEntry) -> Void) { + let entry = ConduitEntry(date: Date()) + completion(entry) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + let entry = ConduitEntry(date: Date()) + let timeline = Timeline(entries: [entry], policy: .never) + completion(timeline) + } +} + +// MARK: - Widget View + +struct ConduitWidgetEntryView: View { + var entry: ConduitProvider.Entry + @Environment(\.widgetFamily) var family + @Environment(\.colorScheme) var colorScheme + + var body: some View { + VStack(spacing: 12) { + // Main "Ask Conduit" pill - ChatGPT style + Link(destination: URL(string: "homewidget://new_chat")!) { + HStack(spacing: 12) { + Image("HubIcon") + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 28, height: 28) + .foregroundStyle(.white.opacity(0.9)) + Text("Ask Conduit") + .font(.system(size: 18, weight: .medium, design: .rounded)) + .foregroundStyle(.white.opacity(0.9)) + Spacer() + } + .padding(.horizontal, 20) + .frame(maxWidth: .infinity) + .frame(height: 56) + .background( + Capsule() + .fill(.white.opacity(0.15)) + ) + } + .buttonStyle(.plain) + + // 4 circular icon buttons - ChatGPT style, fill width + HStack(spacing: 8) { + CircularIconButton(symbol: "camera", url: "homewidget://camera") + CircularIconButton(symbol: "photo.on.rectangle.angled", url: "homewidget://photos") + CircularIconButton(symbol: "waveform", url: "homewidget://mic") + CircularIconButton(symbol: "doc.on.clipboard", url: "homewidget://clipboard") + } + } + .padding(16) + } +} + +// MARK: - Circular Icon Button (ChatGPT Style) + +struct CircularIconButton: View { + let symbol: String + let url: String + + var body: some View { + Link(destination: URL(string: url)!) { + Image(systemName: symbol) + .font(.system(size: 24, weight: .medium)) + .foregroundStyle(.white.opacity(0.9)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background( + RoundedRectangle(cornerRadius: 16, style: .continuous) + .fill(.white.opacity(0.15)) + ) + } + .buttonStyle(.plain) + } +} + +// MARK: - Widget Configuration + +struct ConduitWidget: Widget { + let kind: String = "ConduitWidget" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: ConduitProvider()) { entry in + if #available(iOS 17.0, *) { + ConduitWidgetEntryView(entry: entry) + .containerBackground(.clear, for: .widget) + } else { + ConduitWidgetEntryView(entry: entry) + } + } + .configurationDisplayName("Conduit") + .description("Quick access to chat, camera, photos, and voice.") + .supportedFamilies([.systemMedium]) + .contentMarginsDisabled() + } +} + +// MARK: - Preview + +#Preview(as: .systemMedium) { + ConduitWidget() +} timeline: { + ConduitEntry(date: .now) +} diff --git a/ios/ConduitWidget/ConduitWidgetBundle.swift b/ios/ConduitWidget/ConduitWidgetBundle.swift new file mode 100644 index 0000000..f17aace --- /dev/null +++ b/ios/ConduitWidget/ConduitWidgetBundle.swift @@ -0,0 +1,16 @@ +// +// ConduitWidgetBundle.swift +// ConduitWidget +// +// Created by cogwheel on 07/12/25. +// + +import WidgetKit +import SwiftUI + +@main +struct ConduitWidgetBundle: WidgetBundle { + var body: some Widget { + ConduitWidget() + } +} diff --git a/ios/ConduitWidget/Info.plist b/ios/ConduitWidget/Info.plist new file mode 100644 index 0000000..3d2e9f2 --- /dev/null +++ b/ios/ConduitWidget/Info.plist @@ -0,0 +1,12 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + + diff --git a/ios/ConduitWidgetExtension.entitlements b/ios/ConduitWidgetExtension.entitlements new file mode 100644 index 0000000..69c95a5 --- /dev/null +++ b/ios/ConduitWidgetExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.app.cogwheel.conduit + + + diff --git a/ios/Podfile.lock b/ios/Podfile.lock index aa8f151..4b3aae7 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -54,6 +54,8 @@ PODS: - Flutter - flutter_tts (0.0.1): - Flutter + - home_widget (0.0.1): + - Flutter - image_picker_ios (0.0.1): - Flutter - onnxruntime-c (1.22.0) @@ -115,6 +117,7 @@ DEPENDENCIES: - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_tts (from `.symlinks/plugins/flutter_tts/ios`) + - home_widget (from `.symlinks/plugins/home_widget/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) @@ -162,6 +165,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/flutter_secure_storage/ios" flutter_tts: :path: ".symlinks/plugins/flutter_tts/ios" + home_widget: + :path: ".symlinks/plugins/home_widget/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" package_info_plus: @@ -208,6 +213,7 @@ SPEC CHECKSUMS: flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833 + home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 onnxruntime-c: 7f778680e96145956c0a31945f260321eed2611a onnxruntime-objc: 83d28b87525bd971259a66e153ea32b5d023de19 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index ec2ab04..8a95238 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -15,6 +15,9 @@ 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 */; }; + F15AFEC42EE5499D00A1FABB /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F15AFEC32EE5499D00A1FABB /* WidgetKit.framework */; }; + F15AFEC62EE5499D00A1FABB /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F15AFEC52EE5499D00A1FABB /* SwiftUI.framework */; }; + F15AFED12EE5499E00A1FABB /* ConduitWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F15AFEC22EE5499D00A1FABB /* ConduitWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F1DBCF1D2E601A39004C2540 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F1DBCF132E601A39004C2540 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; F1E401255BF7F4649BBEC0E4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3268AF100A34CCB865515F5F /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ @@ -27,6 +30,13 @@ remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; + F15AFECF2EE5499E00A1FABB /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = F15AFEC12EE5499D00A1FABB; + remoteInfo = ConduitWidgetExtension; + }; F1DBCF1B2E601A39004C2540 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; @@ -54,6 +64,7 @@ dstSubfolderSpec = 13; files = ( F1DBCF1D2E601A39004C2540 /* ShareExtension.appex in Embed Foundation Extensions */, + F15AFED12EE5499E00A1FABB /* ConduitWidgetExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -88,11 +99,22 @@ A2C5C28DAB99348B62D0E111 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C334EBA4AE824079ECAEE9EE /* 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 = ""; }; CF1093DCAFB438AD6653A379 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; + F15AFEC22EE5499D00A1FABB /* ConduitWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ConduitWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + F15AFEC32EE5499D00A1FABB /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + F15AFEC52EE5499D00A1FABB /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + F15AFED82EE549B700A1FABB /* ConduitWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ConduitWidgetExtension.entitlements; sourceTree = ""; }; F1DBCF132E601A39004C2540 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; F1DBCF242E601A7C004C2540 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + F15AFED52EE5499E00A1FABB /* Exceptions for "ConduitWidget" folder in "ConduitWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = F15AFEC12EE5499D00A1FABB /* ConduitWidgetExtension */; + }; F1DBCF222E601A39004C2540 /* Exceptions for "ShareExtension" folder in "ShareExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( @@ -103,6 +125,18 @@ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ + F15AFEC72EE5499D00A1FABB /* ConduitWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + F15AFED52EE5499E00A1FABB /* Exceptions for "ConduitWidget" folder in "ConduitWidgetExtension" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = ConduitWidget; + sourceTree = ""; + }; F1DBCF142E601A39004C2540 /* ShareExtension */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( @@ -134,6 +168,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F15AFEBF2EE5499D00A1FABB /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + F15AFEC62EE5499D00A1FABB /* SwiftUI.framework in Frameworks */, + F15AFEC42EE5499D00A1FABB /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; F1DBCF102E601A39004C2540 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -159,6 +202,8 @@ A2C5C28DAB99348B62D0E111 /* Pods_Runner.framework */, 3268AF100A34CCB865515F5F /* Pods_RunnerTests.framework */, 1D444AAE6AC78C530C74E51F /* Pods_ShareExtension.framework */, + F15AFEC32EE5499D00A1FABB /* WidgetKit.framework */, + F15AFEC52EE5499D00A1FABB /* SwiftUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -193,9 +238,11 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + F15AFED82EE549B700A1FABB /* ConduitWidgetExtension.entitlements */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, F1DBCF142E601A39004C2540 /* ShareExtension */, + F15AFEC72EE5499D00A1FABB /* ConduitWidget */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 8C43905FA2E52A883F49D605 /* Pods */, @@ -209,6 +256,7 @@ 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, F1DBCF132E601A39004C2540 /* ShareExtension.appex */, + F15AFEC22EE5499D00A1FABB /* ConduitWidgetExtension.appex */, ); name = Products; sourceTree = ""; @@ -269,12 +317,33 @@ ); dependencies = ( F1DBCF1C2E601A39004C2540 /* PBXTargetDependency */, + F15AFED02EE5499E00A1FABB /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + F15AFEC12EE5499D00A1FABB /* ConduitWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = F15AFED62EE5499E00A1FABB /* Build configuration list for PBXNativeTarget "ConduitWidgetExtension" */; + buildPhases = ( + F15AFEBE2EE5499D00A1FABB /* Sources */, + F15AFEBF2EE5499D00A1FABB /* Frameworks */, + F15AFEC02EE5499D00A1FABB /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + F15AFEC72EE5499D00A1FABB /* ConduitWidget */, + ); + name = ConduitWidgetExtension; + productName = ConduitWidgetExtension; + productReference = F15AFEC22EE5499D00A1FABB /* ConduitWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; F1DBCF122E601A39004C2540 /* ShareExtension */ = { isa = PBXNativeTarget; buildConfigurationList = F1DBCF232E601A39004C2540 /* Build configuration list for PBXNativeTarget "ShareExtension" */; @@ -303,7 +372,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 1640; + LastSwiftUpdateCheck = 2610; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -315,6 +384,9 @@ CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; + F15AFEC12EE5499D00A1FABB = { + CreatedOnToolsVersion = 26.1.1; + }; F1DBCF122E601A39004C2540 = { CreatedOnToolsVersion = 16.4; }; @@ -336,6 +408,7 @@ 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, F1DBCF122E601A39004C2540 /* ShareExtension */, + F15AFEC12EE5499D00A1FABB /* ConduitWidgetExtension */, ); }; /* End PBXProject section */ @@ -358,6 +431,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F15AFEC02EE5499D00A1FABB /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; F1DBCF112E601A39004C2540 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -500,6 +580,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F15AFEBE2EE5499D00A1FABB /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; F1DBCF0F2E601A39004C2540 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -515,6 +602,11 @@ target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; + F15AFED02EE5499E00A1FABB /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = F15AFEC12EE5499D00A1FABB /* ConduitWidgetExtension */; + targetProxy = F15AFECF2EE5499E00A1FABB /* PBXContainerItemProxy */; + }; F1DBCF1C2E601A39004C2540 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F1DBCF122E601A39004C2540 /* ShareExtension */; @@ -855,6 +947,138 @@ }; name = Release; }; + F15AFED22EE5499E00A1FABB /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = ConduitWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X2662V5DT2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ConduitWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ConduitWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = app.cogwheel.conduit.debug.ConduitWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + F15AFED32EE5499E00A1FABB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = ConduitWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X2662V5DT2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ConduitWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ConduitWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = app.cogwheel.conduit.ConduitWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + F15AFED42EE5499E00A1FABB /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = ConduitWidgetExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = X2662V5DT2; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = ConduitWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = ConduitWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.1; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = app.cogwheel.conduit.ConduitWidget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; F1DBCF1F2E601A39004C2540 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 81AA439A1413A5BB88E56615 /* Pods-ShareExtension.debug.xcconfig */; @@ -1023,6 +1247,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + F15AFED62EE5499E00A1FABB /* Build configuration list for PBXNativeTarget "ConduitWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F15AFED22EE5499E00A1FABB /* Debug */, + F15AFED32EE5499E00A1FABB /* Release */, + F15AFED42EE5499E00A1FABB /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; F1DBCF232E601A39004C2540 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/lib/core/providers/app_startup_providers.dart b/lib/core/providers/app_startup_providers.dart index 4ac5f66..62923b4 100644 --- a/lib/core/providers/app_startup_providers.dart +++ b/lib/core/providers/app_startup_providers.dart @@ -10,6 +10,7 @@ import '../providers/app_providers.dart'; import '../../features/auth/providers/unified_auth_providers.dart'; import '../services/navigation_service.dart'; import '../services/app_intents_service.dart'; +import '../services/home_widget_service.dart'; import '../services/quick_actions_service.dart'; import '../models/conversation.dart'; import '../services/background_streaming_handler.dart'; @@ -172,6 +173,7 @@ class AppStartupFlow extends _$AppStartupFlow { keepAlive(silentLoginCoordinatorProvider); keepAlive(appIntentCoordinatorProvider); keepAlive(quickActionsCoordinatorProvider); + keepAlive(homeWidgetCoordinatorProvider); // Kick background model loading flow (non-blocking) Future.delayed(const Duration(milliseconds: 120), () { diff --git a/lib/core/services/home_widget_service.dart b/lib/core/services/home_widget_service.dart new file mode 100644 index 0000000..2479998 --- /dev/null +++ b/lib/core/services/home_widget_service.dart @@ -0,0 +1,409 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:home_widget/home_widget.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:path/path.dart' as path; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +import '../../features/auth/providers/unified_auth_providers.dart'; +import '../../features/chat/providers/chat_providers.dart'; +import '../../features/chat/services/file_attachment_service.dart'; +import '../../shared/services/tasks/task_queue.dart'; +import '../providers/app_providers.dart'; +import '../utils/debug_logger.dart'; +import 'app_intents_service.dart'; +import 'navigation_service.dart'; + +part 'home_widget_service.g.dart'; + +/// Widget action identifiers matching native widget implementations. +class WidgetActions { + static const String newChat = 'new_chat'; + static const String mic = 'mic'; + static const String camera = 'camera'; + static const String photos = 'photos'; + static const String clipboard = 'clipboard'; +} + +/// App group identifier for iOS widget data sharing. +const String _appGroupId = 'group.app.cogwheel.conduit'; + +/// Android widget provider class name. +const String _androidWidgetName = 'ConduitWidgetProvider'; + +/// iOS widget kind identifier. +const String _iOSWidgetKind = 'ConduitWidget'; + +/// Handles home screen widget interactions for Android and iOS. +/// +/// The widget provides quick actions: +/// - New Chat: Start a fresh conversation +/// - Camera: Take a photo and attach to chat +/// - Photos: Pick from gallery and attach to chat +/// - Clipboard: Paste clipboard content as prompt +@Riverpod(keepAlive: true) +class HomeWidgetCoordinator extends _$HomeWidgetCoordinator { + StreamSubscription? _widgetClickSubscription; + Uri? _pendingWidgetAction; + + @override + FutureOr build() async { + if (kIsWeb) return; + if (!Platform.isIOS && !Platform.isAndroid) return; + + await _initialize(); + + ref.onDispose(() { + _widgetClickSubscription?.cancel(); + }); + } + + Future _initialize() async { + try { + // Set app group for iOS data sharing + if (Platform.isIOS) { + await HomeWidget.setAppGroupId(_appGroupId); + } + + // Handle widget clicks + _widgetClickSubscription = HomeWidget.widgetClicked.listen( + _handleWidgetClick, + onError: (error) { + DebugLogger.error( + 'home-widget-stream', + scope: 'widget', + error: error, + ); + }, + ); + + // Check for initial launch from widget + final initialUri = await HomeWidget.initiallyLaunchedFromHomeWidget(); + if (initialUri != null) { + DebugLogger.log( + 'Widget: Initial launch URI: $initialUri', + scope: 'widget', + ); + // Store for later processing once app is ready + _pendingWidgetAction = initialUri; + // Try to process after a delay to allow router to initialize + _processInitialWidgetAction(); + } + + DebugLogger.log('Home widget service initialized', scope: 'widget'); + } catch (error, stackTrace) { + DebugLogger.error( + 'home-widget-init', + scope: 'widget', + error: error, + stackTrace: stackTrace, + ); + } + } + + /// Process initial widget action after ensuring router is ready. + Future _processInitialWidgetAction() async { + if (_pendingWidgetAction == null) return; + + // Wait for router to be attached and app to be ready + for (var i = 0; i < 50; i++) { + // Try for up to 5 seconds + await Future.delayed(const Duration(milliseconds: 100)); + + // Check if router is available + if (NavigationService.currentRoute != null) { + DebugLogger.log( + 'Widget: Router ready, processing pending action', + scope: 'widget', + ); + final uri = _pendingWidgetAction; + _pendingWidgetAction = null; + await _handleWidgetClick(uri); + return; + } + } + + DebugLogger.log( + 'Widget: Timeout waiting for router, clearing pending action', + scope: 'widget', + ); + _pendingWidgetAction = null; + } + + Future _handleWidgetClick(Uri? uri) async { + if (uri == null) return; + + // If router isn't ready yet, store for later + if (NavigationService.currentRoute == null) { + DebugLogger.log( + 'Widget: Router not ready, storing action for later', + scope: 'widget', + ); + _pendingWidgetAction = uri; + _processInitialWidgetAction(); + return; + } + + final action = uri.host.isNotEmpty + ? uri.host + : uri.pathSegments.firstOrNull; + if (action == null || action.isEmpty) { + // Default action: open new chat + await _handleNewChat(); + return; + } + + DebugLogger.log('Widget action: $action', scope: 'widget'); + + switch (action) { + case WidgetActions.newChat: + await _handleNewChat(); + break; + case WidgetActions.mic: + await _handleMic(); + break; + case WidgetActions.camera: + await _handleCamera(); + break; + case WidgetActions.photos: + await _handlePhotos(); + break; + case WidgetActions.clipboard: + await _handleClipboard(); + break; + default: + DebugLogger.log('Unknown widget action: $action', scope: 'widget'); + await _handleNewChat(); + } + } + + Future _handleNewChat() async { + DebugLogger.log('Widget: Starting new chat', scope: 'widget'); + await _waitForNavigation(); + await ref + .read(appIntentCoordinatorProvider.notifier) + .openChatFromExternal(focusComposer: true, resetChat: true); + } + + Future _handleMic() async { + DebugLogger.log('Widget: Starting voice call', scope: 'widget'); + await _waitForNavigation(); + try { + await ref + .read(appIntentCoordinatorProvider.notifier) + .startVoiceCallFromExternal(); + } catch (error, stackTrace) { + DebugLogger.error( + 'home-widget-mic', + scope: 'widget', + error: error, + stackTrace: stackTrace, + ); + // Fall back to opening chat with focus + await ref + .read(appIntentCoordinatorProvider.notifier) + .openChatFromExternal(focusComposer: true, resetChat: true); + } + } + + Future _handleCamera() async { + DebugLogger.log('Widget: Opening camera', scope: 'widget'); + await _waitForNavigation(); + + // Navigate to chat first + await ref + .read(appIntentCoordinatorProvider.notifier) + .openChatFromExternal(focusComposer: false, resetChat: true); + + // Wait for navigation to settle + await Future.delayed(const Duration(milliseconds: 100)); + + // Check auth state + final navState = ref.read(authNavigationStateProvider); + if (navState != AuthNavigationState.authenticated) { + DebugLogger.log('Widget: Not authenticated for camera', scope: 'widget'); + return; + } + + try { + final picker = ImagePicker(); + final image = await picker.pickImage( + source: ImageSource.camera, + imageQuality: 85, + ); + + if (image != null) { + await _attachFile(File(image.path)); + } + } catch (error, stackTrace) { + DebugLogger.error( + 'home-widget-camera', + scope: 'widget', + error: error, + stackTrace: stackTrace, + ); + } + } + + Future _handlePhotos() async { + DebugLogger.log('Widget: Opening photo picker', scope: 'widget'); + await _waitForNavigation(); + + // Navigate to chat first + await ref + .read(appIntentCoordinatorProvider.notifier) + .openChatFromExternal(focusComposer: false, resetChat: true); + + // Wait for navigation to settle + await Future.delayed(const Duration(milliseconds: 100)); + + // Check auth state + final navState = ref.read(authNavigationStateProvider); + if (navState != AuthNavigationState.authenticated) { + DebugLogger.log('Widget: Not authenticated for photos', scope: 'widget'); + return; + } + + try { + final picker = ImagePicker(); + final images = await picker.pickMultiImage(imageQuality: 85); + + if (images.isNotEmpty) { + for (final image in images) { + await _attachFile(File(image.path)); + } + } + } catch (error, stackTrace) { + DebugLogger.error( + 'home-widget-photos', + scope: 'widget', + error: error, + stackTrace: stackTrace, + ); + } + } + + Future _handleClipboard() async { + DebugLogger.log('Widget: Pasting from clipboard', scope: 'widget'); + await _waitForNavigation(); + + try { + final clipboardData = await Clipboard.getData(Clipboard.kTextPlain); + final text = clipboardData?.text?.trim(); + + if (text == null || text.isEmpty) { + DebugLogger.log('Widget: Clipboard is empty', scope: 'widget'); + // Still open chat even if clipboard is empty + await ref + .read(appIntentCoordinatorProvider.notifier) + .openChatFromExternal(focusComposer: true, resetChat: true); + return; + } + + await ref + .read(appIntentCoordinatorProvider.notifier) + .openChatFromExternal( + prompt: text, + focusComposer: true, + resetChat: true, + ); + } catch (error, stackTrace) { + DebugLogger.error( + 'home-widget-clipboard', + scope: 'widget', + error: error, + stackTrace: stackTrace, + ); + // Fall back to just opening chat + await ref + .read(appIntentCoordinatorProvider.notifier) + .openChatFromExternal(focusComposer: true, resetChat: true); + } + } + + /// Wait for the navigation system to be ready. + Future _waitForNavigation() async { + // Wait for bindings to be initialized + WidgetsBinding.instance.addPostFrameCallback((_) {}); + await Future.delayed(const Duration(milliseconds: 50)); + } + + Future _attachFile(File file) async { + if (!ref.mounted) return; + + // Warm the attachment service + final _ = ref.read(fileAttachmentServiceProvider); + final notifier = ref.read(attachedFilesProvider.notifier); + final taskQueue = ref.read(taskQueueProvider.notifier); + final activeConv = ref.read(activeConversationProvider); + + final attachment = LocalAttachment( + file: file, + displayName: path.basename(file.path), + ); + + notifier.addFiles([attachment]); + + try { + await taskQueue.enqueueUploadMedia( + conversationId: activeConv?.id, + filePath: file.path, + fileName: attachment.displayName, + fileSize: await file.length(), + ); + } catch (error, stackTrace) { + DebugLogger.error( + 'home-widget-upload', + scope: 'widget', + error: error, + stackTrace: stackTrace, + ); + } + + // Focus the composer after attaching + final tick = ref.read(inputFocusTriggerProvider); + ref.read(inputFocusTriggerProvider.notifier).set(tick + 1); + } + + /// Update widget data displayed on home screen. + /// + /// Call this when app state changes that should be reflected in widget. + Future updateWidgetData() async { + if (kIsWeb) return; + if (!Platform.isIOS && !Platform.isAndroid) return; + + try { + // For now, we just trigger a widget update + // In the future, we could pass data like recent conversations + + if (Platform.isAndroid) { + await HomeWidget.updateWidget(androidName: _androidWidgetName); + } else if (Platform.isIOS) { + await HomeWidget.updateWidget(iOSName: _iOSWidgetKind); + } + + DebugLogger.log('Widget data updated', scope: 'widget'); + } catch (error, stackTrace) { + DebugLogger.error( + 'home-widget-update', + scope: 'widget', + error: error, + stackTrace: stackTrace, + ); + } + } +} + +/// Provider to trigger home widget initialization at app startup. +final homeWidgetInitializerProvider = Provider((ref) { + if (kIsWeb) return; + if (!Platform.isIOS && !Platform.isAndroid) return; + + // Initialize the coordinator which sets up widget click handling + ref.watch(homeWidgetCoordinatorProvider); +}); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index de624cd..4228679 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1787,6 +1787,26 @@ } } }, + "widgetAskConduit": "Ask Conduit", + "@widgetAskConduit": { + "description": "Main button text on the home screen widget." + }, + "widgetCamera": "Camera", + "@widgetCamera": { + "description": "Camera button label on the home screen widget." + }, + "widgetPhotos": "Photos", + "@widgetPhotos": { + "description": "Photos button label on the home screen widget." + }, + "widgetClipboard": "Clipboard", + "@widgetClipboard": { + "description": "Clipboard button label on the home screen widget." + }, + "widgetDescription": "Quick access to Conduit chat with camera, photos, and clipboard shortcuts", + "@widgetDescription": { + "description": "Description shown in the widget picker when adding the widget." + }, "mermaidPreviewUnavailable": "Mermaid preview is not available on this platform.", "@mermaidPreviewUnavailable": { "description": "Shown when Mermaid diagrams cannot be rendered on the current platform." diff --git a/pubspec.lock b/pubspec.lock index 37bc987..e0366c0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -725,6 +725,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.3" + home_widget: + dependency: "direct main" + description: + name: home_widget + sha256: "908d033514a981f829fd98213909e11a428104327be3b422718aa643ac9d084a" + url: "https://pub.dev" + source: hosted + version: "0.8.1" hotreloader: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d2fc7e9..8a64222 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -77,6 +77,7 @@ dependencies: quick_actions: 1.1.0 flutter_svg: ^2.2.3 html_unescape: ^2.0.0 + home_widget: ^0.8.1 # Clipboard functionality is available through flutter/services (part of Flutter SDK)