MacTorn_mirror/docs/superpowers/plans/2026-03-14-cooldown-quick-actions.md

12 KiB

Cooldown Quick Action Buttons — Implementation Plan

For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (- [ ]) syntax for tracking.

Goal: When a cooldown reaches 0, turn the cooldown cell into a clickable button that opens the matching Torn Items page subsection.

Architecture: View-layer only changes. CooldownItem and LiveCooldownItem gain optional actionURL/actionLabel params. StatusView constructs URLs (reading booster preference from @AppStorage). SettingsView gets a new Picker for booster/alcohol target.

Tech Stack: SwiftUI, @AppStorage, BrowserManager

Spec: docs/superpowers/specs/2026-03-14-cooldown-quick-actions-design.md


Chunk 1: Cooldown Views + Settings

Task 1: Update CooldownItem with action button support

Files:

  • Modify: MacTorn/MacTorn/Views/StatusView.swift:267-296 (CooldownItem)

  • Step 1: Add new parameters and environment to CooldownItem

Add actionURL: URL? and actionLabel: String? parameters (both defaulting to nil), and add @Environment(\.reduceTransparency). Wrap the body in a conditional: if seconds <= 0 and actionURL is non-nil, render as a Button; otherwise keep existing layout.

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(remaining > 0 ? .orange : .green)

            Text(formattedTime)
                .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 var formattedTime: String {
        if seconds <= 0 { return "Ready" }
        let hours = seconds / 3600
        let minutes = (seconds % 3600) / 60
        let secs = seconds % 60
        if hours > 0 {
            return String(format: "%d:%02d:%02d", hours, minutes, secs)
        }
        return String(format: "%d:%02d", minutes, secs)
    }
}
  • Step 2: Build to verify no compilation errors

Run: cd /Users/pawelorzech/Programowanie/MacTorn && make build Expected: BUILD SUCCEEDED (existing call sites pass no actionURL/actionLabel, so defaults of nil keep them working)

  • Step 3: Commit
git add MacTorn/MacTorn/Views/StatusView.swift
git commit -m "feat: add action button support to CooldownItem"

Task 2: Update LiveCooldownItem with action button support

Files:

  • Modify: MacTorn/MacTorn/Views/StatusView.swift:299-334 (LiveCooldownItem)

  • Step 1: Add new parameters and environment to LiveCooldownItem

Same pattern as CooldownItem — add actionURL: URL?, actionLabel: String?, and @Environment(\.reduceTransparency). The TimelineView body wraps content in a conditional button when ready.

struct LiveCooldownItem: View {
    let label: String
    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)

            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) {
            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
        let minutes = (seconds % 3600) / 60
        let secs = seconds % 60
        if hours > 0 {
            return String(format: "%d:%02d:%02d", hours, minutes, secs)
        }
        return String(format: "%d:%02d", minutes, secs)
    }
}
  • Step 2: Build to verify no compilation errors

Run: cd /Users/pawelorzech/Programowanie/MacTorn && make build Expected: BUILD SUCCEEDED

  • Step 3: Commit
git add MacTorn/MacTorn/Views/StatusView.swift
git commit -m "feat: add action button support to LiveCooldownItem"

Task 3: Wire up URLs in cooldownsSection

Files:

  • Modify: MacTorn/MacTorn/Views/StatusView.swift:3-5 (add @AppStorage property to StatusView)

  • Modify: MacTorn/MacTorn/Views/StatusView.swift:197-217 (cooldownsSection)

  • Step 1: Add @AppStorage property to StatusView

Add after the existing @Environment(\.reduceTransparency) line:

@AppStorage("boosterCooldownTarget") private var boosterCooldownTarget: String = "boosters"
  • Step 2: Update cooldownsSection to pass URLs and labels
private func cooldownsSection(_ cooldowns: Cooldowns) -> some View {
    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")
            .font(.caption.bold())
            .foregroundColor(.secondary)

        HStack(spacing: 16) {
            if let fetchTime = appState.lastUpdated {
                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", 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)
            }
        }
    }
}
  • Step 3: Build to verify

Run: cd /Users/pawelorzech/Programowanie/MacTorn && make build Expected: BUILD SUCCEEDED

  • Step 4: Commit
git add MacTorn/MacTorn/Views/StatusView.swift
git commit -m "feat: wire cooldown action URLs in cooldownsSection"

Task 4: Add booster target setting to SettingsView

Files:

  • Modify: MacTorn/MacTorn/Views/SettingsView.swift:6-7 (add @AppStorage property)

  • Modify: MacTorn/MacTorn/Views/SettingsView.swift:124-132 (add Picker after Reduce Transparency)

  • Step 1: Add @AppStorage property to SettingsView

Add after the existing @AppStorage("preferredBrowser") line (line 7):

@AppStorage("boosterCooldownTarget") private var boosterCooldownTarget: String = "boosters"
  • Step 2: Add Picker after Reduce Transparency toggle

Insert after the closing } of the Reduce Transparency HStack (after line 131), before the closing } of the settings VStack (line 132):

// 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)
}
  • Step 3: Build to verify

Run: cd /Users/pawelorzech/Programowanie/MacTorn && make build Expected: BUILD SUCCEEDED

  • Step 4: Commit
git add MacTorn/MacTorn/Views/SettingsView.swift
git commit -m "feat: add booster cooldown target setting"

Task 5: Run all tests and verify

Files: None (verification only)

  • Step 1: Run unit tests

Run: cd /Users/pawelorzech/Programowanie/MacTorn && make test Expected: All existing tests pass. No tests were broken since we only added optional parameters with defaults.

  • Step 2: Run UI tests

Run: cd /Users/pawelorzech/Programowanie/MacTorn && make test-ui Expected: All existing UI tests pass.

  • Step 3: Build release to verify universal binary

Run: cd /Users/pawelorzech/Programowanie/MacTorn && make release Expected: BUILD SUCCEEDED for universal binary.