From 455f9f3916486e7de67c99236b78d9b236947394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Orzech?= Date: Sat, 17 Jan 2026 18:00:47 +0000 Subject: [PATCH] Add launch at login, shortcuts, and UI improvements Introduces LaunchAtLoginManager and ShortcutsManager for launch at login and customizable quick links. Updates SettingsView with toggles and a shortcuts editor, enhances ContentView and StatusView with improved layout, travel status, and quick links grid. ProgressBarView now visually highlights full bars and improves appearance. Adds travel notifications and updates the README with new features and setup instructions. --- MacTorn/MacTorn.xcodeproj/project.pbxproj | 8 + MacTorn/MacTorn/MacTornApp.swift | 15 + MacTorn/MacTorn/ViewModels/AppState.swift | 54 ++-- .../Views/Components/ProgressBarView.swift | 46 ++- MacTorn/MacTorn/Views/ContentView.swift | 38 ++- MacTorn/MacTorn/Views/SettingsView.swift | 141 ++++++++- MacTorn/MacTorn/Views/StatusView.swift | 289 ++++++++++++------ README.md | 30 +- 8 files changed, 486 insertions(+), 135 deletions(-) diff --git a/MacTorn/MacTorn.xcodeproj/project.pbxproj b/MacTorn/MacTorn.xcodeproj/project.pbxproj index f3e3cd8..1ce9245 100644 --- a/MacTorn/MacTorn.xcodeproj/project.pbxproj +++ b/MacTorn/MacTorn.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ AAA00007 /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10007 /* StatusView.swift */; }; AAA00008 /* ProgressBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10008 /* ProgressBarView.swift */; }; AAA00009 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10009 /* NotificationManager.swift */; }; + AAA00010 /* LaunchAtLoginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10011 /* LaunchAtLoginManager.swift */; }; + AAA00011 /* ShortcutsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10012 /* ShortcutsManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -29,6 +31,8 @@ AAA10008 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = ""; }; AAA10009 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; AAA10010 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AAA10011 /* LaunchAtLoginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAtLoginManager.swift; sourceTree = ""; }; + AAA10012 /* ShortcutsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsManager.swift; sourceTree = ""; }; AAA10000 /* MacTorn.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTorn.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -112,6 +116,8 @@ isa = PBXGroup; children = ( AAA10009 /* NotificationManager.swift */, + AAA10011 /* LaunchAtLoginManager.swift */, + AAA10012 /* ShortcutsManager.swift */, ); path = Utilities; sourceTree = ""; @@ -193,6 +199,8 @@ AAA00007 /* StatusView.swift in Sources */, AAA00008 /* ProgressBarView.swift in Sources */, AAA00009 /* NotificationManager.swift in Sources */, + AAA00010 /* LaunchAtLoginManager.swift in Sources */, + AAA00011 /* ShortcutsManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/MacTorn/MacTorn/MacTornApp.swift b/MacTorn/MacTorn/MacTornApp.swift index 911d724..8bbaf22 100644 --- a/MacTorn/MacTorn/MacTornApp.swift +++ b/MacTorn/MacTorn/MacTornApp.swift @@ -16,14 +16,29 @@ struct MacTornApp: App { } private var menuBarIcon: String { + // Error state if appState.errorMsg != nil { return "exclamationmark.triangle.fill" } + + // Traveling state + if let travel = appState.data?.travel, travel.isTraveling { + return "airplane" + } + + // Abroad state + if let travel = appState.data?.travel, travel.isAbroad { + return "globe" + } + + // Energy full state if let bars = appState.data?.bars { if bars.energy.current >= bars.energy.maximum { return "bolt.fill" } } + + // Default return "bolt" } } diff --git a/MacTorn/MacTorn/ViewModels/AppState.swift b/MacTorn/MacTorn/ViewModels/AppState.swift index ba65f82..9e22282 100644 --- a/MacTorn/MacTorn/ViewModels/AppState.swift +++ b/MacTorn/MacTorn/ViewModels/AppState.swift @@ -13,9 +13,14 @@ class AppState: ObservableObject { @Published var errorMsg: String? @Published var isLoading: Bool = false + // MARK: - Managers + let launchAtLogin = LaunchAtLoginManager() + let shortcutsManager = ShortcutsManager() + // MARK: - State Comparison private var previousBars: Bars? private var previousCooldowns: Cooldowns? + private var previousTravel: Travel? // MARK: - Timer private var timerCancellable: AnyCancellable? @@ -88,6 +93,7 @@ class AppState: ObservableObject { // Store for comparison self.previousBars = decoded.bars self.previousCooldowns = decoded.cooldowns + self.previousTravel = decoded.travel } case 403, 404: self.errorMsg = "Invalid API Key" @@ -104,24 +110,25 @@ class AppState: ObservableObject { } private func checkNotifications(newData: TornResponse) { - guard let prev = previousBars, let current = newData.bars else { return } - - // Energy full notification - if prev.energy.current < prev.energy.maximum && - current.energy.current >= current.energy.maximum { - NotificationManager.shared.send( - title: "Energy Full! ⚡️", - body: "Your energy bar is now full (\(current.energy.maximum)/\(current.energy.maximum))" - ) - } - - // Nerve full notification - if prev.nerve.current < prev.nerve.maximum && - current.nerve.current >= current.nerve.maximum { - NotificationManager.shared.send( - title: "Nerve Full! 💪", - body: "Your nerve bar is now full (\(current.nerve.maximum)/\(current.nerve.maximum))" - ) + // Bar notifications + if let prev = previousBars, let current = newData.bars { + // Energy full notification + if prev.energy.current < prev.energy.maximum && + current.energy.current >= current.energy.maximum { + NotificationManager.shared.send( + title: "Energy Full! ⚡️", + body: "Your energy bar is now full (\(current.energy.maximum)/\(current.energy.maximum))" + ) + } + + // Nerve full notification + if prev.nerve.current < prev.nerve.maximum && + current.nerve.current >= current.nerve.maximum { + NotificationManager.shared.send( + title: "Nerve Full! 💪", + body: "Your nerve bar is now full (\(current.nerve.maximum)/\(current.nerve.maximum))" + ) + } } // Cooldown notifications @@ -145,6 +152,17 @@ class AppState: ObservableObject { ) } } + + // Travel notifications + if let prevTravel = previousTravel, let currentTravel = newData.travel { + // Landed notification + if prevTravel.isTraveling && !currentTravel.isTraveling { + NotificationManager.shared.send( + title: "Landed! ✈️", + body: "You have arrived in \(currentTravel.destination)" + ) + } + } } } diff --git a/MacTorn/MacTorn/Views/Components/ProgressBarView.swift b/MacTorn/MacTorn/Views/Components/ProgressBarView.swift index f175148..6b0d4c6 100644 --- a/MacTorn/MacTorn/Views/Components/ProgressBarView.swift +++ b/MacTorn/MacTorn/Views/Components/ProgressBarView.swift @@ -9,7 +9,11 @@ struct ProgressBarView: View { private var progress: Double { guard maximum > 0 else { return 0 } - return Double(current) / Double(maximum) + return min(1.0, Double(current) / Double(maximum)) + } + + private var isFull: Bool { + current >= maximum } var body: some View { @@ -17,31 +21,47 @@ struct ProgressBarView: View { HStack { Image(systemName: icon) .foregroundColor(color) - .font(.caption) + .font(.caption.bold()) Text(label) .font(.caption.bold()) + .foregroundColor(.primary) Spacer() Text("\(current)/\(maximum)") .font(.caption.monospacedDigit()) - .foregroundColor(.secondary) + .foregroundColor(isFull ? color : .secondary) + .fontWeight(isFull ? .bold : .regular) } + // Progress bar with visible styling GeometryReader { geometry in ZStack(alignment: .leading) { - // Background + // Background track RoundedRectangle(cornerRadius: 4) - .fill(color.opacity(0.2)) + .fill(Color.gray.opacity(0.3)) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(color.opacity(0.3), lineWidth: 1) + ) - // Foreground - RoundedRectangle(cornerRadius: 4) - .fill(color) - .frame(width: geometry.size.width * progress) + // Filled progress + if progress > 0 { + RoundedRectangle(cornerRadius: 4) + .fill( + LinearGradient( + colors: [color, color.opacity(0.7)], + startPoint: .leading, + endPoint: .trailing + ) + ) + .frame(width: max(4, geometry.size.width * progress)) + .shadow(color: color.opacity(0.5), radius: 2, x: 0, y: 0) + } } } - .frame(height: 8) + .frame(height: 10) } } } @@ -49,9 +69,11 @@ struct ProgressBarView: View { #Preview { VStack(spacing: 16) { ProgressBarView(label: "Energy", current: 75, maximum: 100, color: .green, icon: "bolt.fill") - ProgressBarView(label: "Nerve", current: 25, maximum: 50, color: .red, icon: "flame.fill") - ProgressBarView(label: "Happy", current: 1000, maximum: 1000, color: .yellow, icon: "face.smiling.fill") + ProgressBarView(label: "Nerve", current: 50, maximum: 50, color: .red, icon: "flame.fill") + ProgressBarView(label: "Happy", current: 500, maximum: 1000, color: .yellow, icon: "face.smiling.fill") + ProgressBarView(label: "Life", current: 0, maximum: 100, color: .pink, icon: "heart.fill") } .padding() .frame(width: 280) + .background(Color(NSColor.windowBackgroundColor)) } diff --git a/MacTorn/MacTorn/Views/ContentView.swift b/MacTorn/MacTorn/Views/ContentView.swift index b5b1812..003a2cb 100644 --- a/MacTorn/MacTorn/Views/ContentView.swift +++ b/MacTorn/MacTorn/Views/ContentView.swift @@ -2,25 +2,42 @@ import SwiftUI struct ContentView: View { @EnvironmentObject var appState: AppState + @State private var showSettings = false var body: some View { VStack(spacing: 0) { - if appState.apiKey.isEmpty { + if appState.apiKey.isEmpty || showSettings { SettingsView() + .environmentObject(appState) } else { + // Last updated + if let lastUpdated = appState.lastUpdated { + HStack { + Text("Updated: \(lastUpdated, formatter: timeFormatter)") + .font(.caption2) + .foregroundColor(.secondary) + Spacer() + } + .padding(.horizontal) + .padding(.top, 8) + } + StatusView() + .environmentObject(appState) } Divider() - .padding(.vertical, 8) + .padding(.vertical, 4) // Footer buttons HStack { - Button("Settings") { - appState.apiKey = "" // Go back to settings + if !appState.apiKey.isEmpty { + Button(showSettings ? "Back" : "Settings") { + showSettings.toggle() + } + .buttonStyle(.plain) + .foregroundColor(.secondary) } - .buttonStyle(.plain) - .foregroundColor(.secondary) Spacer() @@ -34,7 +51,12 @@ struct ContentView: View { .padding(.horizontal) .padding(.bottom, 8) } - .frame(width: 280) - .environmentObject(appState) + .frame(width: 300) + } + + private var timeFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.timeStyle = .short + return formatter } } diff --git a/MacTorn/MacTorn/Views/SettingsView.swift b/MacTorn/MacTorn/Views/SettingsView.swift index 55dd0f0..3f987f8 100644 --- a/MacTorn/MacTorn/Views/SettingsView.swift +++ b/MacTorn/MacTorn/Views/SettingsView.swift @@ -3,12 +3,20 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var appState: AppState @State private var inputKey: String = "" + @State private var showShortcutsEditor = false var body: some View { VStack(spacing: 16) { + // Header Image(systemName: "bolt.circle.fill") .font(.system(size: 48)) - .foregroundColor(.orange) + .foregroundStyle( + LinearGradient( + colors: [.orange, .yellow], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) Text("MacTorn") .font(.title2.bold()) @@ -17,6 +25,7 @@ struct SettingsView: View { .font(.caption) .foregroundColor(.secondary) + // API Key input SecureField("API Key", text: $inputKey) .textFieldStyle(.roundedBorder) .padding(.horizontal) @@ -31,6 +40,33 @@ struct SettingsView: View { Link("Get API Key from Torn", destination: URL(string: "https://www.torn.com/preferences.php#tab=api")!) .font(.caption) + + Divider() + .padding(.vertical, 8) + + // Launch at Login + Toggle(isOn: Binding( + get: { appState.launchAtLogin.isEnabled }, + set: { _ in appState.launchAtLogin.toggle() } + )) { + Label("Launch at Login", systemImage: "power") + } + .toggleStyle(.switch) + .padding(.horizontal) + + // Shortcuts Editor + Button { + showShortcutsEditor.toggle() + } label: { + Label("Edit Shortcuts", systemImage: "keyboard") + } + .buttonStyle(.plain) + .foregroundColor(.accentColor) + + if showShortcutsEditor { + ShortcutsEditorView() + .environmentObject(appState) + } } .padding() .onAppear { @@ -38,3 +74,106 @@ struct SettingsView: View { } } } + +// MARK: - Shortcuts Editor +struct ShortcutsEditorView: View { + @EnvironmentObject var appState: AppState + @State private var editingShortcut: KeyboardShortcut? + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Quick Links") + .font(.caption.bold()) + + Spacer() + + Button("Reset") { + appState.shortcutsManager.resetToDefaults() + } + .font(.caption2) + .buttonStyle(.plain) + .foregroundColor(.red) + } + + ForEach(appState.shortcutsManager.shortcuts) { shortcut in + ShortcutRowView(shortcut: shortcut) { updated in + appState.shortcutsManager.updateShortcut(updated) + } + } + } + .padding() + .background(Color.gray.opacity(0.1)) + .cornerRadius(8) + } +} + +struct ShortcutRowView: View { + let shortcut: KeyboardShortcut + let onUpdate: (KeyboardShortcut) -> Void + + @State private var isEditing = false + @State private var editedName: String = "" + @State private var editedURL: String = "" + @State private var editedKey: String = "" + + var body: some View { + VStack(spacing: 4) { + HStack { + if isEditing { + TextField("Name", text: $editedName) + .textFieldStyle(.roundedBorder) + .font(.caption) + .frame(width: 60) + + TextField("URL", text: $editedURL) + .textFieldStyle(.roundedBorder) + .font(.caption2) + + TextField("Key", text: $editedKey) + .textFieldStyle(.roundedBorder) + .font(.caption) + .frame(width: 30) + + Button("Save") { + var updated = shortcut + updated.name = editedName + updated.url = editedURL + updated.keyEquivalent = editedKey + onUpdate(updated) + isEditing = false + } + .font(.caption2) + .buttonStyle(.borderedProminent) + } else { + Text(shortcut.name) + .font(.caption) + .frame(width: 60, alignment: .leading) + + Text(shortcut.url) + .font(.caption2) + .foregroundColor(.secondary) + .lineLimit(1) + .truncationMode(.middle) + + Spacer() + + Text("⌘⇧\(shortcut.keyEquivalent.uppercased())") + .font(.caption2.monospaced()) + .foregroundColor(.secondary) + + Button { + editedName = shortcut.name + editedURL = shortcut.url + editedKey = shortcut.keyEquivalent + isEditing = true + } label: { + Image(systemName: "pencil") + .font(.caption2) + } + .buttonStyle(.plain) + } + } + } + } +} diff --git a/MacTorn/MacTorn/Views/StatusView.swift b/MacTorn/MacTorn/Views/StatusView.swift index fea38d4..12a348f 100644 --- a/MacTorn/MacTorn/Views/StatusView.swift +++ b/MacTorn/MacTorn/Views/StatusView.swift @@ -4,101 +4,199 @@ struct StatusView: View { @EnvironmentObject var appState: AppState var body: some View { - VStack(alignment: .leading, spacing: 12) { - // Header + ScrollView { + VStack(alignment: .leading, spacing: 12) { + // Header + headerSection + + // Error state + if let error = appState.errorMsg { + errorSection(error) + } + + // Travel status + if let travel = appState.data?.travel, travel.isTraveling || travel.isAbroad { + travelSection(travel) + } + + // Bars + if let bars = appState.data?.bars { + barsSection(bars) + } + + // Cooldowns + if let cooldowns = appState.data?.cooldowns { + cooldownsSection(cooldowns) + } + + // Quick Links + quickLinksSection + } + .padding() + } + .frame(maxHeight: 400) + } + + // MARK: - Header + private var headerSection: some View { + HStack { + Text("Torn Status") + .font(.headline) + + Spacer() + + if appState.isLoading { + ProgressView() + .scaleEffect(0.6) + } else { + Button { + appState.refreshNow() + } label: { + Image(systemName: "arrow.clockwise") + } + .buttonStyle(.plain) + .foregroundColor(.secondary) + } + } + } + + // MARK: - Error + private func errorSection(_ error: String) -> some View { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + Text(error) + .font(.caption) + .foregroundColor(.secondary) + } + .padding(.vertical, 4) + } + + // MARK: - Travel + private func travelSection(_ travel: Travel) -> some View { + VStack(alignment: .leading, spacing: 4) { HStack { - Text("Torn Status") - .font(.headline) - - Spacer() - - if appState.isLoading { - ProgressView() - .scaleEffect(0.6) - } else { - Button { - appState.refreshNow() - } label: { - Image(systemName: "arrow.clockwise") - } - .buttonStyle(.plain) - .foregroundColor(.secondary) - } - } - - // Last updated - if let lastUpdated = appState.lastUpdated { - Text("Updated: \(lastUpdated, formatter: timeFormatter)") - .font(.caption2) - .foregroundColor(.secondary) - } - - // Error state - if let error = appState.errorMsg { - HStack { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.orange) - Text(error) - .font(.caption) - .foregroundColor(.secondary) - } - .padding(.vertical, 4) - } - - // Bars - if let bars = appState.data?.bars { - VStack(spacing: 8) { - ProgressBarView( - label: "Energy", - current: bars.energy.current, - maximum: bars.energy.maximum, - color: .green, - icon: "bolt.fill" - ) - - ProgressBarView( - label: "Nerve", - current: bars.nerve.current, - maximum: bars.nerve.maximum, - color: .red, - icon: "flame.fill" - ) - - ProgressBarView( - label: "Happy", - current: bars.happy.current, - maximum: bars.happy.maximum, - color: .yellow, - icon: "face.smiling.fill" - ) - - ProgressBarView( - label: "Life", - current: bars.life.current, - maximum: bars.life.maximum, - color: .pink, - icon: "heart.fill" - ) - } - } - - // Cooldowns - if let cooldowns = appState.data?.cooldowns { - Divider() - .padding(.vertical, 4) - - Text("Cooldowns") + Image(systemName: "airplane") + .foregroundColor(.blue) + Text(travel.isTraveling ? "Traveling to \(travel.destination)" : "In \(travel.destination)") .font(.caption.bold()) - .foregroundColor(.secondary) - - HStack(spacing: 16) { - 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") + } + + if travel.isTraveling { + HStack { + Text("Arriving in:") + .font(.caption2) + .foregroundColor(.secondary) + Text(formatTime(travel.timeLeft)) + .font(.caption.monospacedDigit()) + .foregroundColor(.blue) } } } - .padding() + .padding(8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.blue.opacity(0.1)) + .cornerRadius(8) + } + + // MARK: - Bars + private func barsSection(_ bars: Bars) -> some View { + VStack(spacing: 10) { + ProgressBarView( + label: "Energy", + current: bars.energy.current, + maximum: bars.energy.maximum, + color: .green, + icon: "bolt.fill" + ) + + ProgressBarView( + label: "Nerve", + current: bars.nerve.current, + maximum: bars.nerve.maximum, + color: .red, + icon: "flame.fill" + ) + + ProgressBarView( + label: "Happy", + current: bars.happy.current, + maximum: bars.happy.maximum, + color: .yellow, + icon: "face.smiling.fill" + ) + + ProgressBarView( + label: "Life", + current: bars.life.current, + maximum: bars.life.maximum, + color: .pink, + icon: "heart.fill" + ) + } + } + + // MARK: - Cooldowns + private func cooldownsSection(_ cooldowns: Cooldowns) -> some View { + VStack(alignment: .leading, spacing: 8) { + Divider() + + Text("Cooldowns") + .font(.caption.bold()) + .foregroundColor(.secondary) + + HStack(spacing: 16) { + 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") + } + } + } + + // MARK: - Quick Links + private var quickLinksSection: some View { + VStack(alignment: .leading, spacing: 8) { + Divider() + + Text("Quick Links") + .font(.caption.bold()) + .foregroundColor(.secondary) + + LazyVGrid(columns: [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()) + ], spacing: 8) { + ForEach(appState.shortcutsManager.shortcuts) { shortcut in + Button { + appState.shortcutsManager.openURL(shortcut.url) + } label: { + Text(shortcut.name) + .font(.caption2) + .lineLimit(1) + .frame(maxWidth: .infinity) + .padding(.vertical, 4) + .padding(.horizontal, 6) + .background(Color.accentColor.opacity(0.1)) + .cornerRadius(4) + } + .buttonStyle(.plain) + } + } + } + } + + // MARK: - Helpers + private func formatTime(_ 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) } private var timeFormatter: DateFormatter { @@ -117,11 +215,13 @@ struct CooldownItem: View { var body: some View { VStack(spacing: 2) { Image(systemName: icon) + .font(.caption) .foregroundColor(seconds > 0 ? .orange : .green) Text(formattedTime) .font(.caption2.monospacedDigit()) .foregroundColor(seconds > 0 ? .primary : .green) + .fontWeight(seconds <= 0 ? .bold : .regular) } .frame(maxWidth: .infinity) } @@ -130,12 +230,11 @@ struct CooldownItem: View { if seconds <= 0 { return "Ready" } - let minutes = seconds / 60 + let hours = seconds / 3600 + let minutes = (seconds % 3600) / 60 let secs = seconds % 60 - if minutes >= 60 { - let hours = minutes / 60 - let mins = minutes % 60 - return String(format: "%d:%02d:%02d", hours, mins, secs) + if hours > 0 { + return String(format: "%d:%02d:%02d", hours, minutes, secs) } return String(format: "%d:%02d", minutes, secs) } diff --git a/README.md b/README.md index df8add9..0098451 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ # MacTorn -Torn notifier app for macOS +Menu bar companion for the Torn game on macOS. It polls the Torn API and surfaces your current bars, cooldowns, and travel status in a lightweight menu bar window. + +## Features +- Menu bar status window with energy, nerve, happy, and life bars. +- Cooldown timers for drug, medical, and booster. +- Travel status with remaining time and destination. +- Local notifications when bars fill, cooldowns end, or you land. +- Quick links grid for Torn pages with editable labels and URLs. +- Launch at login toggle. + +## Requirements +- macOS 13.0 or later. +- A Torn API key. + +## Setup +1. Open the app and paste your Torn API key. +2. Click "Save & Connect". +3. (Optional) Enable "Launch at Login" and edit Quick Links. + +Get an API key from `https://www.torn.com/preferences.php#tab=api`. + +## Build and Run +1. Open `MacTorn/MacTorn.xcodeproj` in Xcode. +2. Select the MacTorn scheme. +3. Run the app (it appears in the menu bar). + +## Notes +- The app polls the Torn API every 30 seconds. +- Your API key and Quick Links are stored locally in `UserDefaults`.