diff --git a/MacTorn-v1.0.zip b/MacTorn-v1.0.zip index f9b3e38..42006d2 100644 Binary files a/MacTorn-v1.0.zip and b/MacTorn-v1.0.zip differ diff --git a/MacTorn/MacTorn/Models/TornModels.swift b/MacTorn/MacTorn/Models/TornModels.swift index af6fc66..51868f2 100644 --- a/MacTorn/MacTorn/Models/TornModels.swift +++ b/MacTorn/MacTorn/Models/TornModels.swift @@ -85,10 +85,10 @@ struct Cooldowns: Codable, Equatable { // MARK: - Travel struct Travel: Codable, Equatable { - let destination: String - let timestamp: Int - let departed: Int - let timeLeft: Int + let destination: String? + let timestamp: Int? + let departed: Int? + let timeLeft: Int? enum CodingKeys: String, CodingKey { case destination @@ -98,25 +98,27 @@ struct Travel: Codable, Equatable { } var isAbroad: Bool { - destination != "Torn" && timeLeft == 0 + guard let dest = destination, let time = timeLeft else { return false } + return dest != "Torn" && time == 0 } var isTraveling: Bool { - timeLeft > 0 + guard let time = timeLeft else { return false } + return time > 0 } var arrivalDate: Date? { - guard isTraveling else { return nil } - return Date(timeIntervalSince1970: TimeInterval(timestamp)) + guard isTraveling, let ts = timestamp else { return nil } + return Date(timeIntervalSince1970: TimeInterval(ts)) } } // MARK: - Status (Hospital/Jail) struct Status: Codable, Equatable { - let description: String + let description: String? let details: String? - let state: String - let until: Int + let state: String? + let until: Int? var isInHospital: Bool { state == "Hospital" @@ -127,31 +129,35 @@ struct Status: Codable, Equatable { } var isOkay: Bool { - state == "Okay" + state == "Okay" || state == nil } var timeRemaining: Int { - max(0, until - Int(Date().timeIntervalSince1970)) + guard let until = until else { return 0 } + return max(0, until - Int(Date().timeIntervalSince1970)) } } // MARK: - Chain struct Chain: Codable, Equatable { - let current: Int - let maximum: Int - let timeout: Int - let cooldown: Int + let current: Int? + let maximum: Int? + let timeout: Int? + let cooldown: Int? var isActive: Bool { - current > 0 && timeout > 0 + guard let current = current, let timeout = timeout else { return false } + return current > 0 && timeout > 0 } var isOnCooldown: Bool { - cooldown > 0 + guard let cooldown = cooldown else { return false } + return cooldown > 0 } var timeoutRemaining: Int { - max(0, timeout - Int(Date().timeIntervalSince1970)) + guard let timeout = timeout else { return 0 } + return max(0, timeout - Int(Date().timeIntervalSince1970)) } } @@ -159,7 +165,7 @@ struct Chain: Codable, Equatable { struct TornEvent: Codable, Identifiable { let timestamp: Int let event: String - let seen: Int + let seen: Int? var id: Int { timestamp } @@ -175,11 +181,11 @@ struct TornEvent: Codable, Identifiable { // MARK: - Messages struct TornMessage: Codable { - let name: String - let type: String - let title: String - let timestamp: Int - let read: Int + let name: String? + let type: String? + let title: String? + let timestamp: Int? + let read: Int? } // MARK: - Error diff --git a/MacTorn/MacTorn/Views/Components/ChainView.swift b/MacTorn/MacTorn/Views/Components/ChainView.swift index 1738a74..9e35425 100644 --- a/MacTorn/MacTorn/Views/Components/ChainView.swift +++ b/MacTorn/MacTorn/Views/Components/ChainView.swift @@ -9,7 +9,7 @@ struct ChainView: View { HStack { Image(systemName: "link") .foregroundColor(timeoutColor) - Text("Chain: \(chain.current)/\(chain.maximum)") + Text("Chain: \(chain.current ?? 0)/\(chain.maximum ?? 0)") .font(.caption.bold()) Spacer() diff --git a/MacTorn/MacTorn/Views/SettingsView.swift b/MacTorn/MacTorn/Views/SettingsView.swift index 886b251..ba3ce6e 100644 --- a/MacTorn/MacTorn/Views/SettingsView.swift +++ b/MacTorn/MacTorn/Views/SettingsView.swift @@ -3,36 +3,30 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var appState: AppState @State private var inputKey: String = "" - @State private var showNotificationSettings = false // Developer ID for tip feature (bombel) private let developerID = 2362436 var body: some View { - ScrollView { - VStack(spacing: 16) { - // Header - Image(systemName: "bolt.circle.fill") - .font(.system(size: 48)) - .foregroundStyle( - LinearGradient( - colors: [.orange, .yellow], - startPoint: .topLeading, - endPoint: .bottomTrailing - ) + VStack(spacing: 20) { + // Header + Image(systemName: "bolt.circle.fill") + .font(.system(size: 48)) + .foregroundStyle( + LinearGradient( + colors: [.orange, .yellow], + startPoint: .topLeading, + endPoint: .bottomTrailing ) - - Text("MacTorn") - .font(.title2.bold()) - - Text("Enter your Torn API Key") - .font(.caption) - .foregroundColor(.secondary) - - // API Key input - SecureField("API Key", text: $inputKey) + ) + + Text("MacTorn") + .font(.title2.bold()) + + // API Key section + VStack(spacing: 8) { + SecureField("Torn API Key", text: $inputKey) .textFieldStyle(.roundedBorder) - .padding(.horizontal) Button("Save & Connect") { appState.apiKey = inputKey.trimmingCharacters(in: .whitespacesAndNewlines) @@ -44,147 +38,100 @@ 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) - + } + .padding(.horizontal) + + Divider() + + // Settings + VStack(spacing: 12) { // Refresh Interval - refreshIntervalSection + HStack { + Image(systemName: "clock") + .foregroundColor(.secondary) + .frame(width: 20) + + Picker("Refresh", selection: Binding( + get: { appState.refreshInterval }, + set: { newValue in + appState.refreshInterval = newValue + appState.startPolling() + } + )) { + Text("15s").tag(15) + Text("30s").tag(30) + Text("60s").tag(60) + Text("2m").tag(120) + } + .pickerStyle(.segmented) + } // Launch at Login - Toggle(isOn: Binding( - get: { appState.launchAtLogin.isEnabled }, - set: { _ in appState.launchAtLogin.toggle() } - )) { - Label("Launch at Login", systemImage: "power") + HStack { + Image(systemName: "power") + .foregroundColor(.secondary) + .frame(width: 20) + Toggle("Launch at Login", isOn: Binding( + get: { appState.launchAtLogin.isEnabled }, + set: { _ in appState.launchAtLogin.toggle() } + )) + .toggleStyle(.switch) + } + } + .padding(.horizontal) + + Divider() + + // Support section + VStack(spacing: 8) { + HStack(spacing: 4) { + Image(systemName: "gift.fill") + .foregroundColor(.purple) + Text("Support the Developer") + .font(.caption.bold()) } - .toggleStyle(.switch) - .padding(.horizontal) - // Notification Settings + Text("Send me some Xanax or cash :)") + .font(.caption2) + .foregroundColor(.secondary) + Button { - showNotificationSettings.toggle() + openTornProfile() } label: { - HStack { - Image(systemName: "bell.badge") - Text("Notification Rules") - Spacer() - Image(systemName: showNotificationSettings ? "chevron.up" : "chevron.down") + HStack(spacing: 4) { + Image(systemName: "paperplane.fill") + Text("Send to bombel") } .font(.caption) - .padding(.horizontal) + .padding(.vertical, 6) + .padding(.horizontal, 12) + .background(Color.purple.opacity(0.15)) + .cornerRadius(6) } .buttonStyle(.plain) - - if showNotificationSettings { - notificationRulesSection - } - - Divider() - .padding(.vertical, 4) - - // Tip Me section - tipMeSection - - // GitHub link - githubSection } - .padding() + .padding(.vertical, 8) + .padding(.horizontal) + .background(Color.purple.opacity(0.05)) + .cornerRadius(8) + + // GitHub + HStack(spacing: 4) { + Image(systemName: "chevron.left.forwardslash.chevron.right") + .font(.caption2) + .foregroundColor(.gray) + Link("View on GitHub", + destination: URL(string: "https://github.com/pawelorzech/MacTorn")!) + .font(.caption) + } } - .fixedSize(horizontal: false, vertical: true) + .padding() + .frame(width: 320) .onAppear { inputKey = appState.apiKey } } - // MARK: - Refresh Interval - private var refreshIntervalSection: some View { - HStack { - Label("Refresh Interval", systemImage: "clock") - .font(.caption) - - Spacer() - - Picker("", selection: Binding( - get: { appState.refreshInterval }, - set: { newValue in - appState.refreshInterval = newValue - appState.startPolling() - } - )) { - Text("15s").tag(15) - Text("30s").tag(30) - Text("60s").tag(60) - Text("120s").tag(120) - } - .pickerStyle(.segmented) - .frame(width: 180) - } - .padding(.horizontal) - } - - // MARK: - Notification Rules - private var notificationRulesSection: some View { - VStack(spacing: 8) { - ForEach(appState.notificationRules) { rule in - NotificationRuleRow(rule: rule) { updatedRule in - appState.updateRule(updatedRule) - } - } - } - .padding() - .background(Color.gray.opacity(0.1)) - .cornerRadius(8) - .padding(.horizontal) - } - - // MARK: - Tip Me Section - private var tipMeSection: some View { - VStack(spacing: 8) { - HStack { - Image(systemName: "gift.fill") - .foregroundColor(.purple) - Text("Support the Developer") - .font(.caption.bold()) - } - - Text("Send me some Xanax or cash :)") - .font(.caption2) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - - Button { - openTornProfile() - } label: { - HStack { - Image(systemName: "paperplane.fill") - Text("Send Xanax to bombel") - } - .font(.caption) - .padding(.vertical, 8) - .padding(.horizontal, 16) - .background(Color.purple.opacity(0.15)) - .cornerRadius(8) - } - .buttonStyle(.plain) - } - .padding() - .background(Color.purple.opacity(0.05)) - .cornerRadius(8) - } - - // MARK: - GitHub Section - private var githubSection: some View { - HStack { - Image(systemName: "chevron.left.forwardslash.chevron.right") - .foregroundColor(.gray) - Link("View Source on GitHub", - destination: URL(string: "https://github.com/pawelorzech/MacTorn")!) - .font(.caption) - } - } - - // MARK: - Helpers private func openTornProfile() { let url = "https://www.torn.com/profiles.php?XID=\(developerID)" if let url = URL(string: url) { @@ -192,46 +139,3 @@ struct SettingsView: View { } } } - -// MARK: - Notification Rule Row -struct NotificationRuleRow: View { - let rule: NotificationRule - let onUpdate: (NotificationRule) -> Void - - var body: some View { - HStack { - Toggle(isOn: Binding( - get: { rule.enabled }, - set: { newValue in - var updated = rule - updated.enabled = newValue - onUpdate(updated) - } - )) { - HStack { - Text(rule.barType.rawValue) - .font(.caption) - Text("@ \(rule.threshold)%") - .font(.caption2) - .foregroundColor(.secondary) - } - } - .toggleStyle(.switch) - .controlSize(.small) - - Picker("", selection: Binding( - get: { rule.soundName }, - set: { newValue in - var updated = rule - updated.soundName = newValue - onUpdate(updated) - } - )) { - ForEach(NotificationSound.allCases, id: \.rawValue) { sound in - Text(sound.displayName).tag(sound.rawValue) - } - } - .frame(width: 80) - } - } -} diff --git a/MacTorn/MacTorn/Views/StatusView.swift b/MacTorn/MacTorn/Views/StatusView.swift index 6159762..37c5b91 100644 --- a/MacTorn/MacTorn/Views/StatusView.swift +++ b/MacTorn/MacTorn/Views/StatusView.swift @@ -131,7 +131,7 @@ struct StatusView: View { HStack { Image(systemName: "airplane") .foregroundColor(.blue) - Text(travel.isTraveling ? "Traveling to \(travel.destination)" : "In \(travel.destination)") + Text(travel.isTraveling ? "Traveling to \(travel.destination ?? "Unknown")" : "In \(travel.destination ?? "Unknown")") .font(.caption.bold()) } @@ -140,7 +140,7 @@ struct StatusView: View { Text("Arriving in:") .font(.caption2) .foregroundColor(.secondary) - Text(formatTime(travel.timeLeft)) + Text(formatTime(travel.timeLeft ?? 0)) .font(.caption.monospacedDigit()) .foregroundColor(.blue) } diff --git a/README.md b/README.md index c632646..ffa72f7 100644 --- a/README.md +++ b/README.md @@ -9,14 +9,15 @@ A native macOS menu bar app for monitoring your **Torn** game status. ## Features - 📊 **Live Status Bars** - Energy, Nerve, Happy, Life with color-coded progress -- ⏱️ **Cooldown Timers** - Drug, Medical, Booster countdowns -- ✈️ **Travel Monitoring** - Destination tracking with arrival countdown -- 🔗 **Chain Timer** - Active chain counter with timeout warning +- ⏱️ **Cooldown Timers** - Drug, Medical, Booster countdowns with ready state +- ✈️ **Travel Monitoring** - Destination tracking with arrival countdown and abroad state +- 🔗 **Chain Timer** - Active chain counter with timeout warning + cooldown state - 🏥 **Hospital/Jail Status** - Countdown to release -- 📨 **Unread Messages** - Quick access to inbox +- 📨 **Unread Messages** - Inbox badge with one-click open - 🔔 **Events Feed** - Recent activity at a glance -- 🔔 **Smart Notifications** - Custom threshold alerts with sound options -- ⚡ **Quick Links** - 8 configurable shortcuts to Torn pages +- 🔔 **Notifications** - Bars thresholds, cooldown ready, landing, chain expiring, and release +- ⚡ **Quick Links** - Grid of customizable Torn shortcuts (8 defaults) +- 🕒 **Refresh Control** - 15s/30s/60s/2m polling + manual refresh + last updated - 🚀 **Launch at Login** - Start automatically with macOS ## Installation @@ -26,7 +27,7 @@ A native macOS menu bar app for monitoring your **Torn** game status. 3. Open MacTorn from Applications 4. Enter your [Torn API Key](https://www.torn.com/preferences.php#tab=api) -> **Note**: On first launch, macOS may show a security warning. Right-click the app and select "Open" to bypass. +> **Note**: If you download an unsigned build, macOS Gatekeeper will block it. Right-click the app and select "Open", or go to System Settings → Privacy & Security → Open Anyway. ## Requirements @@ -38,10 +39,8 @@ A native macOS menu bar app for monitoring your **Torn** game status. ### Refresh Interval Choose polling frequency: 15s, 30s, 60s, or 120s -### Notification Rules -Customize when to receive alerts: -- Energy/Nerve/Happy/Life at specific thresholds -- Sound selection per notification type +### Notifications +MacTorn sends notifications for bar thresholds, cooldown ready, landing, chain expiring, and release. Notification defaults are stored locally. ### Quick Links 8 preset shortcuts to common Torn pages (fully editable)