Initial commit

This commit is contained in:
Paweł Orzech 2026-01-17 17:57:45 +00:00
commit 0a0f109fa1
No known key found for this signature in database
21 changed files with 1332 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

62
.gitignore vendored Normal file
View file

@ -0,0 +1,62 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## User settings
xcuserdata/
## Obj-C/Swift specific
*.hmap
## App packaging
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
# Pods/
#
# Add this line if you want to avoid checking in source code from the Xcode workspace
# *.xcworkspace
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build/
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots/**/*.png
fastlane/test_output

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Paweł Orzech
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

BIN
MacTorn/.DS_Store vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,396 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 56;
objects = {
/* Begin PBXBuildFile section */
AAA00001 /* MacTornApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10001 /* MacTornApp.swift */; };
AAA00002 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10002 /* ContentView.swift */; };
AAA00003 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AAA10003 /* Assets.xcassets */; };
AAA00004 /* TornModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10004 /* TornModels.swift */; };
AAA00005 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10005 /* AppState.swift */; };
AAA00006 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10006 /* SettingsView.swift */; };
AAA00007 /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10007 /* StatusView.swift */; };
AAA00008 /* ProgressBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10008 /* ProgressBarView.swift */; };
AAA00009 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA10009 /* NotificationManager.swift */; };
/* End PBXBuildFile section */
/* Begin PBXFileReference section */
AAA10001 /* MacTornApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacTornApp.swift; sourceTree = "<group>"; };
AAA10002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
AAA10003 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
AAA10004 /* TornModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TornModels.swift; sourceTree = "<group>"; };
AAA10005 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
AAA10006 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
AAA10007 /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
AAA10008 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = "<group>"; };
AAA10009 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
AAA10010 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
AAA10000 /* MacTorn.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MacTorn.app; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
AAA20001 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
AAA30000 = {
isa = PBXGroup;
children = (
AAA30001 /* MacTorn */,
AAA30002 /* Products */,
);
sourceTree = "<group>";
};
AAA30001 /* MacTorn */ = {
isa = PBXGroup;
children = (
AAA10001 /* MacTornApp.swift */,
AAA10010 /* Info.plist */,
AAA10003 /* Assets.xcassets */,
AAA30003 /* Models */,
AAA30004 /* ViewModels */,
AAA30005 /* Views */,
AAA30007 /* Utilities */,
);
path = MacTorn;
sourceTree = "<group>";
};
AAA30002 /* Products */ = {
isa = PBXGroup;
children = (
AAA10000 /* MacTorn.app */,
);
name = Products;
sourceTree = "<group>";
};
AAA30003 /* Models */ = {
isa = PBXGroup;
children = (
AAA10004 /* TornModels.swift */,
);
path = Models;
sourceTree = "<group>";
};
AAA30004 /* ViewModels */ = {
isa = PBXGroup;
children = (
AAA10005 /* AppState.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
AAA30005 /* Views */ = {
isa = PBXGroup;
children = (
AAA10002 /* ContentView.swift */,
AAA10006 /* SettingsView.swift */,
AAA10007 /* StatusView.swift */,
AAA30006 /* Components */,
);
path = Views;
sourceTree = "<group>";
};
AAA30006 /* Components */ = {
isa = PBXGroup;
children = (
AAA10008 /* ProgressBarView.swift */,
);
path = Components;
sourceTree = "<group>";
};
AAA30007 /* Utilities */ = {
isa = PBXGroup;
children = (
AAA10009 /* NotificationManager.swift */,
);
path = Utilities;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
AAA40000 /* MacTorn */ = {
isa = PBXNativeTarget;
buildConfigurationList = AAA60000 /* Build configuration list for PBXNativeTarget "MacTorn" */;
buildPhases = (
AAA50000 /* Sources */,
AAA20001 /* Frameworks */,
AAA50001 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = MacTorn;
productName = MacTorn;
productReference = AAA10000 /* MacTorn.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
AAA00000 /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1500;
LastUpgradeCheck = 1500;
TargetAttributes = {
AAA40000 = {
CreatedOnToolsVersion = 15.0;
};
};
};
buildConfigurationList = AAA60001 /* Build configuration list for PBXProject "MacTorn" */;
compatibilityVersion = "Xcode 14.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = AAA30000;
productRefGroup = AAA30002 /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
AAA40000 /* MacTorn */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
AAA50001 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
AAA00003 /* Assets.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
AAA50000 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
AAA00001 /* MacTornApp.swift in Sources */,
AAA00002 /* ContentView.swift in Sources */,
AAA00004 /* TornModels.swift in Sources */,
AAA00005 /* AppState.swift in Sources */,
AAA00006 /* SettingsView.swift in Sources */,
AAA00007 /* StatusView.swift in Sources */,
AAA00008 /* ProgressBarView.swift in Sources */,
AAA00009 /* NotificationManager.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin XCBuildConfiguration section */
AAA70000 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
};
AAA70001 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
SWIFT_COMPILATION_MODE = wholemodule;
};
name = Release;
};
AAA70002 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = MacTorn/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
AAA70003 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = NO;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = MacTorn/Info.plist;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mactorn.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
AAA60000 /* Build configuration list for PBXNativeTarget "MacTorn" */ = {
isa = XCConfigurationList;
buildConfigurations = (
AAA70002 /* Debug */,
AAA70003 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
AAA60001 /* Build configuration list for PBXProject "MacTorn" */ = {
isa = XCConfigurationList;
buildConfigurations = (
AAA70000 /* Debug */,
AAA70001 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = AAA00000 /* Project object */;
}

View file

@ -0,0 +1,38 @@
{
"colors": [
{
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "0.000",
"green": "0.000",
"red": "0.000"
}
},
"idiom": "universal"
},
{
"appearances": [
{
"appearance": "luminosity",
"value": "dark"
}
],
"color": {
"color-space": "srgb",
"components": {
"alpha": "1.000",
"blue": "1.000",
"green": "1.000",
"red": "1.000"
}
},
"idiom": "universal"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View file

@ -0,0 +1,58 @@
{
"images": [
{
"idiom": "mac",
"scale": "1x",
"size": "16x16"
},
{
"idiom": "mac",
"scale": "2x",
"size": "16x16"
},
{
"idiom": "mac",
"scale": "1x",
"size": "32x32"
},
{
"idiom": "mac",
"scale": "2x",
"size": "32x32"
},
{
"idiom": "mac",
"scale": "1x",
"size": "128x128"
},
{
"idiom": "mac",
"scale": "2x",
"size": "128x128"
},
{
"idiom": "mac",
"scale": "1x",
"size": "256x256"
},
{
"idiom": "mac",
"scale": "2x",
"size": "256x256"
},
{
"idiom": "mac",
"scale": "1x",
"size": "512x512"
},
{
"idiom": "mac",
"scale": "2x",
"size": "512x512"
}
],
"info": {
"author": "xcode",
"version": 1
}
}

View file

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSUIElement</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>

View file

@ -0,0 +1,29 @@
import SwiftUI
@main
struct MacTornApp: App {
@StateObject private var appState = AppState()
var body: some Scene {
MenuBarExtra {
ContentView()
.environmentObject(appState)
} label: {
Image(systemName: menuBarIcon)
.renderingMode(.template)
}
.menuBarExtraStyle(.window)
}
private var menuBarIcon: String {
if appState.errorMsg != nil {
return "exclamationmark.triangle.fill"
}
if let bars = appState.data?.bars {
if bars.energy.current >= bars.energy.maximum {
return "bolt.fill"
}
}
return "bolt"
}
}

View file

@ -0,0 +1,145 @@
import Foundation
// MARK: - Root Response
struct TornResponse: Codable {
let bars: Bars?
let cooldowns: Cooldowns?
let travel: Travel?
let error: TornError?
}
// MARK: - Bars
struct Bar: Codable, Equatable {
let current: Int
let maximum: Int
let increment: Double
let interval: Int
let ticktime: Int
let fulltime: Int
}
struct Bars: Codable, Equatable {
let energy: Bar
let nerve: Bar
let life: Bar
let happy: Bar
}
// MARK: - Cooldowns
struct Cooldowns: Codable, Equatable {
let drug: Int
let medical: Int
let booster: Int
}
// MARK: - Travel
struct Travel: Codable, Equatable {
let destination: String
let timestamp: Int
let departed: Int
let timeLeft: Int
enum CodingKeys: String, CodingKey {
case destination
case timestamp
case departed
case timeLeft = "time_left"
}
var isAbroad: Bool {
destination != "Torn" && timeLeft == 0
}
var isTraveling: Bool {
timeLeft > 0
}
var arrivalDate: Date? {
guard isTraveling else { return nil }
return Date(timeIntervalSince1970: TimeInterval(timestamp))
}
}
// MARK: - Error
struct TornError: Codable {
let code: Int
let error: String
}
// MARK: - API Configuration
enum TornAPI {
static let baseURL = "https://api.torn.com/user/"
static let selections = "bars,cooldowns,travel"
static func url(for apiKey: String) -> URL? {
URL(string: "\(baseURL)?selections=\(selections)&key=\(apiKey)")
}
}
// MARK: - Keyboard Shortcuts
struct KeyboardShortcut: Identifiable, Codable, Equatable {
let id: String
var name: String
var url: String
var keyEquivalent: String
var modifiers: [String]
static let defaults: [KeyboardShortcut] = [
KeyboardShortcut(
id: "home",
name: "Home",
url: "https://www.torn.com/",
keyEquivalent: "h",
modifiers: ["command", "shift"]
),
KeyboardShortcut(
id: "items",
name: "Items",
url: "https://www.torn.com/item.php",
keyEquivalent: "i",
modifiers: ["command", "shift"]
),
KeyboardShortcut(
id: "gym",
name: "Gym",
url: "https://www.torn.com/gym.php",
keyEquivalent: "g",
modifiers: ["command", "shift"]
),
KeyboardShortcut(
id: "crimes",
name: "Crimes",
url: "https://www.torn.com/crimes.php",
keyEquivalent: "c",
modifiers: ["command", "shift"]
),
KeyboardShortcut(
id: "mission",
name: "Missions",
url: "https://www.torn.com/missions.php",
keyEquivalent: "m",
modifiers: ["command", "shift"]
),
KeyboardShortcut(
id: "travel",
name: "Travel",
url: "https://www.torn.com/travelagency.php",
keyEquivalent: "t",
modifiers: ["command", "shift"]
),
KeyboardShortcut(
id: "hospital",
name: "Hospital",
url: "https://www.torn.com/hospitalview.php",
keyEquivalent: "o",
modifiers: ["command", "shift"]
),
KeyboardShortcut(
id: "faction",
name: "Faction",
url: "https://www.torn.com/factions.php",
keyEquivalent: "f",
modifiers: ["command", "shift"]
)
]
}

View file

@ -0,0 +1,30 @@
import Foundation
import ServiceManagement
@MainActor
class LaunchAtLoginManager: ObservableObject {
@Published var isEnabled: Bool = false
private let service = SMAppService.mainApp
init() {
updateStatus()
}
func updateStatus() {
isEnabled = service.status == .enabled
}
func toggle() {
do {
if isEnabled {
try service.unregister()
} else {
try service.register()
}
updateStatus()
} catch {
print("Launch at Login error: \(error)")
}
}
}

View file

@ -0,0 +1,39 @@
import Foundation
import UserNotifications
class NotificationManager {
static let shared = NotificationManager()
private init() {}
func requestPermission() async {
do {
let granted = try await UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge])
if granted {
print("Notification permission granted")
}
} catch {
print("Notification permission error: \(error)")
}
}
func send(title: String, body: String) {
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
let request = UNNotificationRequest(
identifier: UUID().uuidString,
content: content,
trigger: nil // Immediate
)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("Notification error: \(error)")
}
}
}
}

View file

@ -0,0 +1,46 @@
import Foundation
import SwiftUI
@MainActor
class ShortcutsManager: ObservableObject {
@Published var shortcuts: [KeyboardShortcut] = []
private let storageKey = "customShortcuts"
init() {
loadShortcuts()
}
func loadShortcuts() {
if let data = UserDefaults.standard.data(forKey: storageKey),
let saved = try? JSONDecoder().decode([KeyboardShortcut].self, from: data) {
shortcuts = saved
} else {
shortcuts = KeyboardShortcut.defaults
saveShortcuts()
}
}
func saveShortcuts() {
if let data = try? JSONEncoder().encode(shortcuts) {
UserDefaults.standard.set(data, forKey: storageKey)
}
}
func updateShortcut(_ shortcut: KeyboardShortcut) {
if let index = shortcuts.firstIndex(where: { $0.id == shortcut.id }) {
shortcuts[index] = shortcut
saveShortcuts()
}
}
func resetToDefaults() {
shortcuts = KeyboardShortcut.defaults
saveShortcuts()
}
func openURL(_ urlString: String) {
guard let url = URL(string: urlString) else { return }
NSWorkspace.shared.open(url)
}
}

View file

@ -0,0 +1,155 @@
import Foundation
import Combine
import SwiftUI
@MainActor
class AppState: ObservableObject {
// MARK: - Persisted
@AppStorage("apiKey") var apiKey: String = ""
// MARK: - Published State
@Published var data: TornResponse?
@Published var lastUpdated: Date?
@Published var errorMsg: String?
@Published var isLoading: Bool = false
// MARK: - State Comparison
private var previousBars: Bars?
private var previousCooldowns: Cooldowns?
// MARK: - Timer
private var timerCancellable: AnyCancellable?
init() {
startPolling()
Task {
await NotificationManager.shared.requestPermission()
}
}
func startPolling() {
// Initial fetch
fetchData()
// Set up 30-second polling
timerCancellable = Timer.publish(every: 30, on: .main, in: .common)
.autoconnect()
.sink { [weak self] _ in
self?.fetchData()
}
}
func stopPolling() {
timerCancellable?.cancel()
timerCancellable = nil
}
func refreshNow() {
fetchData()
}
func fetchData() {
guard !apiKey.isEmpty else {
errorMsg = "API Key required"
return
}
guard let url = TornAPI.url(for: apiKey) else {
errorMsg = "Invalid URL"
return
}
isLoading = true
errorMsg = nil
Task {
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
throw APIError.invalidResponse
}
switch httpResponse.statusCode {
case 200:
let decoded = try JSONDecoder().decode(TornResponse.self, from: data)
if let error = decoded.error {
self.errorMsg = "API Error: \(error.error)"
self.data = nil
} else {
// Check for notifications before updating
checkNotifications(newData: decoded)
self.data = decoded
self.lastUpdated = Date()
self.errorMsg = nil
// Store for comparison
self.previousBars = decoded.bars
self.previousCooldowns = decoded.cooldowns
}
case 403, 404:
self.errorMsg = "Invalid API Key"
self.data = nil
default:
self.errorMsg = "HTTP Error: \(httpResponse.statusCode)"
}
} catch {
self.errorMsg = error.localizedDescription
}
self.isLoading = false
}
}
private func checkNotifications(newData: TornResponse) {
guard let prev = previousBars, let current = newData.bars else { return }
// Energy full notification
if prev.energy.current < prev.energy.maximum &&
current.energy.current >= current.energy.maximum {
NotificationManager.shared.send(
title: "Energy Full! ⚡️",
body: "Your energy bar is now full (\(current.energy.maximum)/\(current.energy.maximum))"
)
}
// Nerve full notification
if prev.nerve.current < prev.nerve.maximum &&
current.nerve.current >= current.nerve.maximum {
NotificationManager.shared.send(
title: "Nerve Full! 💪",
body: "Your nerve bar is now full (\(current.nerve.maximum)/\(current.nerve.maximum))"
)
}
// Cooldown notifications
if let prevCD = previousCooldowns, let currentCD = newData.cooldowns {
if prevCD.drug > 0 && currentCD.drug == 0 {
NotificationManager.shared.send(
title: "Drug Ready! 💊",
body: "Drug cooldown has ended"
)
}
if prevCD.medical > 0 && currentCD.medical == 0 {
NotificationManager.shared.send(
title: "Medical Ready! 🏥",
body: "Medical cooldown has ended"
)
}
if prevCD.booster > 0 && currentCD.booster == 0 {
NotificationManager.shared.send(
title: "Booster Ready! 🚀",
body: "Booster cooldown has ended"
)
}
}
}
}
// MARK: - Errors
enum APIError: Error {
case invalidResponse
case invalidData
}

View file

@ -0,0 +1,57 @@
import SwiftUI
struct ProgressBarView: View {
let label: String
let current: Int
let maximum: Int
let color: Color
let icon: String
private var progress: Double {
guard maximum > 0 else { return 0 }
return Double(current) / Double(maximum)
}
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Image(systemName: icon)
.foregroundColor(color)
.font(.caption)
Text(label)
.font(.caption.bold())
Spacer()
Text("\(current)/\(maximum)")
.font(.caption.monospacedDigit())
.foregroundColor(.secondary)
}
GeometryReader { geometry in
ZStack(alignment: .leading) {
// Background
RoundedRectangle(cornerRadius: 4)
.fill(color.opacity(0.2))
// Foreground
RoundedRectangle(cornerRadius: 4)
.fill(color)
.frame(width: geometry.size.width * progress)
}
}
.frame(height: 8)
}
}
}
#Preview {
VStack(spacing: 16) {
ProgressBarView(label: "Energy", current: 75, maximum: 100, color: .green, icon: "bolt.fill")
ProgressBarView(label: "Nerve", current: 25, maximum: 50, color: .red, icon: "flame.fill")
ProgressBarView(label: "Happy", current: 1000, maximum: 1000, color: .yellow, icon: "face.smiling.fill")
}
.padding()
.frame(width: 280)
}

View file

@ -0,0 +1,40 @@
import SwiftUI
struct ContentView: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack(spacing: 0) {
if appState.apiKey.isEmpty {
SettingsView()
} else {
StatusView()
}
Divider()
.padding(.vertical, 8)
// Footer buttons
HStack {
Button("Settings") {
appState.apiKey = "" // Go back to settings
}
.buttonStyle(.plain)
.foregroundColor(.secondary)
Spacer()
Button("Quit") {
NSApplication.shared.terminate(nil)
}
.buttonStyle(.plain)
.foregroundColor(.secondary)
}
.font(.caption)
.padding(.horizontal)
.padding(.bottom, 8)
}
.frame(width: 280)
.environmentObject(appState)
}
}

View file

@ -0,0 +1,40 @@
import SwiftUI
struct SettingsView: View {
@EnvironmentObject var appState: AppState
@State private var inputKey: String = ""
var body: some View {
VStack(spacing: 16) {
Image(systemName: "bolt.circle.fill")
.font(.system(size: 48))
.foregroundColor(.orange)
Text("MacTorn")
.font(.title2.bold())
Text("Enter your Torn API Key")
.font(.caption)
.foregroundColor(.secondary)
SecureField("API Key", text: $inputKey)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
Button("Save & Connect") {
appState.apiKey = inputKey.trimmingCharacters(in: .whitespacesAndNewlines)
appState.refreshNow()
}
.buttonStyle(.borderedProminent)
.disabled(inputKey.isEmpty)
Link("Get API Key from Torn",
destination: URL(string: "https://www.torn.com/preferences.php#tab=api")!)
.font(.caption)
}
.padding()
.onAppear {
inputKey = appState.apiKey
}
}
}

View file

@ -0,0 +1,142 @@
import SwiftUI
struct StatusView: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack(alignment: .leading, spacing: 12) {
// Header
HStack {
Text("Torn Status")
.font(.headline)
Spacer()
if appState.isLoading {
ProgressView()
.scaleEffect(0.6)
} else {
Button {
appState.refreshNow()
} label: {
Image(systemName: "arrow.clockwise")
}
.buttonStyle(.plain)
.foregroundColor(.secondary)
}
}
// Last updated
if let lastUpdated = appState.lastUpdated {
Text("Updated: \(lastUpdated, formatter: timeFormatter)")
.font(.caption2)
.foregroundColor(.secondary)
}
// Error state
if let error = appState.errorMsg {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.orange)
Text(error)
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
}
// Bars
if let bars = appState.data?.bars {
VStack(spacing: 8) {
ProgressBarView(
label: "Energy",
current: bars.energy.current,
maximum: bars.energy.maximum,
color: .green,
icon: "bolt.fill"
)
ProgressBarView(
label: "Nerve",
current: bars.nerve.current,
maximum: bars.nerve.maximum,
color: .red,
icon: "flame.fill"
)
ProgressBarView(
label: "Happy",
current: bars.happy.current,
maximum: bars.happy.maximum,
color: .yellow,
icon: "face.smiling.fill"
)
ProgressBarView(
label: "Life",
current: bars.life.current,
maximum: bars.life.maximum,
color: .pink,
icon: "heart.fill"
)
}
}
// Cooldowns
if let cooldowns = appState.data?.cooldowns {
Divider()
.padding(.vertical, 4)
Text("Cooldowns")
.font(.caption.bold())
.foregroundColor(.secondary)
HStack(spacing: 16) {
CooldownItem(label: "Drug", seconds: cooldowns.drug, icon: "pills.fill")
CooldownItem(label: "Medical", seconds: cooldowns.medical, icon: "cross.case.fill")
CooldownItem(label: "Booster", seconds: cooldowns.booster, icon: "arrow.up.circle.fill")
}
}
}
.padding()
}
private var timeFormatter: DateFormatter {
let formatter = DateFormatter()
formatter.timeStyle = .short
return formatter
}
}
// MARK: - Cooldown Item
struct CooldownItem: View {
let label: String
let seconds: Int
let icon: String
var body: some View {
VStack(spacing: 2) {
Image(systemName: icon)
.foregroundColor(seconds > 0 ? .orange : .green)
Text(formattedTime)
.font(.caption2.monospacedDigit())
.foregroundColor(seconds > 0 ? .primary : .green)
}
.frame(maxWidth: .infinity)
}
private var formattedTime: String {
if seconds <= 0 {
return "Ready"
}
let minutes = seconds / 60
let secs = seconds % 60
if minutes >= 60 {
let hours = minutes / 60
let mins = minutes % 60
return String(format: "%d:%02d:%02d", hours, mins, secs)
}
return String(format: "%d:%02d", minutes, secs)
}
}

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# MacTorn
Torn notifier app for macOS