Compare commits

..

No commits in common. "56037885d03a208604910871de7c337a4d1841c5" and "d1166d3218e44cbb679094cf89d725b6c6acffbc" have entirely different histories.

16 changed files with 37 additions and 131 deletions

View file

@ -5,11 +5,6 @@ All notable changes to MacTorn will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.5.1] - 2026-02-04
### Added
- Expanded browser support in browser picker with additional browser options
## [1.5.0] - 2026-02-04 ## [1.5.0] - 2026-02-04
### Added ### Added

BIN
MacTorn-v1.4.4.zip Normal file

Binary file not shown.

BIN
MacTorn-v1.4.5.zip Normal file

Binary file not shown.

BIN
MacTorn-v1.4.6.zip Normal file

Binary file not shown.

View file

@ -651,7 +651,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.5.1; MARKETING_VERSION = 1.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.app; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -678,7 +678,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.5.1; MARKETING_VERSION = 1.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.app; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
@ -696,7 +696,7 @@
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.1; MARKETING_VERSION = 1.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornTests; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
@ -714,7 +714,7 @@
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.1; MARKETING_VERSION = 1.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornTests; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
@ -732,7 +732,7 @@
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.1; MARKETING_VERSION = 1.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornUITests; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;
@ -749,7 +749,7 @@
DEVELOPMENT_TEAM = ""; DEVELOPMENT_TEAM = "";
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0; MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.5.1; MARKETING_VERSION = 1.5.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornUITests; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_EMIT_LOC_STRINGS = NO;

View file

@ -14,7 +14,7 @@ struct MacTornApp: App {
.onAppear { .onAppear {
updateAppearance() updateAppearance()
} }
.onChange(of: appearanceModeRaw) { .onChange(of: appearanceModeRaw) { _ in
updateAppearance() updateAppearance()
} }
} label: { } label: {

View file

@ -1,11 +1,6 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
// MARK: - Constants
enum TornConstants {
static let developerID = 2362436
}
// MARK: - Root Response // MARK: - Root Response
struct TornResponse: Codable { struct TornResponse: Codable {
let name: String? let name: String?
@ -378,7 +373,7 @@ struct AttackResult: Codable, Identifiable {
let result: String? let result: String?
let respect: Double? let respect: Double?
let id: String var id: String { code ?? UUID().uuidString }
enum CodingKeys: String, CodingKey { enum CodingKeys: String, CodingKey {
case code case code
@ -391,33 +386,6 @@ struct AttackResult: Codable, Identifiable {
case result, respect case result, respect
} }
init(code: String?, timestampStarted: Int?, timestampEnded: Int?, attackerId: Int?, attackerName: String?, defenderId: Int?, defenderName: String?, result: String?, respect: Double?) {
self.code = code
self.timestampStarted = timestampStarted
self.timestampEnded = timestampEnded
self.attackerId = attackerId
self.attackerName = attackerName
self.defenderId = defenderId
self.defenderName = defenderName
self.result = result
self.respect = respect
self.id = code ?? UUID().uuidString
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
code = try container.decodeIfPresent(String.self, forKey: .code)
timestampStarted = try container.decodeIfPresent(Int.self, forKey: .timestampStarted)
timestampEnded = try container.decodeIfPresent(Int.self, forKey: .timestampEnded)
attackerId = try container.decodeIfPresent(Int.self, forKey: .attackerId)
attackerName = try container.decodeIfPresent(String.self, forKey: .attackerName)
defenderId = try container.decodeIfPresent(Int.self, forKey: .defenderId)
defenderName = try container.decodeIfPresent(String.self, forKey: .defenderName)
result = try container.decodeIfPresent(String.self, forKey: .result)
respect = try container.decodeIfPresent(Double.self, forKey: .respect)
id = code ?? UUID().uuidString
}
func opponentName(forUserId userId: Int) -> String { func opponentName(forUserId userId: Int) -> String {
let name: String? let name: String?
if attackerId == userId { if attackerId == userId {

View file

@ -7,77 +7,26 @@ enum PreferredBrowser: String, CaseIterable, Identifiable {
case firefox = "Firefox" case firefox = "Firefox"
case edge = "Microsoft Edge" case edge = "Microsoft Edge"
case brave = "Brave" case brave = "Brave"
case arc = "Arc"
case vivaldi = "Vivaldi"
case zen = "Zen"
case opera = "Opera"
case duckduckgo = "DuckDuckGo"
case orion = "Orion"
case tor = "Tor Browser"
case chromium = "Chromium"
case librewolf = "LibreWolf"
case waterfox = "Waterfox"
case atlas = "ChatGPT Atlas"
var id: String { rawValue } var id: String { rawValue }
var bundleIdentifiers: [String]? { var bundleIdentifier: String? {
switch self { switch self {
case .system: case .system:
return nil return nil
case .safari: case .safari:
return ["com.apple.Safari"] return "com.apple.Safari"
case .chrome: case .chrome:
return ["com.google.Chrome"] return "com.google.Chrome"
case .firefox: case .firefox:
return ["org.mozilla.firefox"] return "org.mozilla.firefox"
case .edge: case .edge:
return ["com.microsoft.edgemac"] return "com.microsoft.edgemac"
case .brave: case .brave:
return ["com.brave.Browser"] return "com.brave.Browser"
case .arc:
return ["company.thebrowser.Browser"]
case .vivaldi:
return ["com.vivaldi.Vivaldi"]
case .zen:
return ["app.zen-browser.zen"]
case .opera:
return ["com.operasoftware.Opera"]
case .duckduckgo:
return ["com.duckduckgo.macos.browser"]
case .orion:
return ["com.kagi.kagimacOS", "com.kagi.kagimacOS.RC"]
case .tor:
return ["com.torproject.tor"]
case .chromium:
return ["org.chromium.Chromium"]
case .librewolf:
return ["io.gitlab.librewolf-community"]
case .waterfox:
return ["net.waterfox.waterfox"]
case .atlas:
return ["com.openai.atlas"]
} }
} }
var installedApplicationURL: URL? {
guard let bundleIdentifiers else { return nil }
for bundleIdentifier in bundleIdentifiers {
if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) {
return appURL
}
}
return nil
}
var isInstalled: Bool {
self == .system || installedApplicationURL != nil
}
static func availableBrowsers() -> [PreferredBrowser] {
PreferredBrowser.allCases.filter { $0.isInstalled }
}
init(storedValue: String?) { init(storedValue: String?) {
guard let storedValue, guard let storedValue,
let value = PreferredBrowser(rawValue: storedValue) else { let value = PreferredBrowser(rawValue: storedValue) else {
@ -101,7 +50,8 @@ final class BrowserManager {
} }
let preference = PreferredBrowser(storedValue: UserDefaults.standard.string(forKey: "preferredBrowser")) let preference = PreferredBrowser(storedValue: UserDefaults.standard.string(forKey: "preferredBrowser"))
guard let appURL = preference.installedApplicationURL else { guard let bundleIdentifier = preference.bundleIdentifier,
let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) else {
NSWorkspace.shared.open(url) NSWorkspace.shared.open(url)
return return
} }

View file

@ -103,8 +103,14 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate {
/// Cancel all travel-related notifications /// Cancel all travel-related notifications
func cancelTravelNotifications() { func cancelTravelNotifications() {
let identifiers = TravelNotificationSetting.defaults.map { "\($0.id)_alert" } let identifiers = [
"travel_2min_alert",
"travel_1min_alert",
"travel_30sec_alert",
"travel_10sec_alert"
]
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers) UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: identifiers)
print("Cancelled travel notifications")
} }
/// Cancel a specific notification by identifier /// Cancel a specific notification by identifier

View file

@ -312,9 +312,6 @@ class AppState: ObservableObject {
} else { } else {
await updateItemError(itemId: itemId, error: "No listings") await updateItemError(itemId: itemId, error: "No listings")
} }
} else {
logger.error("Item \(itemId): failed to parse JSON response")
await updateItemError(itemId: itemId, error: "Parse Error")
} }
} catch { } catch {
logger.error("Item \(itemId) price fetch error: \(error.localizedDescription)") logger.error("Item \(itemId) price fetch error: \(error.localizedDescription)")
@ -584,7 +581,9 @@ class AppState: ObservableObject {
// Check if feedback prompt should be shown // Check if feedback prompt should be shown
self.checkFeedbackPrompt() self.checkFeedbackPrompt()
logger.info("Data updated, lastUpdated: \(self.lastUpdated?.description ?? "nil")") // Force UI update by triggering objectWillChange
self.objectWillChange.send()
logger.info("UI update triggered, lastUpdated: \(self.lastUpdated?.description ?? "nil")")
} }
} }

View file

@ -91,7 +91,7 @@ struct ContentView: View {
private var headerView: some View { private var headerView: some View {
HStack { HStack {
if let lastUpdated = appState.lastUpdated { if let lastUpdated = appState.lastUpdated {
Text("Updated: \(lastUpdated, formatter: Self.timeFormatter)") Text("Updated: \(lastUpdated, formatter: timeFormatter)")
.font(.caption2) .font(.caption2)
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
@ -177,9 +177,9 @@ struct ContentView: View {
.padding(.bottom, 8) .padding(.bottom, 8)
} }
private static let timeFormatter: DateFormatter = { private var timeFormatter: DateFormatter {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.timeStyle = .short formatter.timeStyle = .short
return formatter return formatter
}() }
} }

View file

@ -5,7 +5,7 @@ struct CreditsView: View {
@Binding var showCredits: Bool @Binding var showCredits: Bool
// MARK: - Developer // MARK: - Developer
private let developer = TornContributor(name: "bombel", tornID: TornConstants.developerID) private let developer = TornContributor(name: "bombel", tornID: 2362436)
// MARK: - Special Thanks // MARK: - Special Thanks
private let specialThanks: [TornContributor] = [ private let specialThanks: [TornContributor] = [
@ -92,9 +92,7 @@ struct CreditsView: View {
} }
Button { Button {
if let tornID = developer.tornID { openTornProfile(developer.tornID!)
openTornProfile(tornID)
}
} label: { } label: {
HStack { HStack {
Text(developer.name) Text(developer.name)

View file

@ -7,9 +7,9 @@ struct SettingsView: View {
@AppStorage("preferredBrowser") private var preferredBrowser: String = PreferredBrowser.system.rawValue @AppStorage("preferredBrowser") private var preferredBrowser: String = PreferredBrowser.system.rawValue
@State private var inputKey: String = "" @State private var inputKey: String = ""
@State private var showCredits: Bool = false @State private var showCredits: Bool = false
@State private var availableBrowsers: [PreferredBrowser] = PreferredBrowser.availableBrowsers()
private let developerID = TornConstants.developerID // Developer ID for tip feature (bombel)
private let developerID = 2362436
var body: some View { var body: some View {
if showCredits { if showCredits {
@ -73,7 +73,7 @@ struct SettingsView: View {
Text("2m").tag(120) Text("2m").tag(120)
} }
.pickerStyle(.segmented) .pickerStyle(.segmented)
.onChange(of: appState.refreshInterval) { .onChange(of: appState.refreshInterval) { _ in
Task { @MainActor in Task { @MainActor in
appState.startPolling() appState.startPolling()
} }
@ -114,7 +114,7 @@ struct SettingsView: View {
.frame(width: 20) .frame(width: 20)
Picker("Preferred Browser", selection: $preferredBrowser) { Picker("Preferred Browser", selection: $preferredBrowser) {
ForEach(availableBrowsers) { browser in ForEach(PreferredBrowser.allCases) { browser in
Text(browser.rawValue).tag(browser.rawValue) Text(browser.rawValue).tag(browser.rawValue)
} }
} }
@ -230,15 +230,6 @@ struct SettingsView: View {
.frame(width: 320) .frame(width: 320)
.onAppear { .onAppear {
inputKey = appState.apiKey inputKey = appState.apiKey
refreshAvailableBrowsers()
}
}
private func refreshAvailableBrowsers() {
let browsers = PreferredBrowser.availableBrowsers()
availableBrowsers = browsers
if !browsers.contains(where: { $0.rawValue == preferredBrowser }) {
preferredBrowser = PreferredBrowser.system.rawValue
} }
} }

View file

@ -136,8 +136,8 @@ final class MacTornUITests: XCTestCase {
// MARK: - UI Test Helpers // MARK: - UI Test Helpers
extension XCUIElement { extension XCUIElement {
/// Wait for element to appear within the given timeout /// Wait for element to exist with timeout
func waitForAppearance(timeout: TimeInterval = 5) -> Bool { func waitForExistence(timeout: TimeInterval = 5) -> Bool {
return self.waitForExistence(timeout: timeout) return self.waitForExistence(timeout: timeout)
} }

View file

@ -16,7 +16,6 @@ A native macOS menu bar app for monitoring your **Torn** game status.
## Documentation ## Documentation
For detailed documentation, visit the [MacTorn Wiki](https://github.com/pawelorzech/MacTorn/wiki). For detailed documentation, visit the [MacTorn Wiki](https://github.com/pawelorzech/MacTorn/wiki).
For community discussion and feedback, see the [Torn forums thread](https://www.torn.com/forums.php#/p=threads&f=67&t=16532308).
## Features ## Features