Compare commits

..

5 commits
v1.5.1 ... main

Author SHA1 Message Date
Paweł Orzech
56037885d0
Merge pull request #8 from pawelorzech/fix/medium-priority-phase2
Some checks failed
Tests / Unit Tests (push) Has been cancelled
Tests / UI Tests (push) Has been cancelled
Tests / Build Release (push) Has been cancelled
Fix safety issues, deprecated APIs, and code quality
2026-02-27 23:34:46 +01:00
Paweł Orzech
22f6e5d8e4
Fix safety issues, deprecated APIs, and code quality improvements
- Replace force unwrap developer.tornID! with safe optional binding
- Generate travel notification IDs dynamically from TravelNotificationSetting.defaults
  instead of hardcoded strings, preventing cancellation gaps
- Extract duplicated developer ID (2362436) into TornConstants enum
- Change DateFormatter from computed property to static let to avoid
  expensive re-creation on every render
- Remove redundant objectWillChange.send() already handled by @Published
- Migrate deprecated onChange(of:) { _ in } to new parameterless closure form
2026-02-27 23:34:25 +01:00
Paweł Orzech
464bfea0a4
Merge pull request #7 from pawelorzech/fix/critical-bugs-phase1
Fix 3 critical bugs: infinite recursion, stuck loading, unstable IDs
2026-02-27 23:30:52 +01:00
Paweł Orzech
032ff5887c
Fix 3 critical bugs: infinite recursion, stuck loading state, unstable IDs
- Rename waitForExistence() to waitForAppearance() in UI test helpers
  to fix infinite recursion caused by shadowing XCUIElement's built-in method
- Add else branch in fetchItemPrice() so watchlist items show "Parse Error"
  instead of staying stuck in loading state when JSON parsing fails
- Change AttackResult.id from computed property (generating new UUID on
  every access) to stored property assigned once at init, preventing
  SwiftUI re-render thrashing in ForEach
2026-02-27 23:30:15 +01:00
Paweł Orzech
0ea44f891a
Remove MacTorn v1.4.4–v1.4.7 zip files
Delete binary release archives MacTorn-v1.4.4.zip through MacTorn-v1.4.7.zip from the repository. Removes the four zipped release files from version control to clean up binary assets and reduce repo size.
2026-02-04 14:29:56 +01:00
12 changed files with 50 additions and 22 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

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

View file

@ -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 {

View file

@ -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

View file

@ -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")")
} }
} }

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: 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
} }()
} }

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: 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)

View file

@ -9,8 +9,7 @@ struct SettingsView: View {
@State private var showCredits: Bool = false @State private var showCredits: Bool = false
@State private var availableBrowsers: [PreferredBrowser] = PreferredBrowser.availableBrowsers() @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 {
@ -74,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()
} }

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 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)
} }