|
|
@@ -416,6 +416,7 @@ private struct IconPanelView: View {
|
|
|
@Binding var draggedAppID: UUID?
|
|
|
@Binding var lastDropDestinationAppID: UUID?
|
|
|
let onMove: (UUID, UUID) -> Void
|
|
|
+ @AppStorage("pinnedTileIDsData") private var pinnedTileIDsData = ""
|
|
|
|
|
|
private let actions = [
|
|
|
"house", "magnifyingglass", "target", "map", "heart", "person.2", "clock",
|
|
|
@@ -427,7 +428,9 @@ private struct IconPanelView: View {
|
|
|
HStack(alignment: .top, spacing: 20) {
|
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
|
ForEach(apps) { app in
|
|
|
- Button(action: { onAppTap(app) }) {
|
|
|
+ HStack(spacing: 14) {
|
|
|
+ // Tap-to-open area (kept separate from the pin menu so
|
|
|
+ // clicking "Pin/Unpin" doesn't also trigger open).
|
|
|
HStack(spacing: 14) {
|
|
|
AppIconGlyph(app: app)
|
|
|
Text(app.name)
|
|
|
@@ -435,13 +438,15 @@ private struct IconPanelView: View {
|
|
|
.foregroundStyle(.white.opacity(0.94))
|
|
|
.lineLimit(1)
|
|
|
.truncationMode(.tail)
|
|
|
- Spacer(minLength: 0)
|
|
|
}
|
|
|
- .padding(.vertical, 3)
|
|
|
+ .contentShape(Rectangle())
|
|
|
+ .onTapGesture { onAppTap(app) }
|
|
|
+
|
|
|
+ Spacer(minLength: 0)
|
|
|
}
|
|
|
- .buttonStyle(.plain)
|
|
|
+ .padding(.vertical, 3)
|
|
|
.onDrag {
|
|
|
- // Used by the reorder drop delegate (no need to parse the drop payload).
|
|
|
+ // Used by the reorder drop delegate.
|
|
|
draggedAppID = app.id
|
|
|
lastDropDestinationAppID = nil
|
|
|
return NSItemProvider(object: app.id.uuidString as NSString)
|
|
|
@@ -462,14 +467,30 @@ private struct IconPanelView: View {
|
|
|
|
|
|
VStack(alignment: .trailing, spacing: 14) {
|
|
|
ForEach(Array(apps.enumerated()), id: \.element.id) { index, _ in
|
|
|
- HStack(spacing: 12) {
|
|
|
- ForEach(0..<8, id: \.self) { col in
|
|
|
- let symbol = actions[(index * 3 + col) % actions.count]
|
|
|
- ActionBubble(symbol: symbol, highlighted: col == 0 && index % 3 == 0)
|
|
|
+ HStack(spacing: 10) {
|
|
|
+ // Reduce the number of action icons and add a working pin/unpin button.
|
|
|
+ // Layout: [pin/unpin] + 6 action bubbles
|
|
|
+ ForEach(0..<7, id: \.self) { col in
|
|
|
+ let app = apps[index]
|
|
|
+ if col == 0 {
|
|
|
+ let isPinned = pinnedIDs.contains(app.id)
|
|
|
+ Button(action: { togglePin(app) }) {
|
|
|
+ ActionBubble(
|
|
|
+ symbol: isPinned ? "pin.fill" : "pin",
|
|
|
+ highlighted: isPinned
|
|
|
+ )
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ } else {
|
|
|
+ let actionCol = col - 1
|
|
|
+ let symbol = actions[(index * 3 + actionCol) % actions.count]
|
|
|
+ let highlighted = actionCol == 0 && index % 3 == 0
|
|
|
+ ActionBubble(symbol: symbol, highlighted: highlighted)
|
|
|
+ }
|
|
|
}
|
|
|
Image(systemName: "info.circle")
|
|
|
.foregroundStyle(.white.opacity(0.6))
|
|
|
- .font(.system(size: 17))
|
|
|
+ .font(.system(size: 14))
|
|
|
.frame(width: 20)
|
|
|
}
|
|
|
}
|
|
|
@@ -480,6 +501,20 @@ private struct IconPanelView: View {
|
|
|
.padding(.bottom, 18)
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ private var pinnedIDs: Set<UUID> {
|
|
|
+ decodeUUIDSetGlobal(from: pinnedTileIDsData)
|
|
|
+ }
|
|
|
+
|
|
|
+ private func togglePin(_ app: LauncherApp) {
|
|
|
+ var updated = pinnedIDs
|
|
|
+ if updated.contains(app.id) {
|
|
|
+ updated.remove(app.id)
|
|
|
+ } else {
|
|
|
+ updated.insert(app.id)
|
|
|
+ }
|
|
|
+ pinnedTileIDsData = encodeUUIDSetGlobal(updated)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private struct AppIconGlyph: View {
|
|
|
@@ -523,9 +558,30 @@ private struct ActionBubble: View {
|
|
|
.overlay(
|
|
|
Image(systemName: symbol)
|
|
|
.foregroundStyle(.white.opacity(0.9))
|
|
|
- .font(.system(size: 15, weight: .medium))
|
|
|
+ .font(.system(size: 13, weight: .medium))
|
|
|
)
|
|
|
- .frame(width: 42, height: 42)
|
|
|
+ .frame(width: 34, height: 34)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// MARK: - Pinned app persistence helpers (panel mode)
|
|
|
+private func decodeUUIDSetGlobal(from dataString: String) -> Set<UUID> {
|
|
|
+ guard !dataString.isEmpty, let data = dataString.data(using: .utf8) else { return [] }
|
|
|
+ do {
|
|
|
+ let strings = try JSONDecoder().decode([String].self, from: data)
|
|
|
+ return Set(strings.compactMap(UUID.init(uuidString:)))
|
|
|
+ } catch {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+private func encodeUUIDSetGlobal(_ set: Set<UUID>) -> String {
|
|
|
+ do {
|
|
|
+ let strings = set.map(\.uuidString).sorted()
|
|
|
+ let data = try JSONEncoder().encode(strings)
|
|
|
+ return String(decoding: data, as: UTF8.self)
|
|
|
+ } catch {
|
|
|
+ return ""
|
|
|
}
|
|
|
}
|
|
|
|