|
@@ -94,6 +94,11 @@ enum MeetWidgetMode: Hashable, Sendable {
|
|
|
case hub
|
|
case hub
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+enum ContactsWidgetMode: Hashable, Sendable {
|
|
|
|
|
+ /// Medium only: search field (opens Google Contacts with query) + Favorites / Other shortcuts.
|
|
|
|
|
+ case searchHub
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
enum NewsWidgetMode: Hashable, Sendable {
|
|
enum NewsWidgetMode: Hashable, Sendable {
|
|
|
/// Search bar plus top headlines (local sample; opens Google News in-app).
|
|
/// Search bar plus top headlines (local sample; opens Google News in-app).
|
|
|
case headlines
|
|
case headlines
|
|
@@ -161,6 +166,8 @@ enum WidgetLayoutMode: Hashable {
|
|
|
case youtube(YoutubeWidgetMode)
|
|
case youtube(YoutubeWidgetMode)
|
|
|
/// Google Meet: Calendar-backed Meet links; join/create via in-app browser.
|
|
/// Google Meet: Calendar-backed Meet links; join/create via in-app browser.
|
|
|
case meet(MeetWidgetMode)
|
|
case meet(MeetWidgetMode)
|
|
|
|
|
+ /// Google Contacts: local interactive list with search + call/message/email actions.
|
|
|
|
|
+ case contacts(ContactsWidgetMode)
|
|
|
/// Google News: search + headlines (medium) or topics + picks (large); opens in-app browser.
|
|
/// Google News: search + headlines (medium) or topics + picks (large); opens in-app browser.
|
|
|
case news(NewsWidgetMode)
|
|
case news(NewsWidgetMode)
|
|
|
/// Google Forms: large interactive dashboard; small/medium use default grids.
|
|
/// Google Forms: large interactive dashboard; small/medium use default grids.
|
|
@@ -328,17 +335,7 @@ enum WidgetTemplates {
|
|
|
]
|
|
]
|
|
|
case .contacts:
|
|
case .contacts:
|
|
|
return [
|
|
return [
|
|
|
- v("contacts_small", "Contacts", .small, false, .actionsRow, [
|
|
|
|
|
- a("contacts", "Contacts", "person.crop.circle", "/"),
|
|
|
|
|
- a("duplicates", "Duplicates", "person.2", "/duplicates"),
|
|
|
|
|
- a("labels", "Labels", "tag", "/"),
|
|
|
|
|
- ]),
|
|
|
|
|
- v("contacts_medium", "Contacts Hub", .medium, true, .actionsGrid(columns: 2), [
|
|
|
|
|
- a("contacts", "All", "person.crop.circle", "/"),
|
|
|
|
|
- a("frequent", "Frequent", "clock", "/"),
|
|
|
|
|
- a("duplicates", "Duplicates", "person.2", "/duplicates"),
|
|
|
|
|
- a("trash", "Trash", "trash", "/trash"),
|
|
|
|
|
- ]),
|
|
|
|
|
|
|
+ v("contacts_medium", "Search", .medium, false, .contacts(.searchHub), []),
|
|
|
]
|
|
]
|
|
|
case .blogger:
|
|
case .blogger:
|
|
|
return [
|
|
return [
|
|
@@ -508,7 +505,14 @@ enum WidgetTemplates {
|
|
|
|
|
|
|
|
static func variant(for app: LauncherApp, variantID: String?) -> WidgetVariant {
|
|
static func variant(for app: LauncherApp, variantID: String?) -> WidgetVariant {
|
|
|
let variants = variants(for: app)
|
|
let variants = variants(for: app)
|
|
|
- if let variantID, let match = variants.first(where: { $0.id == variantID }) {
|
|
|
|
|
|
|
+ let normalizedID: String? = {
|
|
|
|
|
+ guard let id = variantID else { return nil }
|
|
|
|
|
+ if WidgetAppProfile.from(app: app) == .contacts {
|
|
|
|
|
+ if id == "contacts_small" || id == "contacts_large" { return "contacts_medium" }
|
|
|
|
|
+ }
|
|
|
|
|
+ return id
|
|
|
|
|
+ }()
|
|
|
|
|
+ if let normalizedID, let match = variants.first(where: { $0.id == normalizedID }) {
|
|
|
return match
|
|
return match
|
|
|
}
|
|
}
|
|
|
return variants.first ?? WidgetVariant(
|
|
return variants.first ?? WidgetVariant(
|
|
@@ -575,6 +579,7 @@ extension WidgetLayoutMode {
|
|
|
case .youtube(.searchBar): true
|
|
case .youtube(.searchBar): true
|
|
|
case .youtube(.interactiveNav): true
|
|
case .youtube(.interactiveNav): true
|
|
|
case .news(_): true
|
|
case .news(_): true
|
|
|
|
|
+ case .contacts(.searchHub): true
|
|
|
case .forms(_): true
|
|
case .forms(_): true
|
|
|
case .docs(_): true
|
|
case .docs(_): true
|
|
|
case .sheets(_): true
|
|
case .sheets(_): true
|
|
@@ -634,6 +639,8 @@ extension WidgetLayoutMode {
|
|
|
case .agenda: return CGSize(width: 400, height: 280)
|
|
case .agenda: return CGSize(width: 400, height: 280)
|
|
|
case .hub: return CGSize(width: 440, height: 520)
|
|
case .hub: return CGSize(width: 440, height: 520)
|
|
|
}
|
|
}
|
|
|
|
|
+ case .contacts(.searchHub):
|
|
|
|
|
+ return CGSize(width: 380, height: 178)
|
|
|
case .news(let mode):
|
|
case .news(let mode):
|
|
|
switch mode {
|
|
switch mode {
|
|
|
case .headlines: return CGSize(width: 420, height: 340)
|
|
case .headlines: return CGSize(width: 420, height: 340)
|
|
@@ -702,6 +709,8 @@ extension WidgetLayoutMode {
|
|
|
case .agenda: return CGSize(width: 360, height: 250)
|
|
case .agenda: return CGSize(width: 360, height: 250)
|
|
|
case .hub: return CGSize(width: 380, height: 480)
|
|
case .hub: return CGSize(width: 380, height: 480)
|
|
|
}
|
|
}
|
|
|
|
|
+ case .contacts(.searchHub):
|
|
|
|
|
+ return CGSize(width: 340, height: 152)
|
|
|
case .news(let mode):
|
|
case .news(let mode):
|
|
|
switch mode {
|
|
switch mode {
|
|
|
case .headlines: return CGSize(width: 380, height: 300)
|
|
case .headlines: return CGSize(width: 380, height: 300)
|