mirror of
https://github.com/pawelorzech/MacTorn.git
synced 2026-01-29 19:54: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