commit 0a0f109fa1c3cfffee394f06b3c70a11d50d9af2 Author: Paweł Orzech Date: Sat Jan 17 17:57:45 2026 +0000 Initial commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..5d6bdfb Binary files /dev/null and b/.DS_Store differ diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52fe2f7 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..07028ca --- /dev/null +++ b/LICENSE @@ -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. diff --git a/MacTorn/.DS_Store b/MacTorn/.DS_Store new file mode 100644 index 0000000..745f169 Binary files /dev/null and b/MacTorn/.DS_Store differ diff --git a/MacTorn/MacTorn.xcodeproj/project.pbxproj b/MacTorn/MacTorn.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f3e3cd8 --- /dev/null +++ b/MacTorn/MacTorn.xcodeproj/project.pbxproj @@ -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 = ""; }; + AAA10002 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + AAA10003 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AAA10004 /* TornModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TornModels.swift; sourceTree = ""; }; + AAA10005 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; + AAA10006 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; + AAA10007 /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = ""; }; + AAA10008 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = ""; }; + AAA10009 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; + AAA10010 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 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 = ""; + }; + 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 = ""; + }; + AAA30002 /* Products */ = { + isa = PBXGroup; + children = ( + AAA10000 /* MacTorn.app */, + ); + name = Products; + sourceTree = ""; + }; + AAA30003 /* Models */ = { + isa = PBXGroup; + children = ( + AAA10004 /* TornModels.swift */, + ); + path = Models; + sourceTree = ""; + }; + AAA30004 /* ViewModels */ = { + isa = PBXGroup; + children = ( + AAA10005 /* AppState.swift */, + ); + path = ViewModels; + sourceTree = ""; + }; + AAA30005 /* Views */ = { + isa = PBXGroup; + children = ( + AAA10002 /* ContentView.swift */, + AAA10006 /* SettingsView.swift */, + AAA10007 /* StatusView.swift */, + AAA30006 /* Components */, + ); + path = Views; + sourceTree = ""; + }; + AAA30006 /* Components */ = { + isa = PBXGroup; + children = ( + AAA10008 /* ProgressBarView.swift */, + ); + path = Components; + sourceTree = ""; + }; + AAA30007 /* Utilities */ = { + isa = PBXGroup; + children = ( + AAA10009 /* NotificationManager.swift */, + ); + path = Utilities; + sourceTree = ""; + }; +/* 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 */; +} diff --git a/MacTorn/MacTorn/Assets.xcassets/AccentColor.colorset/Contents.json b/MacTorn/MacTorn/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..ddd81ff --- /dev/null +++ b/MacTorn/MacTorn/Assets.xcassets/AccentColor.colorset/Contents.json @@ -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 + } +} \ No newline at end of file diff --git a/MacTorn/MacTorn/Assets.xcassets/AppIcon.appiconset/Contents.json b/MacTorn/MacTorn/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..4662e62 --- /dev/null +++ b/MacTorn/MacTorn/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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 + } +} \ No newline at end of file diff --git a/MacTorn/MacTorn/Assets.xcassets/Contents.json b/MacTorn/MacTorn/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/MacTorn/MacTorn/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/MacTorn/MacTorn/Info.plist b/MacTorn/MacTorn/Info.plist new file mode 100644 index 0000000..bc929d5 --- /dev/null +++ b/MacTorn/MacTorn/Info.plist @@ -0,0 +1,24 @@ + + + + + LSUIElement + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/MacTorn/MacTorn/MacTornApp.swift b/MacTorn/MacTorn/MacTornApp.swift new file mode 100644 index 0000000..911d724 --- /dev/null +++ b/MacTorn/MacTorn/MacTornApp.swift @@ -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" + } +} diff --git a/MacTorn/MacTorn/Models/TornModels.swift b/MacTorn/MacTorn/Models/TornModels.swift new file mode 100644 index 0000000..6a2e6dc --- /dev/null +++ b/MacTorn/MacTorn/Models/TornModels.swift @@ -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"] + ) + ] +} diff --git a/MacTorn/MacTorn/Utilities/LaunchAtLoginManager.swift b/MacTorn/MacTorn/Utilities/LaunchAtLoginManager.swift new file mode 100644 index 0000000..270c800 --- /dev/null +++ b/MacTorn/MacTorn/Utilities/LaunchAtLoginManager.swift @@ -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)") + } + } +} diff --git a/MacTorn/MacTorn/Utilities/NotificationManager.swift b/MacTorn/MacTorn/Utilities/NotificationManager.swift new file mode 100644 index 0000000..ae131bf --- /dev/null +++ b/MacTorn/MacTorn/Utilities/NotificationManager.swift @@ -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)") + } + } + } +} diff --git a/MacTorn/MacTorn/Utilities/ShortcutsManager.swift b/MacTorn/MacTorn/Utilities/ShortcutsManager.swift new file mode 100644 index 0000000..4d02bdb --- /dev/null +++ b/MacTorn/MacTorn/Utilities/ShortcutsManager.swift @@ -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) + } +} diff --git a/MacTorn/MacTorn/ViewModels/AppState.swift b/MacTorn/MacTorn/ViewModels/AppState.swift new file mode 100644 index 0000000..ba65f82 --- /dev/null +++ b/MacTorn/MacTorn/ViewModels/AppState.swift @@ -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 +} diff --git a/MacTorn/MacTorn/Views/Components/ProgressBarView.swift b/MacTorn/MacTorn/Views/Components/ProgressBarView.swift new file mode 100644 index 0000000..f175148 --- /dev/null +++ b/MacTorn/MacTorn/Views/Components/ProgressBarView.swift @@ -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) +} diff --git a/MacTorn/MacTorn/Views/ContentView.swift b/MacTorn/MacTorn/Views/ContentView.swift new file mode 100644 index 0000000..b5b1812 --- /dev/null +++ b/MacTorn/MacTorn/Views/ContentView.swift @@ -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) + } +} diff --git a/MacTorn/MacTorn/Views/SettingsView.swift b/MacTorn/MacTorn/Views/SettingsView.swift new file mode 100644 index 0000000..55dd0f0 --- /dev/null +++ b/MacTorn/MacTorn/Views/SettingsView.swift @@ -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 + } + } +} diff --git a/MacTorn/MacTorn/Views/StatusView.swift b/MacTorn/MacTorn/Views/StatusView.swift new file mode 100644 index 0000000..fea38d4 --- /dev/null +++ b/MacTorn/MacTorn/Views/StatusView.swift @@ -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) + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..df8add9 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# MacTorn +Torn notifier app for macOS