fix: Improve travel timer accuracy with direct timestamp usage

- Use API timestamp directly for travel countdown calculations
- Add fallback to timeLeft for backward compatibility
- Add comprehensive test coverage for remainingSeconds method
- Bump version to 1.4.5
- Add CHANGELOG.md
This commit is contained in:
Paweł Orzech 2026-01-25 12:02:56 +01:00
parent a55be3c6be
commit e4c8f6927b
No known key found for this signature in database
4 changed files with 175 additions and 6 deletions

58
CHANGELOG.md Normal file
View file

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

View file

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

View file

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

View file

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