|
|
@@ -614,6 +614,26 @@ private struct PanelAppRowView: View {
|
|
|
|
|
|
@State private var isHovering = false
|
|
|
|
|
|
+ private var quickActions: [WidgetAction] {
|
|
|
+ let variants = WidgetTemplates.variants(for: app)
|
|
|
+ let chosen = variants.first(where: { $0.size == .medium }) ??
|
|
|
+ variants.first(where: { $0.size == .small }) ??
|
|
|
+ variants.first
|
|
|
+ guard let chosen else { return [] }
|
|
|
+
|
|
|
+ let maxActions: Int
|
|
|
+ switch chosen.size {
|
|
|
+ case .small: maxActions = 3
|
|
|
+ case .medium: maxActions = 4
|
|
|
+ case .large: maxActions = 8
|
|
|
+ }
|
|
|
+ return Array(chosen.actions.prefix(maxActions))
|
|
|
+ }
|
|
|
+
|
|
|
+ private var shouldShowQuickActions: Bool {
|
|
|
+ !app.isCreateNew && app.webURL != nil && !quickActions.isEmpty
|
|
|
+ }
|
|
|
+
|
|
|
private var pinnedIDs: Set<UUID> {
|
|
|
decodeUUIDSetGlobal(from: pinnedTileIDsData)
|
|
|
}
|
|
|
@@ -632,20 +652,18 @@ private struct PanelAppRowView: View {
|
|
|
|
|
|
var body: some View {
|
|
|
ZStack(alignment: .topTrailing) {
|
|
|
- Button(action: { onAppTap(app) }) {
|
|
|
- HStack(spacing: 14) {
|
|
|
- AppIconGlyph(app: app)
|
|
|
- Text(app.name)
|
|
|
- .font(.system(size: 18, weight: .medium))
|
|
|
- .foregroundStyle(.white.opacity(0.94))
|
|
|
- .lineLimit(1)
|
|
|
- .truncationMode(.tail)
|
|
|
- Spacer(minLength: 0)
|
|
|
- }
|
|
|
- .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
- .contentShape(Rectangle())
|
|
|
+ HStack(spacing: 14) {
|
|
|
+ AppIconGlyph(app: app)
|
|
|
+ Text(app.name)
|
|
|
+ .font(.system(size: 18, weight: .medium))
|
|
|
+ .foregroundStyle(.white.opacity(0.94))
|
|
|
+ .lineLimit(1)
|
|
|
+ .truncationMode(.tail)
|
|
|
+ Spacer(minLength: 0)
|
|
|
}
|
|
|
- .buttonStyle(.plain)
|
|
|
+ .frame(maxWidth: .infinity, alignment: .leading)
|
|
|
+ .contentShape(Rectangle())
|
|
|
+ .onTapGesture { onAppTap(app) }
|
|
|
|
|
|
if isPinned && !app.isCreateNew {
|
|
|
Image(systemName: "pin.fill")
|
|
|
@@ -666,6 +684,21 @@ private struct PanelAppRowView: View {
|
|
|
.allowsHitTesting(false)
|
|
|
}
|
|
|
|
|
|
+ if shouldShowQuickActions {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ ForEach(quickActions) { action in
|
|
|
+ PanelQuickActionButton(
|
|
|
+ action: action,
|
|
|
+ isRowHovering: isHovering,
|
|
|
+ onTap: { openWidgetAction(action) }
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
|
|
|
+ .padding(.trailing, 34)
|
|
|
+ .padding(.top, 18)
|
|
|
+ }
|
|
|
+
|
|
|
if !app.isCreateNew {
|
|
|
Menu {
|
|
|
Button(isPinned ? "Unpin" : "Pin") { togglePin() }
|
|
|
@@ -783,6 +816,25 @@ private struct PanelAppRowView: View {
|
|
|
)
|
|
|
}
|
|
|
|
|
|
+ private func openWidgetAction(_ action: WidgetAction) {
|
|
|
+ guard let base = app.webURL else { return }
|
|
|
+ let target = resolvedURL(base: base, actionPath: action.urlPath)
|
|
|
+ InAppBrowserWindowManager.shared.open(url: target, title: app.name)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func resolvedURL(base: URL, actionPath: String?) -> URL {
|
|
|
+ guard let actionPath, !actionPath.isEmpty else { return base }
|
|
|
+ if actionPath.hasPrefix("http://") || actionPath.hasPrefix("https://") {
|
|
|
+ return URL(string: actionPath) ?? base
|
|
|
+ }
|
|
|
+ if actionPath.hasPrefix("/") {
|
|
|
+ var c = URLComponents(url: base, resolvingAgainstBaseURL: false)
|
|
|
+ c?.path = actionPath
|
|
|
+ return c?.url ?? base
|
|
|
+ }
|
|
|
+ return URL(string: actionPath, relativeTo: base)?.absoluteURL ?? base
|
|
|
+ }
|
|
|
+
|
|
|
private func addToDesktop() {
|
|
|
guard let webURL = app.webURL else {
|
|
|
showAlert(title: "Unavailable", message: "Desktop shortcuts need a web address.")
|
|
|
@@ -801,6 +853,65 @@ private struct PanelAppRowView: View {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+private struct PanelQuickActionButton: View {
|
|
|
+ let action: WidgetAction
|
|
|
+ let isRowHovering: Bool
|
|
|
+ let onTap: () -> Void
|
|
|
+
|
|
|
+ @State private var isHovering = false
|
|
|
+
|
|
|
+ // Only highlight the hovered icon (not the whole row).
|
|
|
+ private var highlight: Bool { isHovering }
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Button(action: onTap) {
|
|
|
+ Image(systemName: action.systemImage)
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.82))
|
|
|
+ .frame(width: 24, height: 24)
|
|
|
+ .background(
|
|
|
+ Circle()
|
|
|
+ .fill(Color.black.opacity(highlight ? 0.48 : 0.22))
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ Circle()
|
|
|
+ .stroke(Color.white.opacity(highlight ? 0.18 : 0.10), lineWidth: 1)
|
|
|
+ )
|
|
|
+ .shadow(color: .black.opacity(highlight ? 0.28 : 0.20), radius: highlight ? 12 : 8, x: 0, y: 4)
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .scaleEffect(highlight ? 1.08 : 1.0)
|
|
|
+ .animation(.spring(response: 0.28, dampingFraction: 0.85), value: highlight)
|
|
|
+ .overlay(alignment: .top) {
|
|
|
+ Text(action.title)
|
|
|
+ .font(.system(size: 10, weight: .semibold))
|
|
|
+ .foregroundStyle(.white.opacity(0.96))
|
|
|
+ .lineLimit(1)
|
|
|
+ .fixedSize(horizontal: true, vertical: false)
|
|
|
+ .padding(.horizontal, 7)
|
|
|
+ .padding(.vertical, 3)
|
|
|
+ .background(
|
|
|
+ Capsule(style: .continuous)
|
|
|
+ .fill(Color.black.opacity(0.78))
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ Capsule(style: .continuous)
|
|
|
+ .stroke(Color.white.opacity(0.12), lineWidth: 1)
|
|
|
+ )
|
|
|
+ .shadow(color: .black.opacity(0.32), radius: 7, x: 0, y: 3)
|
|
|
+ // Keep the title close to the icon.
|
|
|
+ .offset(y: -22)
|
|
|
+ .opacity(isHovering ? 1 : 0)
|
|
|
+ .scaleEffect(isHovering ? 1 : 0.88, anchor: .bottom)
|
|
|
+ .offset(y: isHovering ? 0 : 5)
|
|
|
+ .animation(.spring(response: 0.28, dampingFraction: 0.82), value: isHovering)
|
|
|
+ .allowsHitTesting(false)
|
|
|
+ }
|
|
|
+ .onHover { isHovering = $0 }
|
|
|
+ .accessibilityLabel(action.title)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
private struct AppIconGlyph: View {
|
|
|
let app: LauncherApp
|
|
|
|