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:
parent
a55be3c6be
commit
e4c8f6927b
4 changed files with 175 additions and 6 deletions
58
CHANGELOG.md
Normal file
58
CHANGELOG.md
Normal 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
|
||||||
|
|
@ -639,7 +639,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.4.4;
|
MARKETING_VERSION = 1.4.5;
|
||||||
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;
|
||||||
|
|
@ -666,7 +666,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.4.4;
|
MARKETING_VERSION = 1.4.5;
|
||||||
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;
|
||||||
|
|
@ -684,7 +684,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.4.4;
|
MARKETING_VERSION = 1.4.5;
|
||||||
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;
|
||||||
|
|
@ -702,7 +702,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.4.4;
|
MARKETING_VERSION = 1.4.5;
|
||||||
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;
|
||||||
|
|
@ -720,7 +720,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.4.4;
|
MARKETING_VERSION = 1.4.5;
|
||||||
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;
|
||||||
|
|
@ -737,7 +737,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.4.4;
|
MARKETING_VERSION = 1.4.5;
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -115,6 +115,13 @@ struct Travel: Codable, Equatable {
|
||||||
|
|
||||||
/// Calculate remaining seconds based on fetch time (for live countdown)
|
/// Calculate remaining seconds based on fetch time (for live countdown)
|
||||||
func remainingSeconds(from fetchTime: Date) -> Int {
|
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 }
|
guard let timeLeft = timeLeft, timeLeft > 0 else { return 0 }
|
||||||
let elapsed = Int(Date().timeIntervalSince(fetchTime))
|
let elapsed = Int(Date().timeIntervalSince(fetchTime))
|
||||||
return max(0, timeLeft - elapsed)
|
return max(0, timeLeft - elapsed)
|
||||||
|
|
|
||||||
|
|
@ -132,4 +132,108 @@ final class TravelTests: XCTestCase {
|
||||||
let travel = try decode(Travel.self, from: json)
|
let travel = try decode(Travel.self, from: json)
|
||||||
XCTAssertFalse(travel.isTraveling)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue