Merge pull request #244 from cogwheel0/widget-quick-access-actions
feat(widget): Add home screen widget with quick access actions
This commit is contained in:
@@ -119,6 +119,19 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync|microphone"/>
|
||||
|
||||
<!-- Home Screen Widget -->
|
||||
<receiver
|
||||
android:name=".ConduitWidgetProvider"
|
||||
android:exported="true"
|
||||
android:label="@string/widget_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.appwidget.provider"
|
||||
android:resource="@xml/conduit_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
package app.cogwheel.conduit
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.widget.RemoteViews
|
||||
import es.antonborri.home_widget.HomeWidgetLaunchIntent
|
||||
|
||||
/**
|
||||
* Home screen widget provider for Conduit.
|
||||
*
|
||||
* Provides quick actions:
|
||||
* - New Chat: Start a fresh conversation
|
||||
* - Mic: Start voice input
|
||||
* - Camera: Take a photo and attach to chat
|
||||
* - Photos: Pick from gallery and attach to chat
|
||||
* - Clipboard: Paste clipboard content as prompt
|
||||
*/
|
||||
class ConduitWidgetProvider : AppWidgetProvider() {
|
||||
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray
|
||||
) {
|
||||
for (appWidgetId in appWidgetIds) {
|
||||
updateAppWidget(context, appWidgetManager, appWidgetId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEnabled(context: Context) {
|
||||
// Called when the first widget is created
|
||||
}
|
||||
|
||||
override fun onDisabled(context: Context) {
|
||||
// Called when the last widget is removed
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTION_NEW_CHAT = "new_chat"
|
||||
private const val ACTION_MIC = "mic"
|
||||
private const val ACTION_CAMERA = "camera"
|
||||
private const val ACTION_PHOTOS = "photos"
|
||||
private const val ACTION_CLIPBOARD = "clipboard"
|
||||
|
||||
private fun updateAppWidget(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetId: Int
|
||||
) {
|
||||
val views = RemoteViews(context.packageName, R.layout.conduit_widget)
|
||||
|
||||
// Set up click handlers using home_widget's launch intent
|
||||
views.setOnClickPendingIntent(
|
||||
R.id.widget_container,
|
||||
HomeWidgetLaunchIntent.getActivity(
|
||||
context,
|
||||
MainActivity::class.java,
|
||||
Uri.parse("homewidget://$ACTION_NEW_CHAT")
|
||||
)
|
||||
)
|
||||
views.setOnClickPendingIntent(
|
||||
R.id.btn_new_chat,
|
||||
HomeWidgetLaunchIntent.getActivity(
|
||||
context,
|
||||
MainActivity::class.java,
|
||||
Uri.parse("homewidget://$ACTION_NEW_CHAT")
|
||||
)
|
||||
)
|
||||
views.setOnClickPendingIntent(
|
||||
R.id.btn_mic,
|
||||
HomeWidgetLaunchIntent.getActivity(
|
||||
context,
|
||||
MainActivity::class.java,
|
||||
Uri.parse("homewidget://$ACTION_MIC")
|
||||
)
|
||||
)
|
||||
views.setOnClickPendingIntent(
|
||||
R.id.btn_camera,
|
||||
HomeWidgetLaunchIntent.getActivity(
|
||||
context,
|
||||
MainActivity::class.java,
|
||||
Uri.parse("homewidget://$ACTION_CAMERA")
|
||||
)
|
||||
)
|
||||
views.setOnClickPendingIntent(
|
||||
R.id.btn_photos,
|
||||
HomeWidgetLaunchIntent.getActivity(
|
||||
context,
|
||||
MainActivity::class.java,
|
||||
Uri.parse("homewidget://$ACTION_PHOTOS")
|
||||
)
|
||||
)
|
||||
views.setOnClickPendingIntent(
|
||||
R.id.btn_clipboard,
|
||||
HomeWidgetLaunchIntent.getActivity(
|
||||
context,
|
||||
MainActivity::class.java,
|
||||
Uri.parse("homewidget://$ACTION_CLIPBOARD")
|
||||
)
|
||||
)
|
||||
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
}
|
||||
}
|
||||
15
android/app/src/main/res/drawable-v31/ic_widget_camera.xml
Normal file
15
android/app/src/main/res/drawable-v31/ic_widget_camera.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_on_secondary_container">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,2L7.17,4H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2H9zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z" />
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_on_secondary_container">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,2h-4.18C14.4,0.84 13.3,0 12,0c-1.3,0 -2.4,0.84 -2.82,2H5c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,18H7v-2h7v2zM17,14H7v-2h10v2zM17,10H7V8h10v2z" />
|
||||
</vector>
|
||||
|
||||
13
android/app/src/main/res/drawable-v31/ic_widget_mic.xml
Normal file
13
android/app/src/main/res/drawable-v31/ic_widget_mic.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Material Design Mic Icon - Material You -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_on_secondary_container">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Mic icon with accent color tint - Material You -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_mic_icon">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
|
||||
</vector>
|
||||
|
||||
12
android/app/src/main/res/drawable-v31/ic_widget_photos.xml
Normal file
12
android/app/src/main/res/drawable-v31/ic_widget_photos.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_on_secondary_container">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
|
||||
</vector>
|
||||
|
||||
12
android/app/src/main/res/drawable-v31/ic_widget_waveform.xml
Normal file
12
android/app/src/main/res/drawable-v31/ic_widget_waveform.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Graphic equalizer icon for voice - Material You -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_on_secondary_container">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7,18h2V6H7v12zM11,22h2V2h-2v20zM3,14h2v-4H3v4zM15,18h2V6h-2v12zM19,10v4h2v-4h-2z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Widget background - Material You dynamic surface color -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/widget_surface" />
|
||||
<corners android:radius="@dimen/widget_corner_radius" />
|
||||
</shape>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Rounded button - Material You -->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#20FFFFFF">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_secondary_container" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
11
android/app/src/main/res/drawable-v31/widget_button_mic.xml
Normal file
11
android/app/src/main/res/drawable-v31/widget_button_mic.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Mic button inside pill - Material You lighter accent -->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#20FFFFFF">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/widget_mic_background" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
||||
12
android/app/src/main/res/drawable-v31/widget_button_pill.xml
Normal file
12
android/app/src/main/res/drawable-v31/widget_button_pill.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Pill-shaped primary button - Material You dynamic color (darker accent) -->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#40FFFFFF">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_primary_dark" />
|
||||
<corners android:radius="24dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Primary button - Material You dynamic primary color -->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#40FFFFFF">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_primary" />
|
||||
<corners android:radius="@dimen/widget_button_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Secondary button - Material You dynamic secondary container color -->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#20000000">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_secondary_container" />
|
||||
<corners android:radius="@dimen/widget_secondary_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
||||
13
android/app/src/main/res/drawable/ic_hub.xml
Normal file
13
android/app/src/main/res/drawable/ic_hub.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Material Design Hub Icon (Filled) -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="#FFFFFF">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M240,920q-50,0 -85,-35t-35,-85q0,-50 35,-85t85,-35q14,0 26,3t23,8l57,-71q-28,-31 -39,-70t-5,-78l-81,-27q-17,25 -43,40t-58,15q-50,0 -85,-35T0,380q0,-50 35,-85t85,-35q50,0 85,35t35,85v8l81,28q20,-36 53.5,-61t75.5,-32v-87q-39,-11 -64.5,-42.5T360,120q0,-50 35,-85t85,-35q50,0 85,35t35,85q0,42 -26,73.5T510,236v87q42,7 75.5,32t53.5,61l81,-28v-8q0,-50 35,-85t85,-35q50,0 85,35t35,85q0,50 -35,85t-85,35q-32,0 -58.5,-15T739,445l-81,27q6,39 -5,77.5T614,620l57,70q11,-5 23,-7.5t26,-2.5q50,0 85,35t35,85q0,50 -35,85t-85,35q-50,0 -85,-35t-35,-85q0,-20 6.5,-38.5T624,728l-57,-71q-41,23 -87.5,23T392,657l-56,71q11,15 17.5,33.5T360,800q0,50 -35,85t-85,35Z" />
|
||||
</vector>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M12,2l1.5,5.5L19,9l-5.5,1.5L12,16l-1.5,-5.5L5,9l5.5,-1.5L12,2z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,15l0.94,2.06L22,18l-2.06,0.94L19,21l-0.94,-2.06L16,18l2.06,-0.94L19,15z"/>
|
||||
</vector>
|
||||
15
android/app/src/main/res/drawable/ic_widget_camera.xml
Normal file
15
android/app/src/main/res/drawable/ic_widget_camera.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_on_secondary_container_fallback">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,12m-3.2,0a3.2,3.2 0,1 1,6.4 0a3.2,3.2 0,1 1,-6.4 0" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,2L7.17,4H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V6c0,-1.1 -0.9,-2 -2,-2h-3.17L15,2H9zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5z" />
|
||||
</vector>
|
||||
|
||||
12
android/app/src/main/res/drawable/ic_widget_clipboard.xml
Normal file
12
android/app/src/main/res/drawable/ic_widget_clipboard.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_on_secondary_container_fallback">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,2h-4.18C14.4,0.84 13.3,0 12,0c-1.3,0 -2.4,0.84 -2.82,2H5c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zM12,2c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,18H7v-2h7v2zM17,14H7v-2h10v2zM17,10H7V8h10v2z" />
|
||||
</vector>
|
||||
|
||||
13
android/app/src/main/res/drawable/ic_widget_mic.xml
Normal file
13
android/app/src/main/res/drawable/ic_widget_mic.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Material Design Mic Icon -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_on_secondary_container_fallback">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
|
||||
</vector>
|
||||
|
||||
13
android/app/src/main/res/drawable/ic_widget_mic_accent.xml
Normal file
13
android/app/src/main/res/drawable/ic_widget_mic_accent.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Mic icon with accent color tint -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_mic_icon_fallback">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,14c1.66,0 2.99,-1.34 2.99,-3L15,5c0,-1.66 -1.34,-3 -3,-3S9,3.34 9,5v6c0,1.66 1.34,3 3,3zM17.3,11c0,3 -2.54,5.1 -5.3,5.1S6.7,14 6.7,11H5c0,3.41 2.72,6.23 6,6.72V21h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
|
||||
</vector>
|
||||
|
||||
12
android/app/src/main/res/drawable/ic_widget_photos.xml
Normal file
12
android/app/src/main/res/drawable/ic_widget_photos.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_on_secondary_container_fallback">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
|
||||
</vector>
|
||||
|
||||
12
android/app/src/main/res/drawable/ic_widget_waveform.xml
Normal file
12
android/app/src/main/res/drawable/ic_widget_waveform.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Graphic equalizer icon for voice -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="@color/widget_on_secondary_container_fallback">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M7,18h2V6H7v12zM11,22h2V2h-2v20zM3,14h2v-4H3v4zM15,18h2V6h-2v12zM19,10v4h2v-4h-2z" />
|
||||
</vector>
|
||||
8
android/app/src/main/res/drawable/widget_background.xml
Normal file
8
android/app/src/main/res/drawable/widget_background.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Widget background - Material 3 surface with large corner radius -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/widget_surface_fallback" />
|
||||
<corners android:radius="@dimen/widget_corner_radius" />
|
||||
</shape>
|
||||
|
||||
11
android/app/src/main/res/drawable/widget_button_circle.xml
Normal file
11
android/app/src/main/res/drawable/widget_button_circle.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Rounded button - ChatGPT style -->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#20FFFFFF">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_secondary_container_fallback" />
|
||||
<corners android:radius="16dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
11
android/app/src/main/res/drawable/widget_button_mic.xml
Normal file
11
android/app/src/main/res/drawable/widget_button_mic.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Mic button inside pill - lighter accent background -->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#20FFFFFF">
|
||||
<item>
|
||||
<shape android:shape="oval">
|
||||
<solid android:color="@color/widget_mic_background_fallback" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
||||
12
android/app/src/main/res/drawable/widget_button_pill.xml
Normal file
12
android/app/src/main/res/drawable/widget_button_pill.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Pill-shaped primary button - Material 3 style with darker accent -->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#40FFFFFF">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_primary_dark_fallback" />
|
||||
<corners android:radius="24dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
||||
12
android/app/src/main/res/drawable/widget_button_primary.xml
Normal file
12
android/app/src/main/res/drawable/widget_button_primary.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Primary button - Material 3 filled button style -->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#40FFFFFF">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_primary_fallback" />
|
||||
<corners android:radius="@dimen/widget_button_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Secondary button - Material 3 tonal button style -->
|
||||
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:color="#20000000">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_secondary_container_fallback" />
|
||||
<corners android:radius="@dimen/widget_secondary_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
||||
54
android/app/src/main/res/drawable/widget_preview.xml
Normal file
54
android/app/src/main/res/drawable/widget_preview.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Widget preview shown in the widget picker - Material 3 style -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- Background -->
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_surface_fallback" />
|
||||
<corners android:radius="@dimen/widget_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
<!-- Primary button preview -->
|
||||
<item
|
||||
android:left="16dp"
|
||||
android:top="16dp"
|
||||
android:right="16dp"
|
||||
android:bottom="72dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_primary_fallback" />
|
||||
<corners android:radius="@dimen/widget_button_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
<!-- Secondary buttons preview -->
|
||||
<item
|
||||
android:left="16dp"
|
||||
android:top="80dp"
|
||||
android:right="176dp"
|
||||
android:bottom="16dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_secondary_container_fallback" />
|
||||
<corners android:radius="@dimen/widget_secondary_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:left="96dp"
|
||||
android:top="80dp"
|
||||
android:right="96dp"
|
||||
android:bottom="16dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_secondary_container_fallback" />
|
||||
<corners android:radius="@dimen/widget_secondary_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:left="176dp"
|
||||
android:top="80dp"
|
||||
android:right="16dp"
|
||||
android:bottom="16dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/widget_secondary_container_fallback" />
|
||||
<corners android:radius="@dimen/widget_secondary_corner_radius" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
|
||||
130
android/app/src/main/res/layout/conduit_widget.xml
Normal file
130
android/app/src/main/res/layout/conduit_widget.xml
Normal file
@@ -0,0 +1,130 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- ChatGPT-style widget design -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/widget_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:background="@drawable/widget_background"
|
||||
android:clipToOutline="true">
|
||||
|
||||
<!-- Main "Ask Conduit" Pill -->
|
||||
<LinearLayout
|
||||
android:id="@+id/btn_new_chat"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:background="@drawable/widget_button_pill"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:layout_marginBottom="12dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:src="@drawable/ic_hub"
|
||||
android:importantForAccessibility="no"
|
||||
android:layout_marginEnd="12dp"
|
||||
tools:ignore="UseAppTint" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/widget_ask_conduit"
|
||||
android:textColor="@color/widget_on_primary"
|
||||
android:textSize="18sp"
|
||||
android:fontFamily="sans-serif-medium" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 4 Circular Icon Buttons Row -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<!-- Camera Button -->
|
||||
<FrameLayout
|
||||
android:id="@+id/btn_camera"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:background="@drawable/widget_button_circle">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="26dp"
|
||||
android:layout_height="26dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_widget_camera"
|
||||
android:importantForAccessibility="no"
|
||||
tools:ignore="UseAppTint" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Photos Button -->
|
||||
<FrameLayout
|
||||
android:id="@+id/btn_photos"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="6dp"
|
||||
android:background="@drawable/widget_button_circle">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="26dp"
|
||||
android:layout_height="26dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_widget_photos"
|
||||
android:importantForAccessibility="no"
|
||||
tools:ignore="UseAppTint" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Voice/Mic Button -->
|
||||
<FrameLayout
|
||||
android:id="@+id/btn_mic"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="6dp"
|
||||
android:background="@drawable/widget_button_circle">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="26dp"
|
||||
android:layout_height="26dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_widget_waveform"
|
||||
android:importantForAccessibility="no"
|
||||
tools:ignore="UseAppTint" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<!-- Clipboard Button -->
|
||||
<FrameLayout
|
||||
android:id="@+id/btn_clipboard"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="6dp"
|
||||
android:background="@drawable/widget_button_circle">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="26dp"
|
||||
android:layout_height="26dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_widget_clipboard"
|
||||
android:importantForAccessibility="no"
|
||||
tools:ignore="UseAppTint" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
31
android/app/src/main/res/values-night/colors.xml
Normal file
31
android/app/src/main/res/values-night/colors.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Material 3 Widget Colors (Dark Mode) -->
|
||||
|
||||
<!-- Primary colors for main button -->
|
||||
<color name="widget_primary">@android:color/system_accent1_200</color>
|
||||
<color name="widget_primary_dark">@android:color/system_accent1_400</color>
|
||||
<color name="widget_on_primary">@android:color/system_accent1_800</color>
|
||||
|
||||
<!-- Mic button colors (lighter accent inside pill) -->
|
||||
<color name="widget_mic_background">@android:color/system_accent1_200</color>
|
||||
<color name="widget_mic_icon">@android:color/system_accent1_800</color>
|
||||
|
||||
<!-- Secondary container colors for action buttons -->
|
||||
<color name="widget_secondary_container">@android:color/system_accent2_700</color>
|
||||
<color name="widget_on_secondary_container">@android:color/system_accent2_100</color>
|
||||
|
||||
<!-- Surface colors for widget background -->
|
||||
<color name="widget_surface">@android:color/system_neutral1_900</color>
|
||||
<color name="widget_surface_variant">@android:color/system_neutral2_700</color>
|
||||
|
||||
<!-- Fallback colors for pre-Android 12 -->
|
||||
<color name="widget_primary_fallback">#D0BCFF</color>
|
||||
<color name="widget_primary_dark_fallback">#9A82DB</color>
|
||||
<color name="widget_mic_background_fallback">#D0BCFF</color>
|
||||
<color name="widget_mic_icon_fallback">#4A3880</color>
|
||||
<color name="widget_secondary_container_fallback">#4A4458</color>
|
||||
<color name="widget_on_secondary_container_fallback">#E8DEF8</color>
|
||||
<color name="widget_surface_fallback">#1C1B1F</color>
|
||||
</resources>
|
||||
|
||||
10
android/app/src/main/res/values-night/dimens.xml
Normal file
10
android/app/src/main/res/values-night/dimens.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Widget dimensions (same for both modes) -->
|
||||
<dimen name="widget_padding">12dp</dimen>
|
||||
<dimen name="widget_button_spacing">6dp</dimen>
|
||||
<dimen name="widget_corner_radius">24dp</dimen>
|
||||
<dimen name="widget_button_corner_radius">16dp</dimen>
|
||||
<dimen name="widget_secondary_corner_radius">12dp</dimen>
|
||||
</resources>
|
||||
|
||||
31
android/app/src/main/res/values/colors.xml
Normal file
31
android/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Material 3 Widget Colors (Light Mode) -->
|
||||
|
||||
<!-- Primary colors for main button -->
|
||||
<color name="widget_primary">@android:color/system_accent1_600</color>
|
||||
<color name="widget_primary_dark">@android:color/system_accent1_800</color>
|
||||
<color name="widget_on_primary">@android:color/white</color>
|
||||
|
||||
<!-- Mic button colors (lighter accent inside pill) -->
|
||||
<color name="widget_mic_background">@android:color/system_accent1_100</color>
|
||||
<color name="widget_mic_icon">@android:color/system_accent1_700</color>
|
||||
|
||||
<!-- Secondary container colors for action buttons -->
|
||||
<color name="widget_secondary_container">@android:color/system_accent2_100</color>
|
||||
<color name="widget_on_secondary_container">@android:color/system_accent1_700</color>
|
||||
|
||||
<!-- Surface colors for widget background -->
|
||||
<color name="widget_surface">@android:color/system_neutral1_10</color>
|
||||
<color name="widget_surface_variant">@android:color/system_neutral2_100</color>
|
||||
|
||||
<!-- Fallback colors for pre-Android 12 -->
|
||||
<color name="widget_primary_fallback">#6750A4</color>
|
||||
<color name="widget_primary_dark_fallback">#4A3880</color>
|
||||
<color name="widget_mic_background_fallback">#E8DEF8</color>
|
||||
<color name="widget_mic_icon_fallback">#6750A4</color>
|
||||
<color name="widget_secondary_container_fallback">#E8DEF8</color>
|
||||
<color name="widget_on_secondary_container_fallback">#1D192B</color>
|
||||
<color name="widget_surface_fallback">#FFFBFE</color>
|
||||
</resources>
|
||||
|
||||
10
android/app/src/main/res/values/dimens.xml
Normal file
10
android/app/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Widget dimensions following Material 3 guidelines -->
|
||||
<dimen name="widget_padding">12dp</dimen>
|
||||
<dimen name="widget_button_spacing">6dp</dimen>
|
||||
<dimen name="widget_corner_radius">24dp</dimen>
|
||||
<dimen name="widget_button_corner_radius">16dp</dimen>
|
||||
<dimen name="widget_secondary_corner_radius">12dp</dimen>
|
||||
</resources>
|
||||
|
||||
14
android/app/src/main/res/values/strings.xml
Normal file
14
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Conduit</string>
|
||||
|
||||
<!-- Widget strings -->
|
||||
<string name="widget_name">Conduit</string>
|
||||
<string name="widget_description">Quick access to Conduit chat with camera, photos, and clipboard shortcuts</string>
|
||||
<string name="widget_ask_conduit">Ask Conduit</string>
|
||||
<string name="widget_camera">Camera</string>
|
||||
<string name="widget_photos">Photos</string>
|
||||
<string name="widget_clipboard">Clipboard</string>
|
||||
<string name="widget_mic">Voice</string>
|
||||
</resources>
|
||||
|
||||
18
android/app/src/main/res/xml/conduit_widget_info.xml
Normal file
18
android/app/src/main/res/xml/conduit_widget_info.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="110dp"
|
||||
android:targetCellWidth="3"
|
||||
android:targetCellHeight="2"
|
||||
android:minResizeWidth="180dp"
|
||||
android:minResizeHeight="110dp"
|
||||
android:maxResizeWidth="400dp"
|
||||
android:maxResizeHeight="200dp"
|
||||
android:updatePeriodMillis="0"
|
||||
android:initialLayout="@layout/conduit_widget"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:previewImage="@drawable/widget_preview"
|
||||
android:description="@string/widget_description"
|
||||
android:widgetFeatures="reconfigurable" />
|
||||
|
||||
133
docs/android-widget-setup.md
Normal file
133
docs/android-widget-setup.md
Normal file
@@ -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
|
||||
<color name="widget_primary_fallback">#YOUR_COLOR</color>
|
||||
```
|
||||
|
||||
### Changing widget size
|
||||
|
||||
Modify `res/xml/conduit_widget_info.xml`:
|
||||
|
||||
```xml
|
||||
<appwidget-provider
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="110dp"
|
||||
android:targetCellWidth="3"
|
||||
android:targetCellHeight="2"
|
||||
...
|
||||
/>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
120
docs/ios-widget-setup.md
Normal file
120
docs/ios-widget-setup.md
Normal file
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
7
ios/ConduitWidget/Assets.xcassets/Contents.json
Normal file
7
ios/ConduitWidget/Assets.xcassets/Contents.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
||||
17
ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/Contents.json
vendored
Normal file
17
ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "hub.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"preserves-vector-representation" : true,
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
||||
2
ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/hub.svg
vendored
Normal file
2
ios/ConduitWidget/Assets.xcassets/HubIcon.imageset/hub.svg
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M240-40q-50 0-85-35t-35-85q0-50 35-85t85-35q14 0 26 3t23 8l57-71q-28-31-39-70t-5-78l-81-27q-17 25-43 40t-58 15q-50 0-85-35T0-580q0-50 35-85t85-35q50 0 85 35t35 85v8l81 28q20-36 53.5-61t75.5-32v-87q-39-11-64.5-42.5T360-840q0-50 35-85t85-35q50 0 85 35t35 85q0 42-26 73.5T510-724v87q42 7 75.5 32t53.5 61l81-28v-8q0-50 35-85t85-35q50 0 85 35t35 85q0 50-35 85t-85 35q-32 0-58.5-15T739-515l-81 27q6 39-5 77.5T614-340l57 70q11-5 23-7.5t26-2.5q50 0 85 35t35 85q0 50-35 85t-85 35q-50 0-85-35t-35-85q0-20 6.5-38.5T624-232l-57-71q-41 23-87.5 23T392-303l-56 71q11 15 17.5 33.5T360-160q0 50-35 85t-85 35Z" fill="currentColor"/></svg>
|
||||
|
||||
|
After Width: | Height: | Size: 719 B |
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
11
ios/ConduitWidget/ConduitWidget.entitlements
Normal file
11
ios/ConduitWidget/ConduitWidget.entitlements
Normal file
@@ -0,0 +1,11 @@
|
||||
<?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>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.cogwheel.conduit</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
129
ios/ConduitWidget/ConduitWidget.swift
Normal file
129
ios/ConduitWidget/ConduitWidget.swift
Normal file
@@ -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<ConduitEntry>) -> 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)
|
||||
}
|
||||
16
ios/ConduitWidget/ConduitWidgetBundle.swift
Normal file
16
ios/ConduitWidget/ConduitWidgetBundle.swift
Normal file
@@ -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()
|
||||
}
|
||||
}
|
||||
12
ios/ConduitWidget/Info.plist
Normal file
12
ios/ConduitWidget/Info.plist
Normal file
@@ -0,0 +1,12 @@
|
||||
<?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>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
10
ios/ConduitWidgetExtension.entitlements
Normal file
10
ios/ConduitWidgetExtension.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?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>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.app.cogwheel.conduit</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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
|
||||
|
||||
@@ -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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
@@ -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 = "<group>";
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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<void>.delayed(const Duration(milliseconds: 120), () {
|
||||
|
||||
409
lib/core/services/home_widget_service.dart
Normal file
409
lib/core/services/home_widget_service.dart
Normal file
@@ -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<Uri?>? _widgetClickSubscription;
|
||||
Uri? _pendingWidgetAction;
|
||||
|
||||
@override
|
||||
FutureOr<void> build() async {
|
||||
if (kIsWeb) return;
|
||||
if (!Platform.isIOS && !Platform.isAndroid) return;
|
||||
|
||||
await _initialize();
|
||||
|
||||
ref.onDispose(() {
|
||||
_widgetClickSubscription?.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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<void>.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<void> _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<void> _handleNewChat() async {
|
||||
DebugLogger.log('Widget: Starting new chat', scope: 'widget');
|
||||
await _waitForNavigation();
|
||||
await ref
|
||||
.read(appIntentCoordinatorProvider.notifier)
|
||||
.openChatFromExternal(focusComposer: true, resetChat: true);
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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<void>.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<void> _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<void>.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<void> _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<void> _waitForNavigation() async {
|
||||
// Wait for bindings to be initialized
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {});
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
}
|
||||
|
||||
Future<void> _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<void> 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<void>((ref) {
|
||||
if (kIsWeb) return;
|
||||
if (!Platform.isIOS && !Platform.isAndroid) return;
|
||||
|
||||
// Initialize the coordinator which sets up widget click handling
|
||||
ref.watch(homeWidgetCoordinatorProvider);
|
||||
});
|
||||
@@ -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."
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user