feat(widget): Add home screen widget with quick access actions
This commit is contained in:
@@ -119,6 +119,19 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync|microphone"/>
|
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.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<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
|
||||||
- flutter_tts (0.0.1):
|
- flutter_tts (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- home_widget (0.0.1):
|
||||||
|
- Flutter
|
||||||
- image_picker_ios (0.0.1):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- onnxruntime-c (1.22.0)
|
- onnxruntime-c (1.22.0)
|
||||||
@@ -115,6 +117,7 @@ DEPENDENCIES:
|
|||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- flutter_tts (from `.symlinks/plugins/flutter_tts/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`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
@@ -162,6 +165,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
flutter_tts:
|
flutter_tts:
|
||||||
:path: ".symlinks/plugins/flutter_tts/ios"
|
:path: ".symlinks/plugins/flutter_tts/ios"
|
||||||
|
home_widget:
|
||||||
|
:path: ".symlinks/plugins/home_widget/ios"
|
||||||
image_picker_ios:
|
image_picker_ios:
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
@@ -208,6 +213,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||||
flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
|
flutter_tts: b88dbc8655d3dc961bc4a796e4e16a4cc1795833
|
||||||
|
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
||||||
onnxruntime-c: 7f778680e96145956c0a31945f260321eed2611a
|
onnxruntime-c: 7f778680e96145956c0a31945f260321eed2611a
|
||||||
onnxruntime-objc: 83d28b87525bd971259a66e153ea32b5d023de19
|
onnxruntime-objc: 83d28b87525bd971259a66e153ea32b5d023de19
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
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, ); }; };
|
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 */; };
|
F1E401255BF7F4649BBEC0E4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3268AF100A34CCB865515F5F /* Pods_RunnerTests.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
@@ -27,6 +30,13 @@
|
|||||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
remoteInfo = Runner;
|
remoteInfo = Runner;
|
||||||
};
|
};
|
||||||
|
F15AFECF2EE5499E00A1FABB /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = F15AFEC12EE5499D00A1FABB;
|
||||||
|
remoteInfo = ConduitWidgetExtension;
|
||||||
|
};
|
||||||
F1DBCF1B2E601A39004C2540 /* PBXContainerItemProxy */ = {
|
F1DBCF1B2E601A39004C2540 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
@@ -54,6 +64,7 @@
|
|||||||
dstSubfolderSpec = 13;
|
dstSubfolderSpec = 13;
|
||||||
files = (
|
files = (
|
||||||
F1DBCF1D2E601A39004C2540 /* ShareExtension.appex in Embed Foundation Extensions */,
|
F1DBCF1D2E601A39004C2540 /* ShareExtension.appex in Embed Foundation Extensions */,
|
||||||
|
F15AFED12EE5499E00A1FABB /* ConduitWidgetExtension.appex in Embed Foundation Extensions */,
|
||||||
);
|
);
|
||||||
name = "Embed Foundation Extensions";
|
name = "Embed Foundation Extensions";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
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; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
F1DBCF242E601A7C004C2540 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet 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 */ = {
|
F1DBCF222E601A39004C2540 /* Exceptions for "ShareExtension" folder in "ShareExtension" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
@@ -103,6 +125,18 @@
|
|||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup 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 */ = {
|
F1DBCF142E601A39004C2540 /* ShareExtension */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
exceptions = (
|
||||||
@@ -134,6 +168,15 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
F15AFEBF2EE5499D00A1FABB /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
F15AFEC62EE5499D00A1FABB /* SwiftUI.framework in Frameworks */,
|
||||||
|
F15AFEC42EE5499D00A1FABB /* WidgetKit.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
F1DBCF102E601A39004C2540 /* Frameworks */ = {
|
F1DBCF102E601A39004C2540 /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -159,6 +202,8 @@
|
|||||||
A2C5C28DAB99348B62D0E111 /* Pods_Runner.framework */,
|
A2C5C28DAB99348B62D0E111 /* Pods_Runner.framework */,
|
||||||
3268AF100A34CCB865515F5F /* Pods_RunnerTests.framework */,
|
3268AF100A34CCB865515F5F /* Pods_RunnerTests.framework */,
|
||||||
1D444AAE6AC78C530C74E51F /* Pods_ShareExtension.framework */,
|
1D444AAE6AC78C530C74E51F /* Pods_ShareExtension.framework */,
|
||||||
|
F15AFEC32EE5499D00A1FABB /* WidgetKit.framework */,
|
||||||
|
F15AFEC52EE5499D00A1FABB /* SwiftUI.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -193,9 +238,11 @@
|
|||||||
97C146E51CF9000F007C117D = {
|
97C146E51CF9000F007C117D = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
F15AFED82EE549B700A1FABB /* ConduitWidgetExtension.entitlements */,
|
||||||
9740EEB11CF90186004384FC /* Flutter */,
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
F1DBCF142E601A39004C2540 /* ShareExtension */,
|
F1DBCF142E601A39004C2540 /* ShareExtension */,
|
||||||
|
F15AFEC72EE5499D00A1FABB /* ConduitWidget */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
8C43905FA2E52A883F49D605 /* Pods */,
|
8C43905FA2E52A883F49D605 /* Pods */,
|
||||||
@@ -209,6 +256,7 @@
|
|||||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
F1DBCF132E601A39004C2540 /* ShareExtension.appex */,
|
F1DBCF132E601A39004C2540 /* ShareExtension.appex */,
|
||||||
|
F15AFEC22EE5499D00A1FABB /* ConduitWidgetExtension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -269,12 +317,33 @@
|
|||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
F1DBCF1C2E601A39004C2540 /* PBXTargetDependency */,
|
F1DBCF1C2E601A39004C2540 /* PBXTargetDependency */,
|
||||||
|
F15AFED02EE5499E00A1FABB /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
productType = "com.apple.product-type.application";
|
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 */ = {
|
F1DBCF122E601A39004C2540 /* ShareExtension */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = F1DBCF232E601A39004C2540 /* Build configuration list for PBXNativeTarget "ShareExtension" */;
|
buildConfigurationList = F1DBCF232E601A39004C2540 /* Build configuration list for PBXNativeTarget "ShareExtension" */;
|
||||||
@@ -303,7 +372,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 1640;
|
LastSwiftUpdateCheck = 2610;
|
||||||
LastUpgradeCheck = 1510;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
@@ -315,6 +384,9 @@
|
|||||||
CreatedOnToolsVersion = 7.3.1;
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
LastSwiftMigration = 1100;
|
LastSwiftMigration = 1100;
|
||||||
};
|
};
|
||||||
|
F15AFEC12EE5499D00A1FABB = {
|
||||||
|
CreatedOnToolsVersion = 26.1.1;
|
||||||
|
};
|
||||||
F1DBCF122E601A39004C2540 = {
|
F1DBCF122E601A39004C2540 = {
|
||||||
CreatedOnToolsVersion = 16.4;
|
CreatedOnToolsVersion = 16.4;
|
||||||
};
|
};
|
||||||
@@ -336,6 +408,7 @@
|
|||||||
97C146ED1CF9000F007C117D /* Runner */,
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||||
F1DBCF122E601A39004C2540 /* ShareExtension */,
|
F1DBCF122E601A39004C2540 /* ShareExtension */,
|
||||||
|
F15AFEC12EE5499D00A1FABB /* ConduitWidgetExtension */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -358,6 +431,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
F15AFEC02EE5499D00A1FABB /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
F1DBCF112E601A39004C2540 /* Resources */ = {
|
F1DBCF112E601A39004C2540 /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -500,6 +580,13 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
F15AFEBE2EE5499D00A1FABB /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
F1DBCF0F2E601A39004C2540 /* Sources */ = {
|
F1DBCF0F2E601A39004C2540 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -515,6 +602,11 @@
|
|||||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
|
F15AFED02EE5499E00A1FABB /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = F15AFEC12EE5499D00A1FABB /* ConduitWidgetExtension */;
|
||||||
|
targetProxy = F15AFECF2EE5499E00A1FABB /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
F1DBCF1C2E601A39004C2540 /* PBXTargetDependency */ = {
|
F1DBCF1C2E601A39004C2540 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = F1DBCF122E601A39004C2540 /* ShareExtension */;
|
target = F1DBCF122E601A39004C2540 /* ShareExtension */;
|
||||||
@@ -855,6 +947,138 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
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 */ = {
|
F1DBCF1F2E601A39004C2540 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 81AA439A1413A5BB88E56615 /* Pods-ShareExtension.debug.xcconfig */;
|
baseConfigurationReference = 81AA439A1413A5BB88E56615 /* Pods-ShareExtension.debug.xcconfig */;
|
||||||
@@ -1023,6 +1247,16 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
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" */ = {
|
F1DBCF232E601A39004C2540 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../providers/app_providers.dart';
|
|||||||
import '../../features/auth/providers/unified_auth_providers.dart';
|
import '../../features/auth/providers/unified_auth_providers.dart';
|
||||||
import '../services/navigation_service.dart';
|
import '../services/navigation_service.dart';
|
||||||
import '../services/app_intents_service.dart';
|
import '../services/app_intents_service.dart';
|
||||||
|
import '../services/home_widget_service.dart';
|
||||||
import '../services/quick_actions_service.dart';
|
import '../services/quick_actions_service.dart';
|
||||||
import '../models/conversation.dart';
|
import '../models/conversation.dart';
|
||||||
import '../services/background_streaming_handler.dart';
|
import '../services/background_streaming_handler.dart';
|
||||||
@@ -172,6 +173,7 @@ class AppStartupFlow extends _$AppStartupFlow {
|
|||||||
keepAlive(silentLoginCoordinatorProvider);
|
keepAlive(silentLoginCoordinatorProvider);
|
||||||
keepAlive(appIntentCoordinatorProvider);
|
keepAlive(appIntentCoordinatorProvider);
|
||||||
keepAlive(quickActionsCoordinatorProvider);
|
keepAlive(quickActionsCoordinatorProvider);
|
||||||
|
keepAlive(homeWidgetCoordinatorProvider);
|
||||||
|
|
||||||
// Kick background model loading flow (non-blocking)
|
// Kick background model loading flow (non-blocking)
|
||||||
Future<void>.delayed(const Duration(milliseconds: 120), () {
|
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": "Mermaid preview is not available on this platform.",
|
||||||
"@mermaidPreviewUnavailable": {
|
"@mermaidPreviewUnavailable": {
|
||||||
"description": "Shown when Mermaid diagrams cannot be rendered on the current platform."
|
"description": "Shown when Mermaid diagrams cannot be rendered on the current platform."
|
||||||
|
|||||||
@@ -725,6 +725,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.3"
|
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:
|
hotreloader:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ dependencies:
|
|||||||
quick_actions: 1.1.0
|
quick_actions: 1.1.0
|
||||||
flutter_svg: ^2.2.3
|
flutter_svg: ^2.2.3
|
||||||
html_unescape: ^2.0.0
|
html_unescape: ^2.0.0
|
||||||
|
home_widget: ^0.8.1
|
||||||
|
|
||||||
# Clipboard functionality is available through flutter/services (part of Flutter SDK)
|
# Clipboard functionality is available through flutter/services (part of Flutter SDK)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user