From f3a340b9d43ccd7a7781e57fbb115791d7dd26fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Orzech?= Date: Sat, 14 Mar 2026 22:56:49 +0100 Subject: [PATCH 1/2] feat: add cooldown quick action buttons when Ready When a cooldown reaches 0, the cell becomes a clickable button that opens the corresponding Torn Items subsection. Booster target (boosters vs alcohol) is read from @AppStorage preference. --- MacTorn/MacTorn/Views/StatusView.swift | 128 +++++++++++++++++++++---- 1 file changed, 107 insertions(+), 21 deletions(-) diff --git a/MacTorn/MacTorn/Views/StatusView.swift b/MacTorn/MacTorn/Views/StatusView.swift index 7f1f99f..712efe5 100644 --- a/MacTorn/MacTorn/Views/StatusView.swift +++ b/MacTorn/MacTorn/Views/StatusView.swift @@ -3,7 +3,8 @@ import SwiftUI struct StatusView: View { @EnvironmentObject var appState: AppState @Environment(\.reduceTransparency) private var reduceTransparency - + @AppStorage("boosterCooldownTarget") private var boosterCooldownTarget: String = "boosters" + var body: some View { ScrollView { VStack(alignment: .leading, spacing: 12) { @@ -195,7 +196,14 @@ struct StatusView: View { // MARK: - Cooldowns private func cooldownsSection(_ cooldowns: Cooldowns) -> some View { - VStack(alignment: .leading, spacing: 8) { + let drugURL = URL(string: "https://www.torn.com/item.php#drugs-items") + let medicalURL = URL(string: "https://www.torn.com/item.php#medical-items") + let boosterURL = boosterCooldownTarget == "alcohol" + ? URL(string: "https://www.torn.com/item.php#alcohol-items") + : URL(string: "https://www.torn.com/item.php#boosters-items") + let boosterLabel = boosterCooldownTarget == "alcohol" ? "Use Alcohol →" : "Use Booster →" + + return VStack(alignment: .leading, spacing: 8) { Divider() Text("Cooldowns") @@ -204,13 +212,13 @@ struct StatusView: View { HStack(spacing: 16) { if let fetchTime = appState.lastUpdated { - LiveCooldownItem(label: "Drug", originalSeconds: cooldowns.drug, fetchTime: fetchTime, icon: "pills.fill") - LiveCooldownItem(label: "Medical", originalSeconds: cooldowns.medical, fetchTime: fetchTime, icon: "cross.case.fill") - LiveCooldownItem(label: "Booster", originalSeconds: cooldowns.booster, fetchTime: fetchTime, icon: "arrow.up.circle.fill") + LiveCooldownItem(label: "Drug", originalSeconds: cooldowns.drug, fetchTime: fetchTime, icon: "pills.fill", actionURL: drugURL, actionLabel: "Use Drug →") + LiveCooldownItem(label: "Medical", originalSeconds: cooldowns.medical, fetchTime: fetchTime, icon: "cross.case.fill", actionURL: medicalURL, actionLabel: "Use Medical →") + LiveCooldownItem(label: "Booster", originalSeconds: cooldowns.booster, fetchTime: fetchTime, icon: "arrow.up.circle.fill", actionURL: boosterURL, actionLabel: boosterLabel) } else { - CooldownItem(label: "Drug", seconds: cooldowns.drug, icon: "pills.fill") - CooldownItem(label: "Medical", seconds: cooldowns.medical, icon: "cross.case.fill") - CooldownItem(label: "Booster", seconds: cooldowns.booster, icon: "arrow.up.circle.fill") + CooldownItem(label: "Drug", seconds: cooldowns.drug, icon: "pills.fill", actionURL: drugURL, actionLabel: "Use Drug →") + CooldownItem(label: "Medical", seconds: cooldowns.medical, icon: "cross.case.fill", actionURL: medicalURL, actionLabel: "Use Medical →") + CooldownItem(label: "Booster", seconds: cooldowns.booster, icon: "arrow.up.circle.fill", actionURL: boosterURL, actionLabel: boosterLabel) } } } @@ -268,19 +276,58 @@ struct CooldownItem: View { let label: String let seconds: Int let icon: String + var actionURL: URL? = nil + var actionLabel: String? = nil + + @Environment(\.reduceTransparency) private var reduceTransparency var body: some View { + if seconds <= 0, let url = actionURL { + Button { + BrowserManager.shared.open(url) + } label: { + cellContent(remaining: seconds) + } + .buttonStyle(.plain) + .accessibilityLabel("Use \(label)") + .accessibilityHint("Opens Torn items page in browser") + } else { + cellContent(remaining: seconds) + } + } + + private func cellContent(remaining: Int) -> some View { VStack(spacing: 2) { Text(label) .font(.caption) - .foregroundColor(seconds > 0 ? .orange : .green) + .foregroundColor(remaining > 0 ? .orange : .green) Text(formattedTime) .font(.caption2.monospacedDigit()) - .foregroundColor(seconds > 0 ? .primary : .green) - .fontWeight(seconds <= 0 ? .bold : .regular) + .foregroundColor(remaining > 0 ? .primary : .green) + .fontWeight(remaining <= 0 ? .bold : .regular) + + if remaining <= 0, let actionLabel { + Text(actionLabel) + .font(.caption2) + .foregroundColor(.green) + .opacity(0.7) + } } .frame(maxWidth: .infinity) + .padding(.vertical, remaining <= 0 && actionURL != nil ? 4 : 0) + .background( + remaining <= 0 && actionURL != nil + ? Color.green.opacity(reduceTransparency ? 0.25 : 0.12) + : Color.clear + ) + .overlay { + if remaining <= 0 && actionURL != nil { + RoundedRectangle(cornerRadius: 6) + .stroke(Color.green.opacity(reduceTransparency ? 0.4 : 0.25)) + } + } + .cornerRadius(6) } private var formattedTime: String { @@ -301,26 +348,65 @@ struct LiveCooldownItem: View { let originalSeconds: Int let fetchTime: Date let icon: String + var actionURL: URL? = nil + var actionLabel: String? = nil + + @Environment(\.reduceTransparency) private var reduceTransparency var body: some View { TimelineView(.periodic(from: fetchTime, by: 1.0)) { context in let elapsed = Int(context.date.timeIntervalSince(fetchTime)) let remaining = max(0, originalSeconds - elapsed) - VStack(spacing: 2) { - Text(label) - .font(.caption) - .foregroundColor(remaining > 0 ? .orange : .green) - - Text(formattedTime(remaining)) - .font(.caption2.monospacedDigit()) - .foregroundColor(remaining > 0 ? .primary : .green) - .fontWeight(remaining <= 0 ? .bold : .regular) + if remaining <= 0, let url = actionURL { + Button { + BrowserManager.shared.open(url) + } label: { + cellContent(remaining: remaining) + } + .buttonStyle(.plain) + .accessibilityLabel("Use \(label)") + .accessibilityHint("Opens Torn items page in browser") + } else { + cellContent(remaining: remaining) } - .frame(maxWidth: .infinity) } } + private func cellContent(remaining: Int) -> some View { + VStack(spacing: 2) { + Text(label) + .font(.caption) + .foregroundColor(remaining > 0 ? .orange : .green) + + Text(formattedTime(remaining)) + .font(.caption2.monospacedDigit()) + .foregroundColor(remaining > 0 ? .primary : .green) + .fontWeight(remaining <= 0 ? .bold : .regular) + + if remaining <= 0, let actionLabel { + Text(actionLabel) + .font(.caption2) + .foregroundColor(.green) + .opacity(0.7) + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, remaining <= 0 && actionURL != nil ? 4 : 0) + .background( + remaining <= 0 && actionURL != nil + ? Color.green.opacity(reduceTransparency ? 0.25 : 0.12) + : Color.clear + ) + .overlay { + if remaining <= 0 && actionURL != nil { + RoundedRectangle(cornerRadius: 6) + .stroke(Color.green.opacity(reduceTransparency ? 0.4 : 0.25)) + } + } + .cornerRadius(6) + } + private func formattedTime(_ seconds: Int) -> String { if seconds <= 0 { return "Ready" } let hours = seconds / 3600 From ce183a794a3a21da0b4de8f3e6d48cf0c552b94f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Orzech?= Date: Sat, 14 Mar 2026 22:57:10 +0100 Subject: [PATCH 2/2] feat: add booster cooldown target setting (Boosters/Alcohol) --- MacTorn/MacTorn/Views/SettingsView.swift | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/MacTorn/MacTorn/Views/SettingsView.swift b/MacTorn/MacTorn/Views/SettingsView.swift index a9c32b5..05b711d 100644 --- a/MacTorn/MacTorn/Views/SettingsView.swift +++ b/MacTorn/MacTorn/Views/SettingsView.swift @@ -5,6 +5,7 @@ struct SettingsView: View { @AppStorage("appearanceMode") private var appearanceMode: String = AppearanceMode.system.rawValue @AppStorage("reduceTransparency") private var reduceTransparency: Bool = false @AppStorage("preferredBrowser") private var preferredBrowser: String = PreferredBrowser.system.rawValue + @AppStorage("boosterCooldownTarget") private var boosterCooldownTarget: String = "boosters" @State private var inputKey: String = "" @State private var showCredits: Bool = false @State private var availableBrowsers: [PreferredBrowser] = PreferredBrowser.availableBrowsers() @@ -129,6 +130,19 @@ struct SettingsView: View { Toggle("Reduce Transparency", isOn: $reduceTransparency) .toggleStyle(.switch) } + + // Booster Cooldown Target + HStack { + Image(systemName: "arrow.up.circle.fill") + .foregroundColor(.secondary) + .frame(width: 20) + + Picker("Booster cooldown link", selection: $boosterCooldownTarget) { + Text("Boosters").tag("boosters") + Text("Alcohol").tag("alcohol") + } + .pickerStyle(.segmented) + } } .padding(.horizontal)