diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5e2ecbb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,58 @@ +# Changelog + +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/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.4.5] - 2025-01-25 + +### Fixed +- Improved travel timer accuracy by using API timestamp directly instead of calculating from fetch time offset +- Travel countdown now stays synchronized regardless of network delays or fetch timing + +### Added +- Comprehensive test coverage for travel timer calculations + +## [1.4.4] - Previous Release + +### Fixed +- Resolve Swift concurrency errors by extracting MainActor functions +- Fix watchlist item mutation to update via copy + +### Added +- Universal Binary support for Intel and Apple Silicon Macs +- Improved accessibility support +- Display cooldown labels as text instead of icons + +## [1.4.3] - Earlier Release + +### Added +- GitHub wiki documentation +- Migrated wiki to GitHub Wiki feature + +## [1.4.2] - Earlier Release + +### Changed +- Various bug fixes and improvements + +## [1.4.1] - Earlier Release + +### Changed +- Various bug fixes and improvements + +## [1.4] - Initial Public Release + +### Added +- Native macOS menu bar app for Torn game monitoring +- Status tab with live bars, cooldowns, and travel monitoring +- Travel tab with live countdown timer in menu bar +- Money tab with cash, vault, points display +- Attacks tab with battle stats and recent attacks +- Faction tab with chain status +- Watchlist tab for item price tracking +- Smart notifications for various game events +- Configurable refresh intervals +- Launch at login support +- Light and dark mode support +- Accessibility support with Reduce Transparency diff --git a/MacTorn/MacTorn.xcodeproj/project.pbxproj b/MacTorn/MacTorn.xcodeproj/project.pbxproj index f75a689..a99cf69 100644 --- a/MacTorn/MacTorn.xcodeproj/project.pbxproj +++ b/MacTorn/MacTorn.xcodeproj/project.pbxproj @@ -639,7 +639,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.4.4; + MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -666,7 +666,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); - MARKETING_VERSION = 1.4.4; + MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.app; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -684,7 +684,7 @@ DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.4.4; + MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -702,7 +702,7 @@ DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.4.4; + MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -720,7 +720,7 @@ DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.4.4; + MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; @@ -737,7 +737,7 @@ DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.4.4; + MARKETING_VERSION = 1.4.5; PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.MacTornUITests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; diff --git a/MacTorn/MacTorn/Models/TornModels.swift b/MacTorn/MacTorn/Models/TornModels.swift index a338279..80e3f80 100644 --- a/MacTorn/MacTorn/Models/TornModels.swift +++ b/MacTorn/MacTorn/Models/TornModels.swift @@ -115,6 +115,13 @@ struct Travel: Codable, Equatable { /// Calculate remaining seconds based on fetch time (for live countdown) func remainingSeconds(from fetchTime: Date) -> Int { + // Primary: Use timestamp directly if available (more accurate) + if let timestamp = timestamp, timestamp > 0 { + let now = Int(Date().timeIntervalSince1970) + return max(0, timestamp - now) + } + + // Fallback: Use timeLeft with fetchTime offset (backward compatibility) guard let timeLeft = timeLeft, timeLeft > 0 else { return 0 } let elapsed = Int(Date().timeIntervalSince(fetchTime)) return max(0, timeLeft - elapsed) diff --git a/MacTorn/MacTornTests/Models/TravelTests.swift b/MacTorn/MacTornTests/Models/TravelTests.swift index 279cce4..55dd347 100644 --- a/MacTorn/MacTornTests/Models/TravelTests.swift +++ b/MacTorn/MacTornTests/Models/TravelTests.swift @@ -132,4 +132,108 @@ final class TravelTests: XCTestCase { let travel = try decode(Travel.self, from: json) XCTAssertFalse(travel.isTraveling) } + + // MARK: - remainingSeconds Tests + + func testRemainingSeconds_usesTimestampDirectly() throws { + // Set arrival time 60 seconds in the future + let futureTimestamp = Int(Date().timeIntervalSince1970) + 60 + let json: [String: Any] = [ + "destination": "Japan", + "timestamp": futureTimestamp, + "departed": futureTimestamp - 1000, + "time_left": 60 + ] + let travel = try decode(Travel.self, from: json) + + // Even with a stale fetchTime, should use timestamp directly + let staleFetchTime = Date().addingTimeInterval(-300) // 5 minutes ago + let remaining = travel.remainingSeconds(from: staleFetchTime) + + // Should be approximately 60 seconds (allow 1-2 seconds tolerance for test execution) + XCTAssertGreaterThanOrEqual(remaining, 58) + XCTAssertLessThanOrEqual(remaining, 62) + } + + func testRemainingSeconds_fallsBackToTimeLeftWhenTimestampNil() throws { + let json: [String: Any] = [ + "destination": "Japan", + "time_left": 120 + ] + let travel = try decode(Travel.self, from: json) + + let fetchTime = Date() + let remaining = travel.remainingSeconds(from: fetchTime) + + // Should use timeLeft since timestamp is nil + XCTAssertGreaterThanOrEqual(remaining, 118) + XCTAssertLessThanOrEqual(remaining, 120) + } + + func testRemainingSeconds_fallsBackToTimeLeftWhenTimestampZero() throws { + let json: [String: Any] = [ + "destination": "Japan", + "timestamp": 0, + "time_left": 90 + ] + let travel = try decode(Travel.self, from: json) + + let fetchTime = Date() + let remaining = travel.remainingSeconds(from: fetchTime) + + // Should use timeLeft since timestamp is 0 + XCTAssertGreaterThanOrEqual(remaining, 88) + XCTAssertLessThanOrEqual(remaining, 90) + } + + func testRemainingSeconds_returnsZeroWhenArrivalPassed() throws { + // Set arrival time in the past + let pastTimestamp = Int(Date().timeIntervalSince1970) - 60 + let json: [String: Any] = [ + "destination": "Japan", + "timestamp": pastTimestamp, + "departed": pastTimestamp - 1000, + "time_left": 0 + ] + let travel = try decode(Travel.self, from: json) + + let remaining = travel.remainingSeconds(from: Date()) + XCTAssertEqual(remaining, 0) + } + + func testRemainingSeconds_consistentRegardlessOfFetchTime() throws { + // Set arrival time 120 seconds in the future + let futureTimestamp = Int(Date().timeIntervalSince1970) + 120 + let json: [String: Any] = [ + "destination": "Japan", + "timestamp": futureTimestamp, + "departed": futureTimestamp - 1000, + "time_left": 120 + ] + let travel = try decode(Travel.self, from: json) + + // Test with different fetchTimes - result should be the same + let recentFetchTime = Date() + let staleFetchTime = Date().addingTimeInterval(-60) + let veryOldFetchTime = Date().addingTimeInterval(-600) + + let remaining1 = travel.remainingSeconds(from: recentFetchTime) + let remaining2 = travel.remainingSeconds(from: staleFetchTime) + let remaining3 = travel.remainingSeconds(from: veryOldFetchTime) + + // All should return approximately the same value (within 1 second tolerance) + XCTAssertEqual(remaining1, remaining2, accuracy: 1) + XCTAssertEqual(remaining2, remaining3, accuracy: 1) + } + + func testRemainingSeconds_zeroWhenNotTraveling() throws { + let json: [String: Any] = [ + "destination": "Torn", + "time_left": 0 + ] + let travel = try decode(Travel.self, from: json) + + let remaining = travel.remainingSeconds(from: Date()) + XCTAssertEqual(remaining, 0) + } }