mirror of
https://github.com/pawelorzech/MacTorn.git
synced 2026-03-31 20:25:43 +00:00
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.
This commit is contained in:
parent
0fc54b257d
commit
f3a340b9d4
1 changed files with 107 additions and 21 deletions
|
|
@ -3,6 +3,7 @@ import SwiftUI
|
||||||
struct StatusView: View {
|
struct StatusView: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@Environment(\.reduceTransparency) private var reduceTransparency
|
@Environment(\.reduceTransparency) private var reduceTransparency
|
||||||
|
@AppStorage("boosterCooldownTarget") private var boosterCooldownTarget: String = "boosters"
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
|
|
@ -195,7 +196,14 @@ struct StatusView: View {
|
||||||
|
|
||||||
// MARK: - Cooldowns
|
// MARK: - Cooldowns
|
||||||
private func cooldownsSection(_ cooldowns: Cooldowns) -> some View {
|
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()
|
Divider()
|
||||||
|
|
||||||
Text("Cooldowns")
|
Text("Cooldowns")
|
||||||
|
|
@ -204,13 +212,13 @@ struct StatusView: View {
|
||||||
|
|
||||||
HStack(spacing: 16) {
|
HStack(spacing: 16) {
|
||||||
if let fetchTime = appState.lastUpdated {
|
if let fetchTime = appState.lastUpdated {
|
||||||
LiveCooldownItem(label: "Drug", originalSeconds: cooldowns.drug, fetchTime: fetchTime, icon: "pills.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")
|
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")
|
LiveCooldownItem(label: "Booster", originalSeconds: cooldowns.booster, fetchTime: fetchTime, icon: "arrow.up.circle.fill", actionURL: boosterURL, actionLabel: boosterLabel)
|
||||||
} else {
|
} else {
|
||||||
CooldownItem(label: "Drug", seconds: cooldowns.drug, icon: "pills.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")
|
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")
|
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 label: String
|
||||||
let seconds: Int
|
let seconds: Int
|
||||||
let icon: String
|
let icon: String
|
||||||
|
var actionURL: URL? = nil
|
||||||
|
var actionLabel: String? = nil
|
||||||
|
|
||||||
|
@Environment(\.reduceTransparency) private var reduceTransparency
|
||||||
|
|
||||||
var body: some View {
|
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) {
|
VStack(spacing: 2) {
|
||||||
Text(label)
|
Text(label)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(seconds > 0 ? .orange : .green)
|
.foregroundColor(remaining > 0 ? .orange : .green)
|
||||||
|
|
||||||
Text(formattedTime)
|
Text(formattedTime)
|
||||||
.font(.caption2.monospacedDigit())
|
.font(.caption2.monospacedDigit())
|
||||||
.foregroundColor(seconds > 0 ? .primary : .green)
|
.foregroundColor(remaining > 0 ? .primary : .green)
|
||||||
.fontWeight(seconds <= 0 ? .bold : .regular)
|
.fontWeight(remaining <= 0 ? .bold : .regular)
|
||||||
|
|
||||||
|
if remaining <= 0, let actionLabel {
|
||||||
|
Text(actionLabel)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.opacity(0.7)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.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 {
|
private var formattedTime: String {
|
||||||
|
|
@ -301,12 +348,32 @@ struct LiveCooldownItem: View {
|
||||||
let originalSeconds: Int
|
let originalSeconds: Int
|
||||||
let fetchTime: Date
|
let fetchTime: Date
|
||||||
let icon: String
|
let icon: String
|
||||||
|
var actionURL: URL? = nil
|
||||||
|
var actionLabel: String? = nil
|
||||||
|
|
||||||
|
@Environment(\.reduceTransparency) private var reduceTransparency
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineView(.periodic(from: fetchTime, by: 1.0)) { context in
|
TimelineView(.periodic(from: fetchTime, by: 1.0)) { context in
|
||||||
let elapsed = Int(context.date.timeIntervalSince(fetchTime))
|
let elapsed = Int(context.date.timeIntervalSince(fetchTime))
|
||||||
let remaining = max(0, originalSeconds - elapsed)
|
let remaining = max(0, originalSeconds - elapsed)
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cellContent(remaining: Int) -> some View {
|
||||||
VStack(spacing: 2) {
|
VStack(spacing: 2) {
|
||||||
Text(label)
|
Text(label)
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
|
|
@ -316,10 +383,29 @@ struct LiveCooldownItem: View {
|
||||||
.font(.caption2.monospacedDigit())
|
.font(.caption2.monospacedDigit())
|
||||||
.foregroundColor(remaining > 0 ? .primary : .green)
|
.foregroundColor(remaining > 0 ? .primary : .green)
|
||||||
.fontWeight(remaining <= 0 ? .bold : .regular)
|
.fontWeight(remaining <= 0 ? .bold : .regular)
|
||||||
|
|
||||||
|
if remaining <= 0, let actionLabel {
|
||||||
|
Text(actionLabel)
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.opacity(0.7)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.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 {
|
private func formattedTime(_ seconds: Int) -> String {
|
||||||
if seconds <= 0 { return "Ready" }
|
if seconds <= 0 { return "Ready" }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue