feat: Improve accessibility support and update README

- Fix Reduce Transparency mode in light mode (dark buttons issue)
- Lower opacity values for better readability when reduceTransparency is enabled
- Add Accessibility section to README documenting macOS accessibility support
- Update README with new light/dark mode screenshots
- Bump version to 1.4.4
This commit is contained in:
Paweł Orzech 2026-01-20 13:00:11 +00:00
parent 273fd31884
commit 21ac399269
No known key found for this signature in database
24 changed files with 180 additions and 63 deletions

Binary file not shown.

BIN
MacTorn-v1.4.4.zip Normal file

Binary file not shown.

View file

@ -30,6 +30,7 @@
AAA00021 /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10022 /* NetworkSession.swift */; }; AAA00021 /* NetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10022 /* NetworkSession.swift */; };
AAA00022 /* TravelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10023 /* TravelView.swift */; }; AAA00022 /* TravelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10023 /* TravelView.swift */; };
AAA00023 /* CreditsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10024 /* CreditsView.swift */; }; AAA00023 /* CreditsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10024 /* CreditsView.swift */; };
AAA00024 /* TransparencyEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10025 /* TransparencyEnvironment.swift */; };
/* Unit Tests */ /* Unit Tests */
BBB00001 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10001 /* MockNetworkSession.swift */; }; BBB00001 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10001 /* MockNetworkSession.swift */; };
BBB00002 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10002 /* TestHelpers.swift */; }; BBB00002 /* TestHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB10002 /* TestHelpers.swift */; };
@ -89,6 +90,7 @@
AAA10022 /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = "<group>"; }; AAA10022 /* NetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkSession.swift; sourceTree = "<group>"; };
AAA10023 /* TravelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TravelView.swift; sourceTree = "<group>"; }; AAA10023 /* TravelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TravelView.swift; sourceTree = "<group>"; };
AAA10024 /* CreditsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsView.swift; sourceTree = "<group>"; }; AAA10024 /* CreditsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditsView.swift; sourceTree = "<group>"; };
AAA10025 /* TransparencyEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransparencyEnvironment.swift; sourceTree = "<group>"; };
AAA10000 /* MacTorn.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTorn.app; sourceTree = BUILT_PRODUCTS_DIR; }; AAA10000 /* MacTorn.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTorn.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* Unit Test Files */ /* Unit Test Files */
BBB10001 /* MockNetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkSession.swift; sourceTree = "<group>"; }; BBB10001 /* MockNetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkSession.swift; sourceTree = "<group>"; };
@ -155,6 +157,7 @@
AAA30005 /* Views */, AAA30005 /* Views */,
AAA30007 /* Utilities */, AAA30007 /* Utilities */,
AAA30008 /* Networking */, AAA30008 /* Networking */,
AAA30009 /* Helpers */,
); );
path = MacTorn; path = MacTorn;
sourceTree = "<group>"; sourceTree = "<group>";
@ -233,6 +236,14 @@
path = Networking; path = Networking;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
AAA30009 /* Helpers */ = {
isa = PBXGroup;
children = (
AAA10025 /* TransparencyEnvironment.swift */,
);
path = Helpers;
sourceTree = "<group>";
};
/* Unit Tests Groups */ /* Unit Tests Groups */
BBB30000 /* MacTornTests */ = { BBB30000 /* MacTornTests */ = {
isa = PBXGroup; isa = PBXGroup;
@ -445,6 +456,7 @@
AAA00021 /* NetworkSession.swift in Sources */, AAA00021 /* NetworkSession.swift in Sources */,
AAA00022 /* TravelView.swift in Sources */, AAA00022 /* TravelView.swift in Sources */,
AAA00023 /* CreditsView.swift in Sources */, AAA00023 /* CreditsView.swift in Sources */,
AAA00024 /* TransparencyEnvironment.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -627,7 +639,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.4.3; MARKETING_VERSION = 1.4.4;
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;
@ -654,7 +666,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MARKETING_VERSION = 1.4.3; MARKETING_VERSION = 1.4.4;
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;
@ -672,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.3; MARKETING_VERSION = 1.4.4;
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;
@ -690,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.3; MARKETING_VERSION = 1.4.4;
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;
@ -708,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.3; MARKETING_VERSION = 1.4.4;
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;
@ -725,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.3; MARKETING_VERSION = 1.4.4;
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;

View file

@ -0,0 +1,13 @@
import SwiftUI
// Environment key for reduce transparency setting
private struct ReduceTransparencyKey: EnvironmentKey {
static let defaultValue: Bool = false
}
extension EnvironmentValues {
var reduceTransparency: Bool {
get { self[ReduceTransparencyKey.self] }
set { self[ReduceTransparencyKey.self] = newValue }
}
}

View file

@ -3,16 +3,37 @@ import SwiftUI
@main @main
struct MacTornApp: App { struct MacTornApp: App {
@StateObject private var appState = AppState() @StateObject private var appState = AppState()
@AppStorage("appearanceMode") private var appearanceModeRaw: String = AppearanceMode.system.rawValue
@AppStorage("reduceTransparency") private var reduceTransparency: Bool = false
var body: some Scene { var body: some Scene {
MenuBarExtra { MenuBarExtra {
ContentView() ContentView()
.environmentObject(appState) .environmentObject(appState)
.environment(\.reduceTransparency, reduceTransparency)
.onAppear {
updateAppearance()
}
.onChange(of: appearanceModeRaw) { _ in
updateAppearance()
}
} label: { } label: {
MenuBarLabel(appState: appState) MenuBarLabel(appState: appState)
} }
.menuBarExtraStyle(.window) .menuBarExtraStyle(.window)
} }
private func updateAppearance() {
let mode = AppearanceMode(rawValue: appearanceModeRaw) ?? .system
switch mode {
case .system:
NSApp.appearance = nil
case .light:
NSApp.appearance = NSAppearance(named: .aqua)
case .dark:
NSApp.appearance = NSAppearance(named: .darkAqua)
}
}
} }
// MARK: - Menu Bar Label // MARK: - Menu Bar Label

View file

@ -5,11 +5,27 @@ import os.log
private let logger = Logger(subsystem: "com.mactorn", category: "AppState") private let logger = Logger(subsystem: "com.mactorn", category: "AppState")
// MARK: - Appearance
enum AppearanceMode: String, CaseIterable {
case system = "System"
case light = "Light"
case dark = "Dark"
var colorScheme: ColorScheme? {
switch self {
case .system: return nil
case .light: return .light
case .dark: return .dark
}
}
}
@MainActor @MainActor
class AppState: ObservableObject { class AppState: ObservableObject {
// MARK: - Persisted // MARK: - Persisted
@AppStorage("apiKey") var apiKey: String = "" @AppStorage("apiKey") var apiKey: String = ""
@AppStorage("refreshInterval") var refreshInterval: Int = 30 @AppStorage("refreshInterval") var refreshInterval: Int = 30
@AppStorage("appearanceMode") var appearanceMode: String = AppearanceMode.system.rawValue
// MARK: - Published State // MARK: - Published State
@Published var data: TornResponse? @Published var data: TornResponse?

View file

@ -2,6 +2,7 @@ import SwiftUI
struct AttacksView: View { struct AttacksView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Environment(\.reduceTransparency) private var reduceTransparency
var body: some View { var body: some View {
ScrollView { ScrollView {
@ -39,9 +40,9 @@ struct AttacksView: View {
} }
} }
.padding() .padding()
.background(Color.red.opacity(0.05)) .background(Color.red.opacity(reduceTransparency ? 0.25 : 0.05))
.cornerRadius(8) .cornerRadius(8)
// Recent Attacks // Recent Attacks
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack { HStack {
@ -91,9 +92,9 @@ struct AttacksView: View {
} }
} }
.padding() .padding()
.background(Color.orange.opacity(0.05)) .background(Color.orange.opacity(reduceTransparency ? 0.25 : 0.05))
.cornerRadius(8) .cornerRadius(8)
// Actions // Actions
HStack(spacing: 8) { HStack(spacing: 8) {
ActionButton(title: "Attack", icon: "bolt.fill", color: .red) { ActionButton(title: "Attack", icon: "bolt.fill", color: .red) {
@ -134,10 +135,11 @@ struct AttacksView: View {
// MARK: - Stat Item // MARK: - Stat Item
struct StatItem: View { struct StatItem: View {
@Environment(\.reduceTransparency) private var reduceTransparency
let label: String let label: String
let value: String let value: String
let color: Color let color: Color
var body: some View { var body: some View {
VStack(spacing: 2) { VStack(spacing: 2) {
Text(value) Text(value)
@ -149,7 +151,7 @@ struct StatItem: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 4) .padding(.vertical, 4)
.background(color.opacity(0.1)) .background(color.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(4) .cornerRadius(4)
} }
} }

View file

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
struct ChainView: View { struct ChainView: View {
@Environment(\.reduceTransparency) private var reduceTransparency
let chain: Chain let chain: Chain
let fetchTime: Date let fetchTime: Date
@ -25,7 +26,7 @@ struct ChainView: View {
} }
} }
.padding(8) .padding(8)
.background(color.opacity(0.1)) .background(color.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(8) .cornerRadius(8)
} }
} else if chain.isOnCooldown { } else if chain.isOnCooldown {

View file

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
struct EventsView: View { struct EventsView: View {
@Environment(\.reduceTransparency) private var reduceTransparency
let events: [TornEvent] let events: [TornEvent]
var body: some View { var body: some View {
@ -33,7 +34,7 @@ struct EventsView: View {
} }
} }
.padding(8) .padding(8)
.background(Color.blue.opacity(0.05)) .background(Color.blue.opacity(reduceTransparency ? 0.25 : 0.05))
.cornerRadius(8) .cornerRadius(8)
} }

View file

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
struct ProgressBarView: View { struct ProgressBarView: View {
@Environment(\.reduceTransparency) private var reduceTransparency
let label: String let label: String
let current: Int let current: Int
let maximum: Int let maximum: Int
@ -40,10 +41,10 @@ struct ProgressBarView: View {
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
// Background track // Background track
RoundedRectangle(cornerRadius: 4) RoundedRectangle(cornerRadius: 4)
.fill(Color.gray.opacity(0.3)) .fill(Color.gray.opacity(reduceTransparency ? 0.5 : 0.3))
.overlay( .overlay(
RoundedRectangle(cornerRadius: 4) RoundedRectangle(cornerRadius: 4)
.stroke(color.opacity(0.3), lineWidth: 1) .stroke(color.opacity(reduceTransparency ? 0.5 : 0.3), lineWidth: 1)
) )
// Filled progress // Filled progress
@ -51,13 +52,13 @@ struct ProgressBarView: View {
RoundedRectangle(cornerRadius: 4) RoundedRectangle(cornerRadius: 4)
.fill( .fill(
LinearGradient( LinearGradient(
colors: [color, color.opacity(0.7)], colors: [color, color.opacity(reduceTransparency ? 0.9 : 0.7)],
startPoint: .leading, startPoint: .leading,
endPoint: .trailing endPoint: .trailing
) )
) )
.frame(width: max(4, geometry.size.width * progress)) .frame(width: max(4, geometry.size.width * progress))
.shadow(color: color.opacity(0.5), radius: 2, x: 0, y: 0) .shadow(color: color.opacity(reduceTransparency ? 0.7 : 0.5), radius: 2, x: 0, y: 0)
} }
} }
} }

View file

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
struct StatusBadgesView: View { struct StatusBadgesView: View {
@Environment(\.reduceTransparency) private var reduceTransparency
let status: Status let status: Status
var body: some View { var body: some View {
@ -18,10 +19,10 @@ struct StatusBadgesView: View {
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 4) .padding(.vertical, 4)
.background(Color.red.opacity(0.1)) .background(Color.red.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(6) .cornerRadius(6)
} }
if status.isInJail { if status.isInJail {
HStack(spacing: 4) { HStack(spacing: 4) {
Image(systemName: "lock.fill") Image(systemName: "lock.fill")
@ -34,7 +35,7 @@ struct StatusBadgesView: View {
} }
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 4) .padding(.vertical, 4)
.background(Color.orange.opacity(0.1)) .background(Color.orange.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(6) .cornerRadius(6)
} }
} }

View file

@ -22,6 +22,7 @@ enum AppTab: String, CaseIterable {
struct ContentView: View { struct ContentView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Environment(\.reduceTransparency) private var reduceTransparency
@State private var showSettings = false @State private var showSettings = false
@State private var currentTab: AppTab = .status @State private var currentTab: AppTab = .status
@ -55,8 +56,8 @@ struct ContentView: View {
// Loading Overlay // Loading Overlay
if appState.isLoading && appState.lastUpdated == nil { if appState.isLoading && appState.lastUpdated == nil {
Color.black.opacity(0.4) (reduceTransparency ? Color(.windowBackgroundColor) : Color.black.opacity(0.4))
.background(.ultraThinMaterial) .background(reduceTransparency ? AnyShapeStyle(Color(.windowBackgroundColor)) : AnyShapeStyle(.ultraThinMaterial))
VStack(spacing: 12) { VStack(spacing: 12) {
ProgressView() ProgressView()
@ -106,7 +107,7 @@ struct ContentView: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 6) .padding(.vertical, 6)
.background(currentTab == tab ? Color.accentColor.opacity(0.2) : Color.clear) .background(currentTab == tab ? Color.accentColor.opacity(reduceTransparency ? 0.3 : 0.2) : Color.clear)
.cornerRadius(6) .cornerRadius(6)
.contentShape(Rectangle()) // Make entire area clickable .contentShape(Rectangle()) // Make entire area clickable
} }

View file

@ -1,6 +1,7 @@
import SwiftUI import SwiftUI
struct CreditsView: View { struct CreditsView: View {
@Environment(\.reduceTransparency) private var reduceTransparency
@Binding var showCredits: Bool @Binding var showCredits: Bool
// MARK: - Developer // MARK: - Developer
@ -107,7 +108,7 @@ struct CreditsView: View {
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.padding(10) .padding(10)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background(Color.orange.opacity(0.1)) .background(Color.orange.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(8) .cornerRadius(8)
} }
} }
@ -139,7 +140,7 @@ struct CreditsView: View {
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.padding(10) .padding(10)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background(Color.secondary.opacity(0.1)) .background(Color.secondary.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(8) .cornerRadius(8)
} }
} }
@ -171,7 +172,7 @@ struct CreditsView: View {
.foregroundColor(.accentColor) .foregroundColor(.accentColor)
.padding(10) .padding(10)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background(Color.secondary.opacity(0.1)) .background(Color.secondary.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(8) .cornerRadius(8)
} }
} }
@ -199,7 +200,7 @@ struct CreditsView: View {
} }
.padding(10) .padding(10)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background(Color.secondary.opacity(0.1)) .background(Color.secondary.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(8) .cornerRadius(8)
} }
} }

View file

@ -2,6 +2,7 @@ import SwiftUI
struct FactionView: View { struct FactionView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Environment(\.reduceTransparency) private var reduceTransparency
var body: some View { var body: some View {
ScrollView { ScrollView {
@ -33,7 +34,7 @@ struct FactionView: View {
.foregroundColor(chainColor(faction.chain)) .foregroundColor(chainColor(faction.chain))
} }
.padding(8) .padding(8)
.background(chainColor(faction.chain).opacity(0.1)) .background(chainColor(faction.chain).opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(6) .cornerRadius(6)
} }
@ -58,9 +59,9 @@ struct FactionView: View {
} }
} }
.padding() .padding()
.background(Color.blue.opacity(0.05)) .background(Color.blue.opacity(reduceTransparency ? 0.25 : 0.05))
.cornerRadius(8) .cornerRadius(8)
// Armory Quick Actions // Armory Quick Actions
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
HStack { HStack {
@ -85,9 +86,9 @@ struct FactionView: View {
} }
} }
.padding() .padding()
.background(Color.purple.opacity(0.05)) .background(Color.purple.opacity(reduceTransparency ? 0.25 : 0.05))
.cornerRadius(8) .cornerRadius(8)
// Actions // Actions
HStack(spacing: 8) { HStack(spacing: 8) {
ActionButton(title: "Faction", icon: "person.3.fill", color: .blue) { ActionButton(title: "Faction", icon: "person.3.fill", color: .blue) {
@ -138,11 +139,12 @@ struct FactionView: View {
// MARK: - Armory Button // MARK: - Armory Button
struct ArmoryButton: View { struct ArmoryButton: View {
@Environment(\.reduceTransparency) private var reduceTransparency
let title: String let title: String
let icon: String let icon: String
let color: Color let color: Color
let action: () -> Void let action: () -> Void
var body: some View { var body: some View {
Button(action: action) { Button(action: action) {
VStack(spacing: 2) { VStack(spacing: 2) {
@ -153,7 +155,7 @@ struct ArmoryButton: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 6) .padding(.vertical, 6)
.background(color.opacity(0.15)) .background(color.opacity(reduceTransparency ? 0.4 : 0.15))
.foregroundColor(color) .foregroundColor(color)
.cornerRadius(6) .cornerRadius(6)
} }

View file

@ -2,6 +2,7 @@ import SwiftUI
struct MoneyView: View { struct MoneyView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Environment(\.reduceTransparency) private var reduceTransparency
var body: some View { var body: some View {
ScrollView { ScrollView {
@ -72,9 +73,9 @@ struct MoneyView: View {
} }
} }
.padding() .padding()
.background(Color.green.opacity(0.05)) .background(Color.green.opacity(reduceTransparency ? 0.25 : 0.05))
.cornerRadius(8) .cornerRadius(8)
// Actions // Actions
HStack(spacing: 8) { HStack(spacing: 8) {
ActionButton(title: "Send Money", icon: "paperplane.fill", color: .blue) { ActionButton(title: "Send Money", icon: "paperplane.fill", color: .blue) {
@ -112,11 +113,12 @@ struct MoneyView: View {
// MARK: - Action Button Component // MARK: - Action Button Component
struct ActionButton: View { struct ActionButton: View {
@Environment(\.reduceTransparency) private var reduceTransparency
let title: String let title: String
let icon: String let icon: String
let color: Color let color: Color
let action: () -> Void let action: () -> Void
var body: some View { var body: some View {
Button(action: action) { Button(action: action) {
VStack(spacing: 4) { VStack(spacing: 4) {
@ -127,7 +129,7 @@ struct ActionButton: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 8) .padding(.vertical, 8)
.background(color.opacity(0.1)) .background(color.opacity(reduceTransparency ? 0.4 : 0.1))
.foregroundColor(color) .foregroundColor(color)
.cornerRadius(8) .cornerRadius(8)
} }

View file

@ -2,6 +2,7 @@ import SwiftUI
struct PropertiesView: View { struct PropertiesView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Environment(\.reduceTransparency) private var reduceTransparency
var body: some View { var body: some View {
ScrollView { ScrollView {
@ -58,6 +59,7 @@ struct PropertiesView: View {
// MARK: - Property Card // MARK: - Property Card
struct PropertyCard: View { struct PropertyCard: View {
@Environment(\.reduceTransparency) private var reduceTransparency
let property: PropertyInfo let property: PropertyInfo
var body: some View { var body: some View {
@ -72,7 +74,7 @@ struct PropertyCard: View {
.foregroundColor(.orange) .foregroundColor(.orange)
.padding(.horizontal, 6) .padding(.horizontal, 6)
.padding(.vertical, 2) .padding(.vertical, 2)
.background(Color.orange.opacity(0.2)) .background(Color.orange.opacity(reduceTransparency ? 0.5 : 0.2))
.cornerRadius(4) .cornerRadius(4)
} }
} }
@ -112,7 +114,7 @@ struct PropertyCard: View {
} }
} }
.padding() .padding()
.background(Color.brown.opacity(0.05)) .background(Color.brown.opacity(reduceTransparency ? 0.25 : 0.05))
.cornerRadius(8) .cornerRadius(8)
} }

View file

@ -2,6 +2,8 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@AppStorage("appearanceMode") private var appearanceMode: String = AppearanceMode.system.rawValue
@AppStorage("reduceTransparency") private var reduceTransparency: Bool = false
@State private var inputKey: String = "" @State private var inputKey: String = ""
@State private var showCredits: Bool = false @State private var showCredits: Bool = false
@ -88,6 +90,30 @@ struct SettingsView: View {
)) ))
.toggleStyle(.switch) .toggleStyle(.switch)
} }
// Appearance Mode
HStack {
Image(systemName: "moon.circle")
.foregroundColor(.secondary)
.frame(width: 20)
Picker("Appearance", selection: $appearanceMode) {
ForEach(AppearanceMode.allCases, id: \.self) { mode in
Text(mode.rawValue).tag(mode.rawValue)
}
}
.pickerStyle(.segmented)
.labelsHidden()
}
// Reduce Transparency (Accessibility)
HStack {
Image(systemName: "eye")
.foregroundColor(.secondary)
.frame(width: 20)
Toggle("Reduce Transparency", isOn: $reduceTransparency)
.toggleStyle(.switch)
}
} }
.padding(.horizontal) .padding(.horizontal)
@ -116,16 +142,16 @@ struct SettingsView: View {
.font(.caption) .font(.caption)
.padding(.vertical, 6) .padding(.vertical, 6)
.padding(.horizontal, 12) .padding(.horizontal, 12)
.background(Color.purple.opacity(0.15)) .background(Color.purple.opacity(reduceTransparency ? 0.4 : 0.15))
.cornerRadius(6) .cornerRadius(6)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
.padding(.vertical, 8) .padding(.vertical, 8)
.padding(.horizontal) .padding(.horizontal)
.background(Color.purple.opacity(0.05)) .background(Color.purple.opacity(reduceTransparency ? 0.25 : 0.05))
.cornerRadius(8) .cornerRadius(8)
// Update Section // Update Section
if let update = appState.updateAvailable { if let update = appState.updateAvailable {
VStack(spacing: 8) { VStack(spacing: 8) {
@ -146,7 +172,7 @@ struct SettingsView: View {
} }
.padding(10) .padding(10)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.background(Color.green.opacity(0.1)) .background(Color.green.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(8) .cornerRadius(8)
} }

View file

@ -2,6 +2,7 @@ import SwiftUI
struct StatusView: View { struct StatusView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Environment(\.reduceTransparency) private var reduceTransparency
var body: some View { var body: some View {
ScrollView { ScrollView {
@ -109,7 +110,7 @@ struct StatusView: View {
} }
.padding(.horizontal, 10) .padding(.horizontal, 10)
.padding(.vertical, 6) .padding(.vertical, 6)
.background(Color.blue.opacity(0.1)) .background(Color.blue.opacity(reduceTransparency ? 0.2 : 0.1))
.cornerRadius(6) .cornerRadius(6)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -150,7 +151,7 @@ struct StatusView: View {
} }
.padding(8) .padding(8)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.background(Color.blue.opacity(0.1)) .background(Color.blue.opacity(reduceTransparency ? 0.2 : 0.1))
.cornerRadius(8) .cornerRadius(8)
.transaction { $0.animation = nil } .transaction { $0.animation = nil }
} }
@ -240,7 +241,7 @@ struct StatusView: View {
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 4) .padding(.vertical, 4)
.padding(.horizontal, 6) .padding(.horizontal, 6)
.background(Color.accentColor.opacity(0.1)) .background(Color.accentColor.opacity(reduceTransparency ? 0.2 : 0.1))
.cornerRadius(4) .cornerRadius(4)
} }
.buttonStyle(.plain) .buttonStyle(.plain)

View file

@ -4,6 +4,7 @@ import AppKit
// MARK: - Flying Status View (separate for proper live updates) // MARK: - Flying Status View (separate for proper live updates)
struct FlyingStatusView: View { struct FlyingStatusView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Environment(\.reduceTransparency) private var reduceTransparency
let destination: String let destination: String
let timestamp: Int let timestamp: Int
let departed: Int let departed: Int
@ -60,7 +61,7 @@ struct FlyingStatusView: View {
GeometryReader { geometry in GeometryReader { geometry in
ZStack(alignment: .leading) { ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4) RoundedRectangle(cornerRadius: 4)
.fill(Color.gray.opacity(0.2)) .fill(Color.gray.opacity(reduceTransparency ? 0.5 : 0.2))
.frame(height: 8) .frame(height: 8)
RoundedRectangle(cornerRadius: 4) RoundedRectangle(cornerRadius: 4)
@ -72,7 +73,7 @@ struct FlyingStatusView: View {
} }
} }
.padding() .padding()
.background(Color.blue.opacity(0.1)) .background(Color.blue.opacity(reduceTransparency ? 0.2 : 0.1))
.cornerRadius(12) .cornerRadius(12)
.transaction { $0.animation = nil } .transaction { $0.animation = nil }
} }
@ -80,6 +81,7 @@ struct FlyingStatusView: View {
struct TravelView: View { struct TravelView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Environment(\.reduceTransparency) private var reduceTransparency
var body: some View { var body: some View {
ScrollView { ScrollView {
@ -164,7 +166,7 @@ struct TravelView: View {
.buttonStyle(.plain) .buttonStyle(.plain)
} }
.padding() .padding()
.background(Color.orange.opacity(0.1)) .background(Color.orange.opacity(reduceTransparency ? 0.2 : 0.1))
.cornerRadius(12) .cornerRadius(12)
} }
@ -183,7 +185,7 @@ struct TravelView: View {
Spacer() Spacer()
} }
.padding() .padding()
.background(Color.green.opacity(0.1)) .background(Color.green.opacity(reduceTransparency ? 0.2 : 0.1))
.cornerRadius(12) .cornerRadius(12)
} }
@ -212,7 +214,7 @@ struct TravelView: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 10) .padding(.vertical, 10)
.background(Color.accentColor.opacity(0.1)) .background(Color.accentColor.opacity(reduceTransparency ? 0.2 : 0.1))
.cornerRadius(8) .cornerRadius(8)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -248,7 +250,7 @@ struct TravelView: View {
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 8) .padding(.vertical, 8)
.background(Color.accentColor.opacity(0.1)) .background(Color.accentColor.opacity(reduceTransparency ? 0.2 : 0.1))
.cornerRadius(8) .cornerRadius(8)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -283,7 +285,7 @@ struct TravelView: View {
} }
} }
.padding() .padding()
.background(Color.secondary.opacity(0.1)) .background(Color.secondary.opacity(reduceTransparency ? 0.2 : 0.1))
.cornerRadius(8) .cornerRadius(8)
} }
} }
@ -312,7 +314,7 @@ struct TravelView: View {
.font(.caption) .font(.caption)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 8) .padding(.vertical, 8)
.background(Color.accentColor.opacity(0.1)) .background(Color.accentColor.opacity(reduceTransparency ? 0.2 : 0.1))
.cornerRadius(6) .cornerRadius(6)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -329,7 +331,7 @@ struct TravelView: View {
.font(.caption) .font(.caption)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 8) .padding(.vertical, 8)
.background(Color.accentColor.opacity(0.1)) .background(Color.accentColor.opacity(reduceTransparency ? 0.2 : 0.1))
.cornerRadius(6) .cornerRadius(6)
} }
.buttonStyle(.plain) .buttonStyle(.plain)

View file

@ -2,6 +2,7 @@ import SwiftUI
struct WatchlistView: View { struct WatchlistView: View {
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
@Environment(\.reduceTransparency) private var reduceTransparency
@State private var showAddItem = false @State private var showAddItem = false
var body: some View { var body: some View {
@ -56,7 +57,7 @@ struct WatchlistView: View {
.lineLimit(1) .lineLimit(1)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, 6) .padding(.vertical, 6)
.background(Color.green.opacity(0.1)) .background(Color.green.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(4) .cornerRadius(4)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -64,10 +65,10 @@ struct WatchlistView: View {
} }
} }
.padding(8) .padding(8)
.background(Color.gray.opacity(0.1)) .background(Color.gray.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(6) .cornerRadius(6)
} }
// Watchlist Items with Prices // Watchlist Items with Prices
if appState.watchlistItems.isEmpty && !showAddItem { if appState.watchlistItems.isEmpty && !showAddItem {
VStack(spacing: 8) { VStack(spacing: 8) {
@ -132,6 +133,7 @@ struct WatchlistView: View {
// MARK: - Watchlist Price Row // MARK: - Watchlist Price Row
struct WatchlistPriceRow: View { struct WatchlistPriceRow: View {
@Environment(\.reduceTransparency) private var reduceTransparency
let item: WatchlistItem let item: WatchlistItem
let onOpen: () -> Void let onOpen: () -> Void
let onRemove: () -> Void let onRemove: () -> Void
@ -200,10 +202,10 @@ struct WatchlistPriceRow: View {
.buttonStyle(.plain) .buttonStyle(.plain)
} }
.padding(8) .padding(8)
.background(Color.gray.opacity(0.1)) .background(Color.gray.opacity(reduceTransparency ? 0.4 : 0.1))
.cornerRadius(6) .cornerRadius(6)
} }
private func formatPrice(_ price: Int) -> String { private func formatPrice(_ price: Int) -> String {
if price >= 1_000_000 { if price >= 1_000_000 {
return String(format: "$%.1fM", Double(price) / 1_000_000) return String(format: "$%.1fM", Double(price) / 1_000_000)

View file

@ -8,7 +8,9 @@ A native macOS menu bar app for monitoring your **Torn** game status.
![License](https://img.shields.io/badge/License-MIT-green) ![License](https://img.shields.io/badge/License-MIT-green)
<p align="center"> <p align="center">
<img src="app.png?v=1.2" alt="MacTorn Screenshot" width="600"> <img src="app_light_1.png" alt="MacTorn Light Mode" width="320">
&nbsp;&nbsp;
<img src="app_dark_1.png" alt="MacTorn Dark Mode" width="320">
</p> </p>
## Features ## Features
@ -57,6 +59,14 @@ A native macOS menu bar app for monitoring your **Torn** game status.
- **🚀 Launch at Login**: Start seamlessly with macOS. - **🚀 Launch at Login**: Start seamlessly with macOS.
- **⚡️ Optimized Startup**: Non-blocking data fetching for instant UI responsiveness. - **⚡️ Optimized Startup**: Non-blocking data fetching for instant UI responsiveness.
## Accessibility
MacTorn respects macOS accessibility settings:
- **Reduce Transparency**: When enabled in System Settings → Accessibility → Display, the app uses solid backgrounds instead of translucent materials for better readability
- **Light & Dark Mode**: Full support for both appearance modes with optimized contrast
- **Color-coded indicators**: Status bars and badges use distinct colors that work well in both modes
## Installation ## Installation
1. Download the latest release from [Releases](https://github.com/pawelorzech/MacTorn/releases) 1. Download the latest release from [Releases](https://github.com/pawelorzech/MacTorn/releases)

BIN
app.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

BIN
app_dark_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

BIN
app_light_1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB