Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 116 additions & 22 deletions README.md

Large diffs are not rendered by default.

26 changes: 23 additions & 3 deletions android/src/main/java/app/capgo/widgetkit/CapgoWidgetKit.java
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ public JSONObject completeWidgetMessage(final String messageId, final JSONObject
return message;
}

public void reloadWidgets(final String kind) {
notifyStoreChanged(null, kind);
notifyWidgetBridgeChanged(null, null, kind);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public JSONObject getPluginVersion() throws JSONException {
String versionName = "android";
try {
Expand All @@ -199,20 +204,35 @@ public JSONObject getPluginVersion() throws JSONException {
}

private void notifyStoreChanged(final String activityId) {
final Intent intent = new Intent(CapgoWidgetKitConstants.ACTION_TEMPLATE_STORE_CHANGED)
.setPackage(context.getPackageName())
.putExtra(CapgoWidgetKitConstants.EXTRA_ACTIVITY_ID, activityId);
notifyStoreChanged(activityId, null);
}

private void notifyStoreChanged(final String activityId, final String kind) {
final Intent intent = new Intent(CapgoWidgetKitConstants.ACTION_TEMPLATE_STORE_CHANGED).setPackage(context.getPackageName());
if (activityId != null) {
intent.putExtra(CapgoWidgetKitConstants.EXTRA_ACTIVITY_ID, activityId);
}
if (kind != null) {
intent.putExtra(CapgoWidgetKitConstants.EXTRA_WIDGET_KIND, kind);
}
context.sendBroadcast(intent);
}

private void notifyWidgetBridgeChanged(final String widgetId, final String messageId) {
notifyWidgetBridgeChanged(widgetId, messageId, null);
}

private void notifyWidgetBridgeChanged(final String widgetId, final String messageId, final String kind) {
final Intent intent = new Intent(CapgoWidgetKitConstants.ACTION_NATIVE_WIDGET_BRIDGE_CHANGED).setPackage(context.getPackageName());
if (widgetId != null) {
intent.putExtra(CapgoWidgetKitConstants.EXTRA_WIDGET_ID, widgetId);
}
if (messageId != null) {
intent.putExtra(CapgoWidgetKitConstants.EXTRA_MESSAGE_ID, messageId);
}
if (kind != null) {
intent.putExtra(CapgoWidgetKitConstants.EXTRA_WIDGET_KIND, kind);
}
context.sendBroadcast(intent);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public final class CapgoWidgetKitConstants {
public static final String EXTRA_ACTION_ID = "actionId";
public static final String EXTRA_SOURCE_ID = "sourceId";
public static final String EXTRA_PAYLOAD_JSON = "payloadJson";
public static final String EXTRA_WIDGET_KIND = "widgetKind";
public static final String EXTRA_WIDGET_ID = "widgetId";
public static final String EXTRA_MESSAGE_ID = "messageId";
public static final String ACTIVITY_IDS_KEY = "activityIds";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@ public void areActivitiesSupported(final PluginCall call) {

@PluginMethod
public void startTemplateActivity(final PluginCall call) {
startTemplateRecord(call);
}

@PluginMethod
public void startTemplateWidget(final PluginCall call) {
startTemplateRecord(call);
}

private void startTemplateRecord(final PluginCall call) {
final JSObject definition = call.getObject("definition");
final JSObject state = call.getObject("state");

Expand Down Expand Up @@ -347,6 +356,12 @@ public void completeWidgetMessage(final PluginCall call) {
}
}

@PluginMethod
public void reloadWidgets(final PluginCall call) {
implementation.reloadWidgets(call.getString("kind"));
call.resolve();
}

@PluginMethod
public void getPluginVersion(final PluginCall call) {
try {
Expand Down
12 changes: 10 additions & 2 deletions android/src/main/java/app/capgo/widgetkit/TemplateRuntime.java
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ static JSONArray acknowledgeEvents(final JSONArray events, final String activity

static JSONObject resolveSurface(final JSONObject record, final String surface, final long nowMs) throws JSONException {
final JSONObject layouts = record.getJSONObject("definition").getJSONObject("layouts");
final JSONObject layout = layouts.optJSONObject(surface);
final JSONObject layout = layoutForSurface(layouts, surface);
if (layout == null) {
return null;
}
Expand Down Expand Up @@ -431,7 +431,7 @@ private static JSONArray frameIdsForMutation(final JSONObject activity, final JS
return new JSONArray();
}

final JSONObject layout = activity.getJSONObject("definition").getJSONObject("layouts").optJSONObject(surface);
final JSONObject layout = layoutForSurface(activity.getJSONObject("definition").getJSONObject("layouts"), surface);
final JSONArray frames = layout != null ? TemplateJsonUtils.arrayOrEmpty(layout, "frames") : new JSONArray();
final JSONArray frameIds = new JSONArray();
for (int index = 0; index < frames.length(); index += 1) {
Expand All @@ -443,6 +443,14 @@ private static JSONArray frameIdsForMutation(final JSONObject activity, final JS
return frameIds;
}

private static JSONObject layoutForSurface(final JSONObject layouts, final String surface) {
final JSONObject layout = layouts.optJSONObject(surface);
if (layout != null) {
return layout;
}
return "homeScreen".equals(surface) ? layouts.optJSONObject("lockScreen") : null;
}

private static int indexOfString(final JSONArray values, final String target) {
for (int index = 0; index < values.length(); index += 1) {
if (target.equals(values.optString(index))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

public class TemplateSampleWidgetProvider extends AppWidgetProvider {

private static final String SURFACE_LOCK_SCREEN = "lockScreen";
private static final String SURFACE_HOME_SCREEN = "homeScreen";
private static final int PREVIEW_WIDTH_PX = 720;
private static final int PREVIEW_HEIGHT_PX = 360;

Expand Down Expand Up @@ -65,7 +65,7 @@ private RemoteViews buildViews(final Context context) {
return views;
}

final JSONObject layout = bridge.resolveLayout(activity.optString("activityId"), SURFACE_LOCK_SCREEN);
final JSONObject layout = bridge.resolveLayout(activity.optString("activityId"), SURFACE_HOME_SCREEN);
if (layout == null) {
renderEmptyState(views);
return views;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,31 @@ import CapgoWidgetKitPlugin
@main
struct ExampleWidgetBundle: WidgetBundle {
var body: some Widget {
ExampleTemplateHomeWidget()

if #available(iOS 16.2, *) {
ExampleTemplateLiveActivityWidget()
}
}
}

struct ExampleTemplateHomeWidget: Widget {
private let kind = "ExampleTemplateHomeWidget"

var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: CapgoTemplateWidgetTimelineProvider()) { entry in
CapgoTemplateHomeWidgetView(entry: entry) { layout in
ExampleTemplateSurfaceCard(layout: layout)
} placeholder: {
ExampleTemplateSurfaceCard(layout: nil)
}
}
.configurationDisplayName("Capgo Template")
.description("Home Screen widget rendered from the shared Capgo SVG template store.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}

@available(iOS 16.2, *)
struct ExampleTemplateLiveActivityWidget: Widget {
private let bridge = CapgoTemplateWidgetBridge()
Expand Down
6 changes: 3 additions & 3 deletions example-app/src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -281,11 +281,11 @@ const renderPreview = () => {
return;
}

const resolvedLayout = resolveTemplateSurface(currentActivity, 'lockScreen');
const resolvedLayout = resolveTemplateSurface(currentActivity, 'homeScreen');
if (!resolvedLayout) {
preview.innerHTML = `
<div class="preview-empty">
<p>The lock screen surface is missing from this activity.</p>
<p>The home screen surface is missing from this activity.</p>
</div>
`;
updateActionState();
Expand Down Expand Up @@ -341,7 +341,7 @@ const checkSupport = async () => {

const startTemplate = async () => {
try {
const result = await CapgoWidgetKit.startTemplateActivity(createWorkoutTemplateActivity(sampleSession()));
const result = await CapgoWidgetKit.startTemplateWidget(createWorkoutTemplateActivity(sampleSession()));
currentActivity = result.activity;
currentEvents = [];
setActivityBadge(currentActivity);
Expand Down
19 changes: 19 additions & 0 deletions example-app/widget-extension/ExampleWidgetBundle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,31 @@ import CapgoWidgetKitPlugin
@main
struct ExampleWidgetBundle: WidgetBundle {
var body: some Widget {
ExampleTemplateHomeWidget()

if #available(iOS 16.2, *) {
ExampleTemplateLiveActivityWidget()
}
}
}

struct ExampleTemplateHomeWidget: Widget {
private let kind = "ExampleTemplateHomeWidget"

var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: CapgoTemplateWidgetTimelineProvider()) { entry in
CapgoTemplateHomeWidgetView(entry: entry) { layout in
ExampleTemplateSurfaceCard(layout: layout)
} placeholder: {
ExampleTemplateSurfaceCard(layout: nil)
}
}
.configurationDisplayName("Capgo Template")
.description("Home Screen widget rendered from the shared Capgo SVG template store.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}

@available(iOS 16.2, *)
struct ExampleTemplateLiveActivityWidget: Widget {
private let bridge = CapgoTemplateWidgetBridge()
Expand Down
117 changes: 117 additions & 0 deletions ios/Sources/CapgoWidgetKitPlugin/CapgoTemplateHomeWidget.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#if canImport(SwiftUI) && canImport(WidgetKit)
import Foundation
import OSLog
import SwiftUI
import WidgetKit

@available(iOS 16.0, *)
public struct CapgoTemplateWidgetEntry: TimelineEntry, Sendable {
public let date: Date
public let layout: CapgoResolvedTemplateLayout?

public init(date: Date, layout: CapgoResolvedTemplateLayout?) {
self.date = date
self.layout = layout
}
}

@available(iOS 16.0, *)
public struct CapgoTemplateWidgetTimelineProvider: TimelineProvider {
public typealias Entry = CapgoTemplateWidgetEntry

private let surface: CapgoTemplateSurface
private let status: String?
private let bundle: Bundle
private let refreshInterval: TimeInterval
private let bridge: CapgoTemplateWidgetBridge

public init(
surface: CapgoTemplateSurface = .homeScreen,
status: String? = "active",
bundle: Bundle = .main,
refreshInterval: TimeInterval = 15 * 60,
bridge: CapgoTemplateWidgetBridge = CapgoTemplateWidgetBridge()
) {
self.surface = surface
self.status = status
self.bundle = bundle
self.refreshInterval = refreshInterval
self.bridge = bridge
}

public func placeholder(in context: Context) -> CapgoTemplateWidgetEntry {
CapgoTemplateWidgetEntry(date: Date(), layout: nil)
}

public func getSnapshot(in context: Context, completion: @escaping (CapgoTemplateWidgetEntry) -> Void) {
completion(entry(date: Date()))
}

public func getTimeline(in context: Context, completion: @escaping (Timeline<CapgoTemplateWidgetEntry>) -> Void) {
let date = Date()
let entry = entry(date: date)
let nextReload = date.addingTimeInterval(max(refreshInterval, 60))
completion(Timeline(entries: [entry], policy: .after(nextReload)))
}

private func entry(date: Date) -> CapgoTemplateWidgetEntry {
do {
let layout = try bridge.resolveLatestLayout(surface: surface, status: status, bundle: bundle, now: date)
return CapgoTemplateWidgetEntry(date: date, layout: layout)
} catch {
logTemplateHomeWidgetResolutionFailure(error, surface: surface, bundle: bundle, status: status)
return CapgoTemplateWidgetEntry(date: date, layout: nil)
}
}
}

@available(iOS 16.0, *)
public struct CapgoTemplateHomeWidgetView<SVGContent: View, Placeholder: View>: View {
private let entry: CapgoTemplateWidgetEntry
private let showsHotspots: Bool
private let svgContent: (CapgoResolvedTemplateLayout) -> SVGContent
private let placeholder: () -> Placeholder

public init(
entry: CapgoTemplateWidgetEntry,
showsHotspots: Bool = true,
@ViewBuilder svgContent: @escaping (CapgoResolvedTemplateLayout) -> SVGContent,
@ViewBuilder placeholder: @escaping () -> Placeholder
) {
self.entry = entry
self.showsHotspots = showsHotspots
self.svgContent = svgContent
self.placeholder = placeholder
}

public var body: some View {
CapgoTemplateSurfaceView(
layout: entry.layout,
showsHotspots: showsHotspots,
svgContent: svgContent,
placeholder: placeholder
)
.widgetURL(entry.layout?.openUrl.flatMap(URL.init(string:)))
}
}

@available(iOS 16.0, *)
private enum CapgoTemplateHomeWidgetLog {
static let logger = Logger(subsystem: "app.capgo.widgetkit", category: "TemplateHomeWidget")
}

@available(iOS 16.0, *)
private func logTemplateHomeWidgetResolutionFailure(
_ error: Error,
surface: CapgoTemplateSurface,
bundle: Bundle,
status: String?
) {
let statusContext = status ?? "any"
let bundleIdentifier = bundle.bundleIdentifier ?? "unknown"
let context = "status=\(statusContext) surface=\(surface.rawValue) bundle=\(bundleIdentifier)"
CapgoTemplateHomeWidgetLog.logger.error(
"Failed to resolve Home Screen widget template \(context, privacy: .public): \(error.localizedDescription, privacy: .public)"
)
}
#endif
Loading