Compare commits
10 commits
d1166d3218
...
56037885d0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56037885d0 | ||
|
|
22f6e5d8e4 | ||
|
|
464bfea0a4 | ||
|
|
032ff5887c | ||
|
|
0ea44f891a | ||
|
|
c46da1e13f | ||
|
|
7b7fe98666 | ||
|
|
bbeb89b9ba | ||
|
|
bbf977c6c0 | ||
|
|
4391a8b6b4 |
16 changed files with 131 additions and 37 deletions
|
|
@ -5,6 +5,11 @@ 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
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -651,7 +651,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.5.0;
|
MARKETING_VERSION = 1.5.1;
|
||||||
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.0;
|
MARKETING_VERSION = 1.5.1;
|
||||||
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.0;
|
MARKETING_VERSION = 1.5.1;
|
||||||
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.0;
|
MARKETING_VERSION = 1.5.1;
|
||||||
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.0;
|
MARKETING_VERSION = 1.5.1;
|
||||||
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.0;
|
MARKETING_VERSION = 1.5.1;
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ struct MacTornApp: App {
|
||||||
.onAppear {
|
.onAppear {
|
||||||
updateAppearance()
|
updateAppearance()
|
||||||
}
|
}
|
||||||
.onChange(of: appearanceModeRaw) { _ in
|
.onChange(of: appearanceModeRaw) {
|
||||||
updateAppearance()
|
updateAppearance()
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
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?
|
||||||
|
|
@ -373,7 +378,7 @@ struct AttackResult: Codable, Identifiable {
|
||||||
let result: String?
|
let result: String?
|
||||||
let respect: Double?
|
let respect: Double?
|
||||||
|
|
||||||
var id: String { code ?? UUID().uuidString }
|
let id: String
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case code
|
case code
|
||||||
|
|
@ -386,6 +391,33 @@ 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 {
|
||||||
|
|
|
||||||
|
|
@ -7,26 +7,77 @@ 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 bundleIdentifier: String? {
|
var bundleIdentifiers: [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 {
|
||||||
|
|
@ -50,8 +101,7 @@ final class BrowserManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
let preference = PreferredBrowser(storedValue: UserDefaults.standard.string(forKey: "preferredBrowser"))
|
let preference = PreferredBrowser(storedValue: UserDefaults.standard.string(forKey: "preferredBrowser"))
|
||||||
guard let bundleIdentifier = preference.bundleIdentifier,
|
guard let appURL = preference.installedApplicationURL else {
|
||||||
let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleIdentifier) else {
|
|
||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -103,14 +103,8 @@ class NotificationManager: NSObject, UNUserNotificationCenterDelegate {
|
||||||
|
|
||||||
/// Cancel all travel-related notifications
|
/// Cancel all travel-related notifications
|
||||||
func cancelTravelNotifications() {
|
func cancelTravelNotifications() {
|
||||||
let identifiers = [
|
let identifiers = TravelNotificationSetting.defaults.map { "\($0.id)_alert" }
|
||||||
"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
|
||||||
|
|
|
||||||
|
|
@ -312,6 +312,9 @@ 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)")
|
||||||
|
|
@ -581,9 +584,7 @@ class AppState: ObservableObject {
|
||||||
// Check if feedback prompt should be shown
|
// Check if feedback prompt should be shown
|
||||||
self.checkFeedbackPrompt()
|
self.checkFeedbackPrompt()
|
||||||
|
|
||||||
// Force UI update by triggering objectWillChange
|
logger.info("Data updated, lastUpdated: \(self.lastUpdated?.description ?? "nil")")
|
||||||
self.objectWillChange.send()
|
|
||||||
logger.info("UI update triggered, lastUpdated: \(self.lastUpdated?.description ?? "nil")")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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: timeFormatter)")
|
Text("Updated: \(lastUpdated, formatter: Self.timeFormatter)")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
|
@ -177,9 +177,9 @@ struct ContentView: View {
|
||||||
.padding(.bottom, 8)
|
.padding(.bottom, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var timeFormatter: DateFormatter {
|
private static let timeFormatter: DateFormatter = {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.timeStyle = .short
|
formatter.timeStyle = .short
|
||||||
return formatter
|
return formatter
|
||||||
}
|
}()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: 2362436)
|
private let developer = TornContributor(name: "bombel", tornID: TornConstants.developerID)
|
||||||
|
|
||||||
// MARK: - Special Thanks
|
// MARK: - Special Thanks
|
||||||
private let specialThanks: [TornContributor] = [
|
private let specialThanks: [TornContributor] = [
|
||||||
|
|
@ -92,7 +92,9 @@ struct CreditsView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
Button {
|
Button {
|
||||||
openTornProfile(developer.tornID!)
|
if let tornID = developer.tornID {
|
||||||
|
openTornProfile(tornID)
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
Text(developer.name)
|
Text(developer.name)
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
||||||
// Developer ID for tip feature (bombel)
|
private let developerID = TornConstants.developerID
|
||||||
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) { _ in
|
.onChange(of: appState.refreshInterval) {
|
||||||
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(PreferredBrowser.allCases) { browser in
|
ForEach(availableBrowsers) { browser in
|
||||||
Text(browser.rawValue).tag(browser.rawValue)
|
Text(browser.rawValue).tag(browser.rawValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,6 +230,15 @@ 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -136,8 +136,8 @@ final class MacTornUITests: XCTestCase {
|
||||||
// MARK: - UI Test Helpers
|
// MARK: - UI Test Helpers
|
||||||
|
|
||||||
extension XCUIElement {
|
extension XCUIElement {
|
||||||
/// Wait for element to exist with timeout
|
/// Wait for element to appear within the given timeout
|
||||||
func waitForExistence(timeout: TimeInterval = 5) -> Bool {
|
func waitForAppearance(timeout: TimeInterval = 5) -> Bool {
|
||||||
return self.waitForExistence(timeout: timeout)
|
return self.waitForExistence(timeout: timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ 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
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue