mirror of
https://github.com/pawelorzech/MacTorn.git
synced 2026-01-30 04:04:27 +00:00
Initial commit
This commit is contained in:
commit
0a0f109fa1
21 changed files with 1332 additions and 0 deletions
BIN
.DS_Store
vendored
Normal file
BIN
.DS_Store
vendored
Normal file
Binary file not shown.
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
62
.gitignore
vendored
Normal file
62
.gitignore
vendored
Normal 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
21
LICENSE
Normal 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
BIN
MacTorn/.DS_Store
vendored
Normal file
Binary file not shown.
396
MacTorn/MacTorn.xcodeproj/project.pbxproj
Normal file
396
MacTorn/MacTorn.xcodeproj/project.pbxproj
Normal 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 */;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
6
MacTorn/MacTorn/Assets.xcassets/Contents.json
Normal file
6
MacTorn/MacTorn/Assets.xcassets/Contents.json
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
24
MacTorn/MacTorn/Info.plist
Normal file
24
MacTorn/MacTorn/Info.plist
Normal 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>
|
||||||
29
MacTorn/MacTorn/MacTornApp.swift
Normal file
29
MacTorn/MacTorn/MacTornApp.swift
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
145
MacTorn/MacTorn/Models/TornModels.swift
Normal file
145
MacTorn/MacTorn/Models/TornModels.swift
Normal 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"]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
30
MacTorn/MacTorn/Utilities/LaunchAtLoginManager.swift
Normal file
30
MacTorn/MacTorn/Utilities/LaunchAtLoginManager.swift
Normal 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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
39
MacTorn/MacTorn/Utilities/NotificationManager.swift
Normal file
39
MacTorn/MacTorn/Utilities/NotificationManager.swift
Normal 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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
46
MacTorn/MacTorn/Utilities/ShortcutsManager.swift
Normal file
46
MacTorn/MacTorn/Utilities/ShortcutsManager.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
155
MacTorn/MacTorn/ViewModels/AppState.swift
Normal file
155
MacTorn/MacTorn/ViewModels/AppState.swift
Normal 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
|
||||||
|
}
|
||||||
57
MacTorn/MacTorn/Views/Components/ProgressBarView.swift
Normal file
57
MacTorn/MacTorn/Views/Components/ProgressBarView.swift
Normal 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)
|
||||||
|
}
|
||||||
40
MacTorn/MacTorn/Views/ContentView.swift
Normal file
40
MacTorn/MacTorn/Views/ContentView.swift
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
MacTorn/MacTorn/Views/SettingsView.swift
Normal file
40
MacTorn/MacTorn/Views/SettingsView.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
142
MacTorn/MacTorn/Views/StatusView.swift
Normal file
142
MacTorn/MacTorn/Views/StatusView.swift
Normal 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
2
README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# MacTorn
|
||||||
|
Torn notifier app for macOS
|
||||||
Loading…
Reference in a new issue