├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .gitmodules ├── LiveSplitKit ├── LiveSplitKit.xcodeproj │ ├── project.pbxproj │ └── xcshareddata │ │ └── xcschemes │ │ ├── LiveSplitKit.xcscheme │ │ └── LiveSplitKitTests.xcscheme ├── LiveSplitKit │ ├── CLiveSplitCore │ │ └── livesplit_core.h │ ├── Color.swift │ ├── Font+NSFont.swift │ ├── Font.swift │ ├── LSKit.swift │ ├── LiveSplitCore Extensions.swift │ ├── LiveSplitCore.swift │ ├── LiveSplitCore │ │ └── liblivesplit_core.a │ ├── LiveSplitKit.docc │ │ └── LiveSplitKit.md │ ├── LiveSplitKit.h │ ├── RunEditorState.swift │ ├── TimeComparison.swift │ └── module.map └── LiveSplitKitTests │ ├── LSCoreTesting.swift │ └── LiveSplitKitTests.swift ├── Odyssey.split.zip ├── Podfile ├── Podfile.lock ├── Programming-Notes.md ├── README.md ├── SplitsIOKit ├── .gitignore ├── .swiftpm │ └── xcode │ │ ├── package.xcworkspace │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── xcschemes │ │ └── SplitsIOKit.xcscheme ├── Package.swift ├── README.md ├── Sources │ └── SplitsIOKit │ │ ├── SRL.swift │ │ ├── SpeedrunCom │ │ ├── Game.swift │ │ ├── Run.swift │ │ └── SpeedrunCom.swift │ │ ├── SplitsIOAuth.swift │ │ ├── SplitsIOExchangeFormat.swift │ │ └── SplitsIOKit.swift └── Tests │ ├── LinuxMain.swift │ └── SplitsIOKitTests │ ├── SpeedrunComTests.swift │ ├── SplitsIOKitTests.swift │ └── XCTestManifests.swift ├── Splitter.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── Splitter - Release.xcscheme │ ├── SplitterTests.xcscheme │ └── SplitterUITests.xcscheme ├── Splitter.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ ├── WorkspaceSettings.xcsettings │ └── xcschemes │ └── Splitter.xcscheme ├── Splitter ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── icon_128x128.png │ │ ├── icon_128x128@2x.png │ │ ├── icon_16x16.png │ │ ├── icon_16x16@2x.png │ │ ├── icon_256x256.png │ │ ├── icon_256x256@2x.png │ │ ├── icon_32x32.png │ │ ├── icon_32x32@2x.png │ │ ├── icon_512x512.png │ │ └── icon_512x512@2x.png │ ├── Contents.json │ ├── CurrentSplitColor.colorset │ │ └── Contents.json │ ├── Document Icon │ │ ├── Contents.json │ │ ├── docBG.iconset │ │ │ ├── icon_128x128.png │ │ │ ├── icon_128x128@2x.png │ │ │ ├── icon_16x16.png │ │ │ ├── icon_16x16@2x.png │ │ │ ├── icon_256x256.png │ │ │ ├── icon_256x256@2x.png │ │ │ ├── icon_32x32.png │ │ │ ├── icon_32x32@2x.png │ │ │ ├── icon_512x512.png │ │ │ └── icon_512x512@2x.png │ │ ├── livesplit.iconset │ │ │ ├── icon_128x128.png │ │ │ ├── icon_128x128@2x.png │ │ │ ├── icon_16x16.png │ │ │ ├── icon_16x16@2x.png │ │ │ ├── icon_256x256.png │ │ │ ├── icon_256x256@2x.png │ │ │ ├── icon_32x32.png │ │ │ ├── icon_32x32@2x.png │ │ │ ├── icon_512x512.png │ │ │ └── icon_512x512@2x.png │ │ ├── splitDocIcon.iconset │ │ │ ├── icon_128x128.png │ │ │ ├── icon_128x128@2x.png │ │ │ ├── icon_16x16.png │ │ │ ├── icon_16x16@2x.png │ │ │ ├── icon_256x256.png │ │ │ ├── icon_256x256@2x.png │ │ │ ├── icon_32x32.png │ │ │ ├── icon_32x32@2x.png │ │ │ ├── icon_512x512.png │ │ │ └── icon_512x512@2x.png │ │ └── splitsioDoc.iconset │ │ │ ├── icon_128x128.png │ │ │ ├── icon_128x128@2x.png │ │ │ ├── icon_16x16.png │ │ │ ├── icon_16x16@2x.png │ │ │ ├── icon_256x256.png │ │ │ ├── icon_256x256@2x.png │ │ │ ├── icon_32x32.png │ │ │ ├── icon_32x32@2x.png │ │ │ ├── icon_512x512.png │ │ │ └── icon_512x512@2x.png │ ├── Game Controller.imageset │ │ ├── Contents.json │ │ └── Gamepad 2.png │ ├── Hotkeys.imageset │ │ ├── Contents.json │ │ └── Icon.png │ ├── accentColor.colorset │ │ └── Contents.json │ ├── chevron.down.1.png │ ├── chevron.down.2.png │ ├── chevron.down.png │ ├── chevron.up.1.png │ ├── chevron.up.2.png │ ├── chevron.up.png │ ├── downarrow.imageset │ │ ├── Contents.json │ │ └── downarrow.png │ ├── flag.imageset │ │ ├── Contents.json │ │ └── flagsmaller.png │ ├── folder.imageset │ │ ├── Contents.json │ │ └── folder.pdf │ ├── gearshape.imageset │ │ ├── Contents.json │ │ └── gearshape.pdf │ ├── info.imageset │ │ ├── Contents.json │ │ └── info.png │ ├── lines.imageset │ │ ├── Contents.json │ │ ├── List@1x.png │ │ └── List@2x.png │ ├── menuBarIcon.imageset │ │ ├── Contents.json │ │ └── Splitter Glyph 2.svg │ ├── pencil.imageset │ │ ├── Contents.json │ │ └── PencilIcon.png │ ├── plus.app.imageset │ │ ├── Contents.json │ │ └── plus.app.pdf │ ├── splitsio.imageset │ │ ├── Contents.json │ │ └── logo-578e4861acb4fde546fe2a2f1cc2d750da727b0eefef55e876aa40c6cbc4ebd8.png │ ├── square.and.arrow.down.fill.imageset │ │ ├── Contents.json │ │ └── square.and.arrow.down.fill.pdf │ ├── stop.imageset │ │ ├── Contents.json │ │ ├── stop.1.png │ │ ├── stop.2.png │ │ └── stop.png │ ├── stopSquare.imageset │ │ ├── Contents.json │ │ ├── stop.1.png │ │ ├── stop.2.png │ │ └── stop.png │ └── trash.imageset │ │ ├── Contents.json │ │ └── trash.4.png ├── Credits.html ├── Data │ ├── CommonPaths.swift │ ├── Documents │ │ ├── Common │ │ │ ├── SplitterDoc.swift │ │ │ └── SplitterDocBundle.swift │ │ ├── Native │ │ │ ├── Document.swift │ │ │ ├── SplitterAppearance.swift │ │ │ └── runInfo.swift │ │ ├── Other Formats │ │ │ ├── LiveSplit │ │ │ │ ├── LiveSplit.swift │ │ │ │ └── lss.swift │ │ │ └── SplitsIO │ │ │ │ └── SplitsIODoc.swift │ │ └── SplitterDocumentController.swift │ ├── Time Conversion.swift │ └── TimeSplit.swift ├── Extensions.swift ├── Info.plist ├── IntentHandling.swift ├── Intents.intentdefinition ├── LiveSplit Core │ └── liblivesplit_core.a ├── Prefs │ ├── DebugPrefsViewController.swift │ ├── DebugPrefsViewController.xib │ ├── GeneralPrefsViewController.swift │ ├── GeneralPrefsViewController.xib │ ├── Hotkeys │ │ ├── AdvancedHotkeyViewController.swift │ │ ├── AdvancedHotkeyViewController.xib │ │ ├── DefaultHotkeys.swift │ │ ├── GlobalKeybindPrefs.swift │ │ ├── HotkeysViewController.swift │ │ ├── HotkeysViewController.xib │ │ ├── KeybindHandlers.swift │ │ └── Storage.swift │ ├── PrefPanes.swift │ └── SettingsKeys.swift ├── SetupForUITesting.swift ├── Splitter-Bridging-Header.h ├── Splitter.entitlements ├── SplitterDocLSS.icns ├── SplitterDocSplitsio.icns ├── StatusBarController.swift ├── Super Mario 3D Land Any%.json ├── Super Mario Odyssey Any%.json ├── TimerCore │ ├── CodableLayout.swift │ ├── FontManager.swift │ ├── Fontable.swift │ ├── SplitterRun.swift │ ├── SplitterTimer.swift │ └── ZippyFontDecoding.swift ├── UI │ ├── Controls │ │ ├── EditableSegmentIconView.swift │ │ └── SplitterColorWell.swift │ ├── Import │ │ ├── CategoryViewController.swift │ │ ├── DownloadView.swift │ │ ├── DownloadWindow.storyboard │ │ └── DownloadWindowController.swift │ ├── LoadableNib.swift │ ├── LoadingViewController.swift │ ├── LoadingViewController.xib │ ├── MenuItemConstants.swift │ ├── NSViewExtensions.swift │ ├── NibLoadable.swift │ ├── Splits Editor │ │ ├── CustomComparisonList.swift │ │ ├── CustomComparisonList.xib │ │ ├── SplitsEditorOutlineView.swift │ │ ├── SplitsEditorTextField.swift │ │ ├── SplitsEditorViewController.swift │ │ └── SplitsEditorViewController.xib │ ├── Splits.ioAccount │ │ ├── AccountButton │ │ │ ├── AccountButtonView.swift │ │ │ └── AccountButtonView.xib │ │ ├── AccountProfileViewController │ │ │ ├── AccountProfileView.swift │ │ │ └── AccountProfileView.xib │ │ ├── AccountViewController.swift │ │ └── Login │ │ │ ├── LoginView.swift │ │ │ └── LoginView.xib │ ├── SplitsIOUpload.swift │ ├── TouchBarDelegate.swift │ ├── View Controller │ │ ├── Base.lproj │ │ │ └── Main.storyboard │ │ ├── Colors.swift │ │ ├── Components │ │ │ ├── KeyValueComponent │ │ │ │ ├── KeyValueComponent.swift │ │ │ │ └── KeyValueComponent.xib │ │ │ ├── OptionsRow │ │ │ │ ├── OptionsRow.swift │ │ │ │ └── OptionsRow.xib │ │ │ ├── PrevNextRow │ │ │ │ ├── PrevNextRow.swift │ │ │ │ └── PrevNextRow.xib │ │ │ ├── SegmentComponent │ │ │ │ └── SegmentComponent.swift │ │ │ ├── Shared │ │ │ │ ├── ComponentState.swift │ │ │ │ ├── SplitterComponent.swift │ │ │ │ └── SplitterComponentType.swift │ │ │ ├── SplitsComponent │ │ │ │ ├── SplitTableColumnIdentifiers.swift │ │ │ │ ├── SplitsComponent.swift │ │ │ │ ├── SplitsComponent.xib │ │ │ │ ├── SplitsComponentAdvancedOptions.swift │ │ │ │ ├── SplitsComponentOptions.swift │ │ │ │ ├── SplitterTableDelegate.swift │ │ │ │ ├── SplitterTableView.swift │ │ │ │ └── VerticalCenteredTextFieldCell.swift │ │ │ ├── StartRow │ │ │ │ ├── StartRow.swift │ │ │ │ └── StartRow.xib │ │ │ ├── TimeRow │ │ │ │ ├── TimeRow.swift │ │ │ │ ├── TimeRow.xib │ │ │ │ └── TimeRowOptions.swift │ │ │ └── TitleComponent │ │ │ │ ├── TitleComponent.swift │ │ │ │ └── TitleComponent.xib │ │ ├── Controls │ │ │ ├── Button Actions.swift │ │ │ ├── Icon Actions.swift │ │ │ ├── Menu Items.swift │ │ │ ├── StepperWithNumberField.swift │ │ │ └── Themeing │ │ │ │ ├── MetadataImage.swift │ │ │ │ ├── Themeable.swift │ │ │ │ ├── ThemedButton.swift │ │ │ │ ├── ThemedImage.swift │ │ │ │ ├── ThemedPopUpButton.swift │ │ │ │ └── ThemedTextField.swift │ │ ├── DraggingStackView.swift │ │ ├── Options Windows │ │ │ ├── InfoOptionsViewController.swift │ │ │ └── Layout Editor │ │ │ │ ├── ColorsView │ │ │ │ ├── ColorsView.swift │ │ │ │ └── ColorsView.xib │ │ │ │ ├── ComponentOptions.swift │ │ │ │ ├── ComponentOptionsFontStack.swift │ │ │ │ ├── DragIndicator.swift │ │ │ │ ├── FontPopUpButton.swift │ │ │ │ ├── GeneralLayoutSettingsViewController.swift │ │ │ │ ├── LayoutEditorListCell.swift │ │ │ │ ├── LayoutEditorListCell.xib │ │ │ │ ├── LayoutEditorViewController.swift │ │ │ │ └── WindowSettings │ │ │ │ ├── WindowSettingViewController.swift │ │ │ │ └── WindowSettingViewController.xib │ │ ├── SplitsIOUpload.swift │ │ ├── Timer Functions.swift │ │ ├── UISettings.swift │ │ ├── ViewController.swift │ │ ├── WelcomeWindowController.swift │ │ └── Windows │ │ │ ├── BlankWindowController.swift │ │ │ ├── MainWindow.swift │ │ │ ├── MainWindowController.swift │ │ │ ├── QuickAlert.swift │ │ │ └── blankWindow.xib │ └── WelcomeView.swift └── UpdateController.swift ├── SplitterTests ├── Info.plist ├── Odyssey.json ├── Odyssey.split │ ├── appearance.json │ ├── gameIcon.png │ ├── runInfo.json │ └── segIcons │ │ ├── 0.png │ │ ├── 1.png │ │ ├── 10.png │ │ ├── 11.png │ │ ├── 12.png │ │ ├── 13.png │ │ ├── 14.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ ├── 8.png │ │ └── 9.png ├── Odyssey2 copy.json ├── Odyssey2.json ├── SplitterTests.swift ├── TestAttempts.json ├── TestPlan.xctestplan ├── Untitled2.json └── Untitled3.json ├── SplitterUITests ├── FileTests.swift ├── Info.plist ├── LayoutEditorTests.swift ├── SplitterUITests.swift └── UITest Sample Files │ └── LongList.split │ ├── appearance.json │ ├── layout.lsl │ ├── runInfo.json │ └── splits.lss ├── appcenter-post-clone.sh ├── appdmg.json ├── package-lock.json └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj merge=union -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: Splitter # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: michael_berk 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://mberk.com/donate/'] # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/swift 3 | # Edit at https://www.gitignore.io/?templates=swift 4 | 5 | ### Swift ### 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | ## Build generated 11 | build/ 12 | DerivedData/ 13 | archive/ 14 | 15 | ## Various settings 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | xcuserdata/ 25 | 26 | ## Other 27 | *.moved-aside 28 | *.xccheckout 29 | *.xcscmblueprint 30 | *.sh 31 | 32 | ## Obj-C/Swift specific 33 | *.hmap 34 | *.ipa 35 | *.dSYM.zip 36 | *.dSYM 37 | 38 | ## Playgrounds 39 | timeline.xctimeline 40 | playground.xcworkspace 41 | *.lfs 42 | *.lss 43 | *.split 44 | robobot.json 45 | appdmg.json 46 | *.app 47 | *.dmg 48 | /SplitterTests/*.json 49 | 50 | Pods/CocoaPodsKeys/ 51 | 52 | # Swift Package Manager 53 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 54 | Packages/ 55 | # Package.pins 56 | Package.resolved 57 | .build/ 58 | # Add this line if you want to avoid checking in Xcode SPM integration. 59 | # .swiftpm/xcode 60 | 61 | # CocoaPods 62 | # We recommend against adding the Pods directory to your .gitignore. However 63 | # you should judge for yourself, the pros and cons are mentioned at: 64 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 65 | Pods/* 66 | # Add this line if you want to avoid checking in source code from the Xcode workspace 67 | # *.xcworkspace 68 | 69 | # Carthage 70 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 71 | # Carthage/Checkouts 72 | 73 | Carthage/Build 74 | 75 | # Accio dependency management 76 | Dependencies/ 77 | .accio/ 78 | 79 | rSplitterTest.sh 80 | releaseSplitter.sh 81 | 82 | # fastlane 83 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 84 | # screenshots whenever they are needed. 85 | # For more information about the recommended setup visit: 86 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 87 | 88 | fastlane/report.xml 89 | fastlane/Preview.html 90 | fastlane/screenshots/**/*.png 91 | fastlane/test_output 92 | 93 | # Code Injection 94 | # After new code Injection tools there's a generated folder /iOSInjectionProject 95 | # https://github.com/johnno1962/injectionforxcode 96 | 97 | iOSInjectionProject/ 98 | 99 | # End of https://www.gitignore.io/api/swift 100 | /Pods/CocoaPodsKeys/SplitterKeys.h 101 | /Pods/CocoaPodsKeys/SplitterKeys.m 102 | /ExportOptions.plist 103 | /GithubExportOptions.plist 104 | /NotarizationUUIDs.log 105 | /archive.xcarchive/ 106 | Package.resolved 107 | archiveLog.txt 108 | Config.xcconfig 109 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "Preferences"] 2 | path = Preferences 3 | url = https://github.com/MichaelJBerk/Preferences.git 4 | [submodule "FontPopUp"] 5 | path = FontPopUp 6 | url = https://github.com/MichaelJBerk/FontPopUp 7 | -------------------------------------------------------------------------------- /LiveSplitKit/LiveSplitKit.xcodeproj/xcshareddata/xcschemes/LiveSplitKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /LiveSplitKit/LiveSplitKit.xcodeproj/xcshareddata/xcschemes/LiveSplitKitTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 14 | 15 | 17 | 23 | 24 | 25 | 26 | 27 | 37 | 38 | 44 | 45 | 47 | 48 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /LiveSplitKit/LiveSplitKit/Color.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSColorExtension.swift 3 | // LiveSplitKit 4 | // 5 | // Created by Michael Berk on 1/21/22. 6 | // 7 | 8 | import AppKit 9 | 10 | extension NSColor { 11 | convenience init(_ color: [Double]) { 12 | let floats = color.map({CGFloat($0)}) 13 | self.init(red: floats[0], green: floats[1], blue: floats[2], alpha: floats[3]) 14 | } 15 | func toDouble() -> [Double] { 16 | let converted = self.usingColorSpace(.sRGB) 17 | let floats = [converted!.redComponent, converted!.greenComponent, converted!.blueComponent, converted!.alphaComponent] 18 | return floats.map({Double($0)}) 19 | } 20 | convenience init(_ color: [Float]) { 21 | let floats = color.map({CGFloat($0)}) 22 | self.init(red: floats[0], green: floats[1], blue: floats[2], alpha: floats[3]) 23 | } 24 | func toFloat() -> [Float] { 25 | let converted = self.usingColorSpace(.sRGB) 26 | let floats = [converted!.redComponent, converted!.greenComponent, converted!.blueComponent, converted!.alphaComponent] 27 | return floats.map({Float($0)}) 28 | } 29 | } 30 | 31 | extension NSColor { 32 | static func from(hex: String) -> NSColor { 33 | let hexVal = hex.replacingOccurrences(of: "#", with: "") 34 | let hex = UInt32(hexVal, radix: 16)! 35 | let a = CGFloat((hex >> 24) & 0xFF) / 255.0 36 | let r = CGFloat((hex >> 16) & 0xFF) / 255.0 37 | let g = CGFloat((hex >> 8) & 0xFF) / 255.0 38 | let b = CGFloat((hex) & 0xFF) / 255.0 39 | 40 | let color = NSColor(red: r, green: g, blue: b, alpha: a) 41 | 42 | return color 43 | } 44 | } 45 | 46 | extension SettingValue { 47 | public static func fromNSColor(_ color: NSColor) -> SettingValue { 48 | let floats = color.toFloat() 49 | return .fromColor(floats[0], floats[1], floats[2], floats[3]) 50 | } 51 | public static func fromAlternatingNSColor(_ color1: NSColor, _ color2: NSColor) -> SettingValue { 52 | let floats1 = color1.toFloat() 53 | let floats2 = color2.toFloat() 54 | return .fromAlternatingGradient(floats1[0], floats1[1], floats1[2], floats1[3], floats2[0], floats2[1], floats2[2], floats2[3]) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /LiveSplitKit/LiveSplitKit/Font+NSFont.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Font+NSFont.swift 3 | // LiveSplitKit 4 | // 5 | // Created by Michael Berk on 6/8/23. 6 | // 7 | 8 | import Foundation 9 | #if canImport(Cocoa) 10 | import Cocoa 11 | extension LiveSplitFont.Weight { 12 | /** 13 | Representation of the font's weight that can be used in an NSFontDescriptor 14 | 15 | This value represents the font's weight using units that can be used with NSFontDescriptor. 16 | 17 | ```swift 18 | let fontAttributes: [NSFontDescriptor.AttributeName: Any] = [ 19 | .family: fontName, 20 | .traits: [NSFontDescriptor.TraitKey.weight: LiveSplitFont.Weight.medium] 21 | ] 22 | let fontDescriptor = NSFontDescriptor(fontAttributes: fontAttributes) 23 | let font = NSFont(descriptor: fontDescriptor, size: NSFont.systemFontSize)! 24 | ``` 25 | */ 26 | public var nsFontWeight: CGFloat { 27 | let black = NSFont.Weight.black.rawValue 28 | let light = NSFont.Weight.light.rawValue 29 | let regular = NSFont.Weight.regular.rawValue 30 | 31 | switch cssWeight { 32 | case 100: 33 | return NSFont.Weight.ultraLight.rawValue 34 | case 200: 35 | return NSFont.Weight.thin.rawValue 36 | case 300: 37 | return NSFont.Weight.light.rawValue 38 | case 350: 39 | return light + (regular - light)/2 40 | case 400: 41 | return NSFont.Weight.regular.rawValue 42 | case 500: 43 | return NSFont.Weight.medium.rawValue 44 | case 600: 45 | return NSFont.Weight.semibold.rawValue 46 | case 700: 47 | return NSFont.Weight.bold.rawValue 48 | case 800: 49 | return NSFont.Weight.heavy.rawValue 50 | case 900: 51 | return black 52 | case 950: 53 | return black + (1 - black)/2 54 | default: 55 | return NSFont.Weight.regular.rawValue 56 | } 57 | } 58 | } 59 | 60 | 61 | public extension LiveSplitFont { 62 | ///Create an NSFont based on the LiveSplitFont 63 | func toNSFont(size: CGFloat = NSFont.systemFontSize) -> NSFont? { 64 | var fontSlant = 0.0 65 | if style == .italic { 66 | fontSlant = 1 67 | } 68 | //Cannot add stretch since there's no equivalent in NSFont. 69 | let med = 0.23000000417232513 70 | let num = round(weight.nsFontWeight * 10)/10 71 | let descriptor = NSFontDescriptor(fontAttributes: [ 72 | .family: family, 73 | .traits: [ 74 | .slant: fontSlant, 75 | NSFontDescriptor.TraitKey.weight: num, 76 | ] 77 | ]) 78 | let font = NSFont(descriptor: descriptor, size: size) 79 | return font 80 | } 81 | } 82 | #endif 83 | -------------------------------------------------------------------------------- /LiveSplitKit/LiveSplitKit/LSKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LSKit.swift 3 | // LiveSplitKit 4 | // 5 | // Created by Michael Berk on 1/21/22. 6 | // 7 | 8 | import Foundation 9 | 10 | public func updateLayoutState(_ layout: Layout, timer: LSTimer) -> String { 11 | let state = layout.state(timer) 12 | let refMutState = LayoutStateRefMut(ptr: state.ptr) 13 | let json = layout.updateStateAsJson(refMutState, timer) 14 | return json 15 | } 16 | 17 | class CThing { 18 | func hey() { 19 | print("hello, worldb") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LiveSplitKit/LiveSplitKit/LiveSplitCore/liblivesplit_core.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/LiveSplitKit/LiveSplitKit/LiveSplitCore/liblivesplit_core.a -------------------------------------------------------------------------------- /LiveSplitKit/LiveSplitKit/LiveSplitKit.docc/LiveSplitKit.md: -------------------------------------------------------------------------------- 1 | # ``LiveSplitKit`` 2 | 3 | Summary 4 | 5 | ## Overview 6 | 7 | Text 8 | 9 | ## Topics 10 | 11 | ### Group 12 | 13 | - ``Symbol`` -------------------------------------------------------------------------------- /LiveSplitKit/LiveSplitKit/LiveSplitKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // LiveSplitKit.h 3 | // LiveSplitKit 4 | // 5 | // Created by Michael Berk on 1/21/22. 6 | // 7 | 8 | #import 9 | 10 | //! Project version number for LiveSplitKit. 11 | FOUNDATION_EXPORT double LiveSplitKitVersionNumber; 12 | 13 | //! Project version string for LiveSplitKit. 14 | FOUNDATION_EXPORT const unsigned char LiveSplitKitVersionString[]; 15 | 16 | // In this header, you should import all the public headers of your framework using statements like #import 17 | 18 | 19 | #import "livesplit_core.h" 20 | -------------------------------------------------------------------------------- /LiveSplitKit/LiveSplitKit/module.map: -------------------------------------------------------------------------------- 1 | 2 | 3 | framework module LiveSplitKit { 4 | umbrella header "LiveSplitKit.h" 5 | export * 6 | module * { export * } 7 | 8 | link "CLiveSplitCore" 9 | // use "CLiveSplitCore" 10 | } 11 | 12 | framework module CLiveSplitCore [extern_c] { 13 | header "livesplit_core.h" 14 | link "livesplit_core" 15 | link framework "Carbon" 16 | link framework "CoreFoundation" 17 | link framework "CoreGraphics" 18 | // export * 19 | } 20 | -------------------------------------------------------------------------------- /LiveSplitKit/LiveSplitKitTests/LSCoreTesting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LSCoreTesting.swift 3 | // SplitterTests 4 | // 5 | // Created by Michael Berk on 3/24/22. 6 | // Copyright © 2022 Michael Berk. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import LiveSplitKit 11 | 12 | class LSCoreTesting: XCTestCase { 13 | 14 | var run: Run! 15 | var layout: Layout! 16 | 17 | override func setUpWithError() throws { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | run = Run() 20 | let segment = Segment("") 21 | run.pushSegment(segment) 22 | layout = Layout.defaultLayout() 23 | 24 | } 25 | 26 | override func tearDownWithError() throws { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | } 29 | 30 | ///Test that the methods for getting/setting timing method work 31 | func testTimingMethod() { 32 | guard let le = LayoutEditor(layout) else {return} 33 | le.select(1) 34 | let method1 = le.getTimingMethod(for: 0) 35 | XCTAssert(method1 == nil) 36 | le.setColumn(0, timingMethod: .realTime) 37 | let method2 = le.getTimingMethod(for: 0) 38 | XCTAssert(method2 == .realTime) 39 | le.setColumn(0, timingMethod: .gameTime) 40 | let method3 = le.getTimingMethod(for: 0) 41 | XCTAssert(method3 == .gameTime) 42 | } 43 | 44 | func testGetFont() { 45 | guard let le = LayoutEditor(layout) else {return} 46 | let font = LiveSplitFont(family: "times", style: .normal, weight: .normal, stretch: .normal) 47 | guard let setVal = SettingValue.fromFont(font) else {return} 48 | le.setGeneralSettingsValue(3, setVal) 49 | layout = le.close() 50 | 51 | let timer = LSTimer(run) 52 | let jState = layout.stateAsJson(timer!) 53 | 54 | let pasteboard = NSPasteboard.general 55 | pasteboard.declareTypes([.string], owner: nil) 56 | pasteboard.setString(jState, forType: .string) 57 | 58 | /* 59 | 1: Custom Timer Font 60 | 2: Custom Times Font 61 | 3: Custom Text Font 62 | */ 63 | // SettingValue.fromFont(<#T##String#>, <#T##String#>, <#T##String#>, <#T##String#>) 64 | // let font = le.state().fieldValue(false, 3) 65 | // let json = font.asJson() 66 | // print(json) 67 | // print("hey") 68 | // le.setGeneralSettingsValue(<#T##size_t#>, T##SettingValue) 69 | } 70 | 71 | func testExample() throws { 72 | // This is an example of a functional test case. 73 | // Use XCTAssert and related functions to verify your tests produce the correct results. 74 | // Any test you write for XCTest can be annotated as throws and async. 75 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 76 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 77 | } 78 | 79 | func testPerformanceExample() throws { 80 | // This is an example of a performance test case. 81 | self.measure { 82 | // Put the code you want to measure the time of here. 83 | } 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /LiveSplitKit/LiveSplitKitTests/LiveSplitKitTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LiveSplitKitTests.swift 3 | // LiveSplitKitTests 4 | // 5 | // Created by Michael Berk on 1/21/22. 6 | // 7 | 8 | import XCTest 9 | @testable import LiveSplitKit 10 | 11 | class LiveSplitKitTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // Any test you write for XCTest can be annotated as throws and async. 25 | // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | } 28 | 29 | func testPerformanceExample() throws { 30 | // This is an example of a performance test case. 31 | self.measure { 32 | // Put the code you want to measure the time of here. 33 | } 34 | } 35 | 36 | func testColor() { 37 | // let run = Run() 38 | // run.pushSegment(.init("Hello")) 39 | // let timer = LSTimer(run)! 40 | // let layout = Layout.defaultLayout() 41 | // let state = layout.state(timer) 42 | let color = "#8000CC36" 43 | let c = NSColor.from(hex: color) 44 | 45 | 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /Odyssey.split.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Odyssey.split.zip -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | platform :osx, '10.15' 3 | use_frameworks! 4 | inhibit_all_warnings! 5 | 6 | def splitter_pods 7 | pod 'MASShortcut' 8 | pod 'ZippyJSON' 9 | end 10 | 11 | target 'Splitter' do 12 | splitter_pods 13 | 14 | target 'SplitterTests' do 15 | splitter_pods 16 | end 17 | 18 | target 'SplitterUITests' do 19 | splitter_pods 20 | end 21 | 22 | post_install do |installer| 23 | installer.generated_projects.each do |project| 24 | project.targets.each do |target| 25 | target.build_configurations.each do |config| 26 | config.build_settings['MACOSX_DEPLOYMENT_TARGET'] = '10.15' 27 | end 28 | end 29 | puts "Fix MASShortcut" 30 | end 31 | end 32 | 33 | plugin 'cocoapods-keys', { 34 | :project => "splitter_pods", 35 | :keys => [ 36 | "SPLIT_SIO_SECRET", 37 | "SPLIT_SIO_CLIENT" 38 | ] 39 | } 40 | 41 | end -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - JJLISO8601DateFormatter (0.1.5) 3 | - Keys (1.0.1) 4 | - MASShortcut (2.4.0) 5 | - ZippyJSON (1.2.10): 6 | - JJLISO8601DateFormatter (= 0.1.5) 7 | - ZippyJSONCFamily (= 1.2.9) 8 | - ZippyJSONCFamily (1.2.9) 9 | 10 | DEPENDENCIES: 11 | - Keys (from `Pods/CocoaPodsKeys`) 12 | - MASShortcut 13 | - ZippyJSON 14 | 15 | SPEC REPOS: 16 | trunk: 17 | - JJLISO8601DateFormatter 18 | - MASShortcut 19 | - ZippyJSON 20 | - ZippyJSONCFamily 21 | 22 | EXTERNAL SOURCES: 23 | Keys: 24 | :path: Pods/CocoaPodsKeys 25 | 26 | SPEC CHECKSUMS: 27 | JJLISO8601DateFormatter: 36bc0fc8fbd77440fe8b33e0281a4c57170781e4 28 | Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 29 | MASShortcut: d9e4909e878661cc42877cc9d6efbe638273ab57 30 | ZippyJSON: 565d1cbf09c1fac147ae6323356b04caaf9e1397 31 | ZippyJSONCFamily: a6fdeeb750d4891d68b8324dd90d87111f314388 32 | 33 | PODFILE CHECKSUM: 3c11cb38b1a54edac2985aaaa16d5a4793454bf8 34 | 35 | COCOAPODS: 1.15.2 36 | -------------------------------------------------------------------------------- /Programming-Notes.md: -------------------------------------------------------------------------------- 1 | # Programming Notes 2 | 3 | This document is a list of general guidelines for working on Splitter. 4 | 5 | 6 | ## SF Symbols 7 | - Use of SF Symbols is encouraged, but remember that macOS ≤ 10.15 doesn't support it, so there needs to be a fallback for that operating system 8 | - When using a new SF Symbol, always check if the symbol name changed in a previous version, and fall back to it if necessary. 9 | - DON'T force-unwrap an SF Symbol unless you're sure that the symbol name is the same on all supported versions of macOS 10 | - For example, `line.3.horizontal` was `line.horizontal.3` in macOS Big Sur. Since I had used the name `line.3.horizontal` force-unwrapped, the app would crash on Big Sur. (commit 69abc858dc6393a01e61b6732837258e17aec43c) 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Splitter 2 | 3 | ![Splitter](https://splitter.mberk.com/splitter-smaller.png) 4 | 5 | A native Speedrunning timer for macOS 6 | 7 | **[FAQ](https://github.com/MichaelJBerk/Splitter/wiki/FAQ)** 8 | 9 | **[Tips](https://github.com/MichaelJBerk/Splitter/wiki/Tips)** 10 | 11 | **[Tip Jar](https://mberk.com/donate/)** 12 | 13 | ## Download 14 | 15 | ### Stable 16 | 17 | The latest stable release of Splitter is available on the Mac App Store: 18 | 19 | [Download Latest Stable Release](https://apps.apple.com/us/app/splitter-speedrun-timer/id1502505482?ls=1) 20 | 21 | ## Beta Testing 22 | 23 | If you're a _"thrill seeker"_, want to help test the next version of Splitter, or just like living on the edge, you can try out the latest beta release, which is based on the `dev` branch 24 | 25 | (https://github.com/MichaelJBerk/Splitter/releases) 26 | 27 | ## Discussion 28 | 29 | You can suggest features, report issues, and contribute to the overall discussion about Splitter on the official [Discord Server](https://discord.gg/S6zCHYq). 30 | 31 | ## Supported Formats 32 | 33 | [Info about Splitter's native file format](https://github.com/MichaelJBerk/Splitter/wiki/.Split-Format) 34 | 35 | In addition to the Splitter-native `.split` format, the app can edit and save the following: 36 | - LiveSplit (`.lss`) 37 | - Splits.io (`.json`) 38 | 39 | Note that some features (such as appearance and color settings) are only saved to a `.split` file. As such, if you don't want to have to set them every time you open the file, it's recommended to save the file as `.split` (by clicking "File" -> "Save as..." in the menubar). If you need to need to share it with someone who isn't using Splitter, just open the `.split` file, and save it in whatever other format works best. 40 | 41 | ## Building Splitter 42 | 43 | If you want to build Splitter yourself: 44 | - Install CocoaPods if you don't already have it installed 45 | - Clone this repo 46 | - run `pod install` in the project's root directory 47 | - You'll be prompted to set the secret and client keys for Splits.io, but you can press the Return key to leave them blank. See [cocoapods-keys](https://github.com/orta/cocoapods-keys?tab=readme-ov-file) for more info. 48 | - Open Splitter.xcworkspace in Xcode 49 | 50 | 51 | ### Acknowledgements 52 | [LiveSplit Core](https://github.com/LiveSplit/livesplit-core) 53 | 54 | [Splits.io](https://splits.io/) 55 | 56 | [Files by John Sundell](https://github.com/JohnSundell/Files) 57 | 58 | [MASShortcut by Vadim Shpakovski](https://github.com/shpakovski/MASShortcut) 59 | 60 | [Preferences by Sindre Sorhus](https://github.com/sindresorhus/Preferences) 61 | 62 | [Sparkle by Sparkle Project](https://github.com/sparkle-project/Sparkle) 63 | 64 | [Cocoapods](https://cocoapods.org) 65 | -------------------------------------------------------------------------------- /SplitsIOKit/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | -------------------------------------------------------------------------------- /SplitsIOKit/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /SplitsIOKit/.swiftpm/xcode/xcshareddata/xcschemes/SplitsIOKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 33 | 39 | 40 | 41 | 42 | 43 | 53 | 54 | 60 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /SplitsIOKit/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "SplitsIOKit", 8 | platforms: [.macOS(SupportedPlatform.MacOSVersion.v10_14)], 9 | products: [ 10 | // Products define the executables and libraries a package produces, and make them visible to other packages. 11 | .library( 12 | name: "SplitsIOKit", 13 | targets: ["SplitsIOKit"]), 14 | ], 15 | dependencies: [ 16 | .package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", .upToNextMajor(from: "5.0.0")), 17 | .package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.2.0")), 18 | .package(url: "https://github.com/pitiphong-p/URLQueryItemEncoder.git", .upToNextMajor(from: "0.2.0")), 19 | .package(name: "Files", url: "https://github.com/JohnSundell/Files", from: "4.0.0"), 20 | .package(name: "OAuth2", url: "https://github.com/p2/OAuth2.git", .upToNextMajor(from: "5.2.0")) 21 | 22 | // Dependencies declare other packages that this package depends on. 23 | // .package(url: /* package url */, from: "1.0.0"), 24 | ], 25 | targets: [ 26 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 27 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 28 | .target( 29 | name: "SplitsIOKit", 30 | dependencies: ["SwiftyJSON", "Alamofire", "Files", "OAuth2", "URLQueryItemEncoder"]), 31 | .testTarget( 32 | name: "SplitsIOKitTests", 33 | dependencies: ["SplitsIOKit", "Files"]), 34 | ] 35 | ) 36 | -------------------------------------------------------------------------------- /SplitsIOKit/README.md: -------------------------------------------------------------------------------- 1 | # SplitsIOKit 2 | 3 | A Swift library for interacting with Splits.io 4 | -------------------------------------------------------------------------------- /SplitsIOKit/Sources/SplitsIOKit/SRL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Michael Berk on 8/11/20. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct SRLRun: Decodable { 11 | let count: String 12 | let races: [SRLRace] 13 | } 14 | 15 | // MARK: - SRLRace 16 | public struct SRLRace: Decodable { 17 | let id: String 18 | let game: SRLGame 19 | let goal: String 20 | let time, state: Int 21 | let statetext, filename: String 22 | let numentrants: Int 23 | var entrants: [String: SRLEntrant] 24 | 25 | private enum CodingKeys: String, CodingKey { 26 | case id 27 | case game 28 | case goal 29 | case time 30 | case state 31 | case statetext 32 | case filename 33 | case numentrants 34 | case entrants 35 | } 36 | public init(from decoder: Decoder) throws { 37 | let container = try decoder.container(keyedBy: CodingKeys.self) 38 | id = try container.decode(String.self, forKey: .id) 39 | game = try container.decode(SRLGame.self, forKey: .game) 40 | goal = try container.decode(String.self, forKey: .goal) 41 | time = try container.decode(Int.self, forKey: .time) 42 | state = try container.decode(Int.self, forKey: .state) 43 | statetext = try container.decode(String.self, forKey: .statetext) 44 | filename = try container.decode(String.self, forKey: .filename) 45 | numentrants = try container.decode(Int.self, forKey: .numentrants) 46 | entrants = [String:SRLEntrant]() 47 | 48 | let subContainer = try container.nestedContainer(keyedBy: GenericCodingKeys.self, forKey: .entrants) 49 | for key in subContainer.allKeys { 50 | entrants[key.stringValue] = try subContainer.decode(SRLEntrant.self, forKey: key) 51 | } 52 | 53 | 54 | } 55 | } 56 | 57 | public struct SRLEntrant: Codable { 58 | let displayname: String 59 | let place, time: Int 60 | let message: String? 61 | let statetext, twitch, trueskill: String 62 | } 63 | 64 | // MARK: - SRLGame 65 | public struct SRLGame: Codable { 66 | let id: Int 67 | let name, abbrev: String 68 | let popularity, popularityrank: Double 69 | } 70 | 71 | public struct GenericCodingKeys: CodingKey { 72 | public var intValue: Int? 73 | public var stringValue: String 74 | 75 | public init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" } 76 | public init?(stringValue: String) { self.stringValue = stringValue } 77 | 78 | public static func makeKey(name: String) -> GenericCodingKeys { 79 | return GenericCodingKeys(stringValue: name)! 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /SplitsIOKit/Sources/SplitsIOKit/SplitsIOAuth.swift: -------------------------------------------------------------------------------- 1 | // 2 | // File.swift 3 | // 4 | // 5 | // Created by Michael Berk on 8/11/20. 6 | // 7 | 8 | import Foundation 9 | import Cocoa 10 | import OAuth2 11 | /// Class for handling authentication with Splits.io 12 | /// 13 | /// To use this, you'll need to set up a client and secret on Splits.io. 14 | public class SplitsIOAuth { 15 | let oAuth2: OAuth2CodeGrant 16 | 17 | /// Initalize SplitsIOAuth. Requires developer setup from Splits.io 18 | /// - Parameters: 19 | /// - client: Client ID 20 | /// - secret: Secret ID 21 | /// - redirects: Array of URI redirects for the application 22 | /// - url: Requires https 23 | public init(client: String, secret: String, redirects: String, url: String) { 24 | oAuth2 = OAuth2CodeGrant(settings: [ 25 | "client_id": "\(client)", 26 | "client_secret": "\(secret)", 27 | "authorize_uri": "\(url)/oauth/authorize", 28 | "token_uri": "\(url)/oauth/token", 29 | "redirect_uris": [redirects], 30 | "scope": "upload_run delete_run manage_race", 31 | "keychain": "true", 32 | ] as OAuth2JSON) 33 | } 34 | 35 | public func authenticate(completion: @escaping() -> ()) throws { 36 | oAuth2.logger = OAuth2DebugLogger(.trace) 37 | do { 38 | let url = try oAuth2.authorizeURL() 39 | try oAuth2.authorizer.openAuthorizeURLInBrowser(url) 40 | oAuth2.didAuthorizeOrFail = { authParams, error in 41 | NotificationCenter.default.post(name: .splitsIOLogin, object: nil) 42 | completion() 43 | 44 | } 45 | } catch { 46 | print("authURL error:", error) 47 | } 48 | } 49 | public func logout(completion: @escaping() -> ()) { 50 | oAuth2.forgetTokens() 51 | NotificationCenter.default.post(name: .splitsIOLogout, object: nil) 52 | completion() 53 | 54 | } 55 | 56 | 57 | } 58 | 59 | extension Notification.Name { 60 | public static let splitsIOLogin = Notification.Name(rawValue: "splitsIOLogin") 61 | public static let splitsIOLogout = Notification.Name(rawValue: "splitsIOLogout") 62 | } 63 | -------------------------------------------------------------------------------- /SplitsIOKit/Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | import SplitsIOKitTests 4 | 5 | var tests = [XCTestCaseEntry]() 6 | tests += SplitsIOKitTests.allTests() 7 | XCTMain(tests) 8 | -------------------------------------------------------------------------------- /SplitsIOKit/Tests/SplitsIOKitTests/SpeedrunComTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SpeedrunComTests.swift 3 | // 4 | // 5 | // Created by Michael Berk on 11/16/21. 6 | // 7 | 8 | import XCTest 9 | import Files 10 | 11 | @testable import SplitsIOKit 12 | 13 | final class SpeedrunComTests: XCTestCase { 14 | 15 | func testSearch() { 16 | let expectation = XCTestExpectation(description: "Complete Search") 17 | Speedruncom().searchSpeedruncom(for: "Super Mario Odyssey", completion: { games, error in 18 | XCTAssertFalse(games == nil) 19 | print(games?.compactMap({$0.names.international})) 20 | expectation.fulfill() 21 | }) 22 | wait(for: [expectation], timeout: 10.0) 23 | } 24 | 25 | func testGetRuns() { 26 | let smsGameID = "v1pxjz68" 27 | let expectation = XCTestExpectation(description: "Get Game") 28 | Speedruncom().getRuns(query: .init(game: smsGameID, max: 1, offset: 0), completion: {runs, error in 29 | if let error = error { 30 | print("Error\n\(error)") 31 | XCTFail("GetRuns Failed") 32 | return 33 | } 34 | XCTAssertFalse(runs == nil) 35 | print(runs!.compactMap {$0.category}) 36 | expectation.fulfill() 37 | }) 38 | wait(for: [expectation], timeout: 10.0) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SplitsIOKit/Tests/SplitsIOKitTests/SplitsIOKitTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Files 3 | 4 | @testable import SplitsIOKit 5 | 6 | final class SplitsIOKitTests: XCTestCase { 7 | 8 | func testSearch() { 9 | let expectation = XCTestExpectation(description: "Complete Search") 10 | SplitsIOKit().searchSplitsIO(for: "Psychonauts", completion: { games in 11 | XCTAssertFalse(games == nil) 12 | expectation.fulfill() 13 | }) 14 | wait(for: [expectation], timeout: 10.0) 15 | } 16 | 17 | @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) 18 | func testAsyncSearch() async { 19 | let games = await SplitsIOKit().searchSplitsIO(for: "Psychonauts") 20 | XCTAssertFalse(games == nil) 21 | } 22 | 23 | func testGetRun() { 24 | let expectation = XCTestExpectation(description: "Find run") 25 | SplitsIOKit().getRun(runID: "5yj6", completion: { run in 26 | expectation.fulfill() 27 | }) 28 | wait(for: [expectation], timeout: 10.0) 29 | } 30 | func testGetLivesplitRun() { 31 | let expectation = XCTestExpectation(description: "Find run") 32 | SplitsIOKit().getRunAsLivesplit(runID: "5yj6", completion: { run in 33 | let rFile = try? File(path: run!) 34 | try? rFile?.delete() 35 | expectation.fulfill() 36 | }) 37 | wait(for: [expectation], timeout: 10.0) 38 | } 39 | func testGetRunsFromCat() { 40 | let expectation = XCTestExpectation(description: "get runs from cat") 41 | SplitsIOKit().getRunFromCat(categoryID: "4075", completion: { runs in 42 | expectation.fulfill() 43 | 44 | }) 45 | wait(for: [expectation], timeout: 10.0) 46 | } 47 | func testGetCategories() { 48 | let expectation = XCTestExpectation(description: "Get categories") 49 | SplitsIOKit().getCategories(for: "yi", completion: { cats in 50 | expectation.fulfill() 51 | 52 | }) 53 | wait(for: [expectation], timeout: 10.0) 54 | } 55 | func testSRL() { 56 | let expectation = XCTestExpectation(description: "get runs from SRL") 57 | SplitsIOKit().getLatestSRLRaces(completion: { SRL in 58 | expectation.fulfill() 59 | print(SRL.count) 60 | 61 | }) 62 | wait(for: [expectation], timeout: 10.0) 63 | } 64 | func testGetRunner() { 65 | let expectation = XCTestExpectation(description: "get lphantom") 66 | SplitsIOKit().getRunner(name: "lphantom") {runner in 67 | if let runner = runner { 68 | expectation.fulfill() 69 | print(runner.displayName) 70 | } 71 | } 72 | wait(for: [expectation], timeout: 10.0) 73 | } 74 | func testGetGamesFromRunner() { 75 | let expectation = XCTestExpectation(description: "get lphantom games") 76 | SplitsIOKit().getGamesFromRunner(runnerName: "lphantom", completion: { games in 77 | XCTAssertFalse(games == nil) 78 | expectation.fulfill() 79 | 80 | }) 81 | wait(for: [expectation], timeout: 10.0) 82 | 83 | } 84 | // func testGetGamesFromSpedruncom() { 85 | // let expectation = XCTestExpectation(description: "get lphantom games") 86 | // Speedruncom().searchSpeedruncom(for: "Super Mario Odyssey", completion: { games, _ in in 87 | // XCTAssertFalse(games == nil) 88 | // expectation.fulfill() 89 | // 90 | // }) 91 | // wait(for: [expectation], timeout: 10.0) 92 | // } 93 | 94 | 95 | 96 | } 97 | -------------------------------------------------------------------------------- /SplitsIOKit/Tests/SplitsIOKitTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(SplitsIOKitTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Splitter.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Splitter.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Splitter.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Files", 6 | "repositoryURL": "https://github.com/JohnSundell/Files", 7 | "state": { 8 | "branch": null, 9 | "revision": "22fe84797d499ffca911ccd896b34efaf06a50b9", 10 | "version": "4.1.1" 11 | } 12 | }, 13 | { 14 | "package": "Preferences", 15 | "repositoryURL": "https://github.com/sindresorhus/Preferences", 16 | "state": { 17 | "branch": null, 18 | "revision": "73664ff9f9faeb40f4b8f2c02d21b4486c82cce9", 19 | "version": "2.0.0" 20 | } 21 | }, 22 | { 23 | "package": "SwiftyJSON", 24 | "repositoryURL": "https://github.com/SwiftyJSON/SwiftyJSON.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "2b6054efa051565954e1d2b9da831680026cd768", 28 | "version": "5.0.0" 29 | } 30 | } 31 | ] 32 | }, 33 | "version": 1 34 | } 35 | -------------------------------------------------------------------------------- /Splitter.xcodeproj/xcshareddata/xcschemes/Splitter - Release.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /Splitter.xcodeproj/xcshareddata/xcschemes/SplitterTests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 10 | 11 | 16 | 17 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 43 | 44 | 45 | 46 | 52 | 53 | 55 | 56 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Splitter.xcodeproj/xcshareddata/xcschemes/SplitterUITests.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 10 | 11 | 16 | 17 | 19 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 43 | 44 | 45 | 46 | 52 | 53 | 55 | 56 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /Splitter.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Splitter.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Splitter.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon_16x16.png", 5 | "scale" : "1x", 6 | "idiom" : "mac", 7 | "size" : "16x16" 8 | }, 9 | { 10 | "idiom" : "mac", 11 | "filename" : "icon_16x16@2x.png", 12 | "size" : "16x16", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "idiom" : "mac", 17 | "filename" : "icon_32x32.png", 18 | "size" : "32x32", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "idiom" : "mac", 23 | "size" : "32x32", 24 | "filename" : "icon_32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "filename" : "icon_128x128.png", 29 | "idiom" : "mac", 30 | "size" : "128x128", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "scale" : "2x", 35 | "size" : "128x128", 36 | "idiom" : "mac", 37 | "filename" : "icon_128x128@2x.png" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon_256x256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "filename" : "icon_256x256@2x.png", 47 | "idiom" : "mac", 48 | "scale" : "2x", 49 | "size" : "256x256" 50 | }, 51 | { 52 | "filename" : "icon_512x512.png", 53 | "idiom" : "mac", 54 | "size" : "512x512", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "filename" : "icon_512x512@2x.png", 60 | "scale" : "2x", 61 | "idiom" : "mac" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/AppIcon.appiconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/AppIcon.appiconset/icon_128x128.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/AppIcon.appiconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/AppIcon.appiconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/AppIcon.appiconset/icon_16x16.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/AppIcon.appiconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/AppIcon.appiconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/AppIcon.appiconset/icon_256x256.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/AppIcon.appiconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/AppIcon.appiconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/AppIcon.appiconset/icon_32x32.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/AppIcon.appiconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/AppIcon.appiconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/AppIcon.appiconset/icon_512x512.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/AppIcon.appiconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/CurrentSplitColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | }, 6 | "colors" : [ 7 | { 8 | "idiom" : "universal", 9 | "color" : { 10 | "color-space" : "srgb", 11 | "components" : { 12 | "red" : "91", 13 | "alpha" : "1.000", 14 | "blue" : "181", 15 | "green" : "74" 16 | } 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_128x128.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_16x16.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_256x256.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_32x32.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_512x512.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/docBG.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_128x128.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_16x16.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_256x256.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_32x32.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_512x512.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/livesplit.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_128x128.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_16x16.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_256x256.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_32x32.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_512x512.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitDocIcon.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_128x128.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_128x128@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_16x16.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_16x16@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_256x256.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_256x256@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_32x32.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_32x32@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_512x512.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Document Icon/splitsioDoc.iconset/icon_512x512@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Game Controller.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Gamepad 2.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Game Controller.imageset/Gamepad 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Game Controller.imageset/Gamepad 2.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Hotkeys.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "Icon.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/Hotkeys.imageset/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/Hotkeys.imageset/Icon.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/accentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "color-space" : "srgb", 6 | "components" : { 7 | "alpha" : "1.000", 8 | "blue" : "0.710", 9 | "green" : "0.290", 10 | "red" : "0.357" 11 | } 12 | }, 13 | "idiom" : "universal" 14 | } 15 | ], 16 | "info" : { 17 | "author" : "xcode", 18 | "version" : 1 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/chevron.down.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/chevron.down.1.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/chevron.down.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/chevron.down.2.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/chevron.down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/chevron.down.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/chevron.up.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/chevron.up.1.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/chevron.up.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/chevron.up.2.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/chevron.up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/chevron.up.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/downarrow.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "downarrow.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/downarrow.imageset/downarrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/downarrow.imageset/downarrow.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/flag.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "flagsmaller.png", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "preserves-vector-representation" : true, 14 | "template-rendering-intent" : "template" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/flag.imageset/flagsmaller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/flag.imageset/flagsmaller.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/folder.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "folder.pdf", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/folder.imageset/folder.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/folder.imageset/folder.pdf -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/gearshape.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "gearshape.pdf", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/gearshape.imageset/gearshape.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/gearshape.imageset/gearshape.pdf -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/info.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "info.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "template" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/info.imageset/info.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/info.imageset/info.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/lines.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "List@1x.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "List@2x.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "author" : "xcode", 20 | "version" : 1 21 | }, 22 | "properties" : { 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/lines.imageset/List@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/lines.imageset/List@1x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/lines.imageset/List@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/lines.imageset/List@2x.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/menuBarIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "Splitter Glyph 2.svg", 5 | "idiom" : "universal" 6 | } 7 | ], 8 | "info" : { 9 | "author" : "xcode", 10 | "version" : 1 11 | }, 12 | "properties" : { 13 | "auto-scaling" : "auto", 14 | "preserves-vector-representation" : true, 15 | "template-rendering-intent" : "template" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/pencil.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "filename" : "PencilIcon.png", 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/pencil.imageset/PencilIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/pencil.imageset/PencilIcon.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/plus.app.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "plus.app.pdf", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/plus.app.imageset/plus.app.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/plus.app.imageset/plus.app.pdf -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/splitsio.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "logo-578e4861acb4fde546fe2a2f1cc2d750da727b0eefef55e876aa40c6cbc4ebd8.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "template-rendering-intent" : "original" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/splitsio.imageset/logo-578e4861acb4fde546fe2a2f1cc2d750da727b0eefef55e876aa40c6cbc4ebd8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/splitsio.imageset/logo-578e4861acb4fde546fe2a2f1cc2d750da727b0eefef55e876aa40c6cbc4ebd8.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/square.and.arrow.down.fill.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "square.and.arrow.down.fill.pdf", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/square.and.arrow.down.fill.imageset/square.and.arrow.down.fill.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/square.and.arrow.down.fill.imageset/square.and.arrow.down.fill.pdf -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/stop.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "stop.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "stop.1.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "stop.2.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | }, 23 | "properties" : { 24 | "preserves-vector-representation" : true, 25 | "template-rendering-intent" : "template" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/stop.imageset/stop.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/stop.imageset/stop.1.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/stop.imageset/stop.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/stop.imageset/stop.2.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/stop.imageset/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/stop.imageset/stop.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/stopSquare.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "stop.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "stop.1.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "stop.2.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/stopSquare.imageset/stop.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/stopSquare.imageset/stop.1.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/stopSquare.imageset/stop.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/stopSquare.imageset/stop.2.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/stopSquare.imageset/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/stopSquare.imageset/stop.png -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/trash.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "trash.4.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | }, 21 | "properties" : { 22 | "preserves-vector-representation" : true, 23 | "template-rendering-intent" : "template" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Splitter/Assets.xcassets/trash.imageset/trash.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/Assets.xcassets/trash.imageset/trash.4.png -------------------------------------------------------------------------------- /Splitter/Credits.html: -------------------------------------------------------------------------------- 1 | 6 | 7 |
Icons made by Freepik from www.flaticon.com 8 |
9 |
https://www.mberk.com/splitter
10 | 11 | 12 | -------------------------------------------------------------------------------- /Splitter/Data/CommonPaths.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommonPaths.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 2/12/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | enum CommonPaths { 13 | static let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("/Splitter") 14 | static let keybinds = appSupport.appendingPathComponent("/splitter.splitkeys") 15 | } 16 | 17 | -------------------------------------------------------------------------------- /Splitter/Data/Documents/Common/SplitterDocBundle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitterDocBundle.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 2/12/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Files 11 | 12 | 13 | ///Class for Splitter's Document Bundles 14 | class SplitterDocBundle: SplitterDoc { 15 | 16 | override internal var file: File? { 17 | return nil 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /Splitter/Data/Documents/Other Formats/LiveSplit/LiveSplit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LiveSplit.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 2/5/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | import Files 12 | import LiveSplitKit 13 | 14 | extension ViewController { 15 | func loadLS(url: URL, asTemplate: Bool = false) { 16 | if let run = Run.parseFile(path: url.path, loadFiles: true) { 17 | self.run = SplitterRun (run: run, isNewRun: false) 18 | if asTemplate { 19 | self.run.timer.resetHistories() 20 | } 21 | self.run.updateLayoutState() 22 | } 23 | } 24 | } 25 | 26 | 27 | extension NSImage { 28 | //TODO: Use this when saving to LiveSplit 29 | /** Turns an NSImage into the format necessary to be used with LiveSplitCore 30 | - Parameters: 31 | - withImage: Function that takes the data and passes it to LiveSplitCore 32 | 33 | In LiveSplitCore, methods dealing with images take two parameters: a pointer (`UnsafeMutableRawPointer?`) and the image's length (`Int`). 34 | 35 | `toLSImage` provides an easy way to deal with this in Swift, using an existing `NSImage`. 36 | 37 | Let's say you have `editor` - a `RunEditor`, and want to set the game's icon - an `NSImage` called `gameIcon`. You would use this method like so: 38 | ```swift 39 | gameIcon.toLSImage { pointer, len in 40 | editor.setGameIcon(pointer, len) 41 | } 42 | ``` 43 | */ 44 | func toLSImage(_ withImage: (UnsafeMutableRawPointer?, Int) -> ()) { 45 | if var giData = tiffRepresentation { 46 | let giLen = giData.count 47 | giData.withUnsafeMutableBytes( { bytes in 48 | let umbp = bytes.baseAddress 49 | withImage(umbp, giLen) 50 | }) 51 | } 52 | } 53 | 54 | static func parseImageFromLiveSplit(ptr: UnsafeMutableRawPointer?, len: size_t) -> NSImage? { 55 | if let p = ptr { 56 | let data = Data(bytes: p, count: Int(len)) 57 | if let img = NSImage(data: data) { 58 | return img 59 | } 60 | } 61 | return nil 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Splitter/Data/Documents/Other Formats/LiveSplit/lss.swift: -------------------------------------------------------------------------------- 1 | // 2 | // lss.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 2/10/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Files 11 | 12 | class lss: SplitterDoc { 13 | 14 | /* 15 | override var windowNibName: String? { 16 | // Override returning the nib file name of the document 17 | // If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this method and override -makeWindowControllers instead. 18 | return "lss" 19 | } 20 | */ 21 | 22 | override func windowControllerDidLoadNib(_ aController: NSWindowController) { 23 | super.windowControllerDidLoadNib(aController) 24 | 25 | // Add any code here that needs to be executed once the windowController has loaded the document's window. 26 | } 27 | 28 | var tempURL: URL? 29 | private var urlToLoad: URL? { 30 | if let url = tempURL { 31 | return url 32 | } 33 | else if let url = fileURL { 34 | return url 35 | } 36 | return nil 37 | } 38 | var template: Bool = false 39 | 40 | override func makeWindowControllers() { 41 | let load = loadViewController() 42 | let vc = load.vc 43 | if let url = urlToLoad { 44 | vc.loadLS(url: url, asTemplate: template) 45 | } 46 | vc.setupVC() 47 | } 48 | 49 | override func save(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, delegate: Any?, didSave didSaveSelector: Selector?, contextInfo: UnsafeMutableRawPointer?) { 50 | determineSave(to: url, ofType: typeName, for: saveOperation, delegate: delegate, didSave: didSaveSelector, contextInfo: contextInfo) 51 | } 52 | 53 | override func close() { 54 | super.close() 55 | if let tempURL = tempURL { 56 | let tempFile = try? File(path: tempURL.path) 57 | try? tempFile?.delete() 58 | } 59 | 60 | } 61 | 62 | override func data(ofType typeName: String) throws -> Data { 63 | 64 | throw NSError(domain: NSOSStatusErrorDomain, code: unimpErr, userInfo: nil) 65 | } 66 | 67 | override func read(from data: Data, ofType typeName: String) throws { 68 | // Insert code here to read your document from the given data of the specified type, throwing an error in case of failure. 69 | // Alternatively, you could remove this method and override read(from:ofType:) instead. If you do, you should also override isEntireFileLoaded to return false if the contents are lazily loaded. 70 | 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Splitter/Data/Time Conversion.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Time Conversion.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 3/3/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Parse RFC 3339 date string to NSDate 12 | /// 13 | /// :param: rfc3339DateTimeString string with format "yyyy-MM-ddTHH:mm:ssZ" 14 | /// :returns: NSDate, or nil if string cannot be parsed 15 | public func dateForRFC3339DateTimeString(rfc3339DateTimeString: String) -> Date? { 16 | let formatter = getThreadLocalRFC3339DateFormatter() 17 | return formatter.date(from: rfc3339DateTimeString) 18 | } 19 | 20 | /// Generate RFC 3339 date string for an NSDate 21 | /// 22 | /// :param: date NSDate 23 | /// :returns: String 24 | public func rfc3339DateTimeStringForDate(date: Date) -> String { 25 | let formatter = getThreadLocalRFC3339DateFormatter() 26 | return formatter.string(from: date) 27 | } 28 | 29 | // Date formatters are not thread-safe, so use a thread-local instance 30 | private func getThreadLocalRFC3339DateFormatter() -> DateFormatter { 31 | return cachedThreadLocalObjectWithKey(key: "net.kristopherjohnson.getThreadLocalRFC3339DateFormatter") { 32 | let en_US_POSIX = Locale(identifier: "en_US_POSIX") 33 | let rfc3339DateFormatter = DateFormatter() 34 | rfc3339DateFormatter.locale = en_US_POSIX 35 | rfc3339DateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssXXX" 36 | rfc3339DateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 37 | return rfc3339DateFormatter 38 | } 39 | } 40 | 41 | /// Return a thread-local object, creating it if it has not already been created 42 | /// 43 | /// :param: create closure that will be invoked to create the object 44 | /// :returns: object of type T 45 | private func cachedThreadLocalObjectWithKey(key: String, create: () -> T) -> T { 46 | let threadDictionary = Thread.current.threadDictionary 47 | if let cachedObject = threadDictionary[key] as! T? { 48 | return cachedObject 49 | } 50 | else { 51 | let newObject = create() 52 | threadDictionary[key] = newObject 53 | return newObject 54 | } 55 | // If we don't have a thread-local dictionary for some reason, 56 | // then just call create() on every call 57 | 58 | } 59 | -------------------------------------------------------------------------------- /Splitter/Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Extensions.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 4/28/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension NSImage { 12 | 13 | static func isDiff(_ lhs: NSImage?, _ rhs: NSImage?) -> Bool { 14 | if lhs?.tiffRepresentation != rhs?.tiffRepresentation || (lhs != nil && rhs == nil) || (lhs == nil && rhs != nil) { 15 | return true 16 | } 17 | return false 18 | } 19 | } 20 | 21 | extension NSGridView { 22 | // open override var isFlipped: Bool { 23 | // return true 24 | // } 25 | } 26 | extension NSVisualEffectView { 27 | open override var isFlipped: Bool { 28 | return true 29 | } 30 | } 31 | extension NSMenuItem { 32 | convenience init(title: String, action: Selector, keyEquivalent: String, representedObject: Any?) { 33 | self.init(title: title, action: action, keyEquivalent: keyEquivalent) 34 | self.representedObject = representedObject 35 | } 36 | } 37 | extension NSView { 38 | ///Toggles whether the view is hidden` 39 | func hide() { 40 | self.isHidden.toggle() 41 | } 42 | } 43 | extension NSAppearance { 44 | var isDark: Bool { 45 | if name.rawValue.contains("Dark") { 46 | return true 47 | } 48 | return false 49 | } 50 | } 51 | extension NSImage { 52 | static var gameControllerIcon: NSImage { 53 | let icon = #imageLiteral(resourceName: "Game Controller") 54 | icon.isTemplate = true 55 | return icon 56 | } 57 | } 58 | 59 | extension NSControl.StateValue { 60 | 61 | func toBool() -> Bool { 62 | if self == .on { 63 | return true 64 | } else if self == .off { 65 | return false 66 | } 67 | return false 68 | } 69 | 70 | init (bool: Bool) { 71 | if bool { 72 | self = .on 73 | } else { 74 | self = .off 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /Splitter/LiveSplit Core/liblivesplit_core.a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/LiveSplit Core/liblivesplit_core.a -------------------------------------------------------------------------------- /Splitter/Prefs/DebugPrefsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DebugPrefsViewController.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 7/26/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | #if DEBUG 9 | import Cocoa 10 | import Preferences 11 | 12 | class DebugPrefsViewController: NSViewController, PreferencePane { 13 | var preferencePaneIdentifier: Preferences.PaneIdentifier = .debug 14 | 15 | var preferencePaneTitle: String = "Debug" 16 | var toolbarItemIcon: NSImage { 17 | if #available(macOS 11.0, *), let img = NSImage(systemSymbolName: "hammer", accessibilityDescription: nil) { 18 | return img 19 | } else { 20 | return NSImage(named: NSImage.mobileMeName)! 21 | } 22 | } 23 | 24 | override var nibName: NSNib.Name? { "DebugPrefsViewController" } 25 | @IBOutlet var splitsIOURLTextField: NSTextField! 26 | @IBOutlet var splitsIOClientIDField: NSTextField! 27 | @IBOutlet var splitsIOSecretIDField: NSTextField! 28 | @IBOutlet var placeholderSIOCheck: NSButton! 29 | 30 | 31 | override func viewDidLoad() { 32 | super.viewDidLoad() 33 | // Do view setup here. 34 | preferredContentSize = NSSize(width: self.view.frame.size.width, height: self.view.frame.size.height) 35 | placeholderSIOCheck.state = .init(bool: Settings.placeholderSIO) 36 | 37 | } 38 | override func viewDidAppear() { 39 | super.viewDidAppear() 40 | splitsIOURLTextField.stringValue = Settings.splitsIOURL.absoluteString 41 | 42 | } 43 | 44 | @IBAction func editURLTextField(_ sender: NSTextField) { 45 | if let url = URL(string: sender.stringValue) { 46 | Settings.splitsIOURL = url 47 | } 48 | } 49 | 50 | @IBAction func editClientTextField(_ sender: NSTextField) { 51 | if sender.stringValue == "" { 52 | Settings.splitsIOClientOverride = nil 53 | } else { 54 | Settings.splitsIOClientOverride = sender.stringValue 55 | } 56 | } 57 | 58 | @IBAction func editSecretTextField(_ sender: NSTextField) { 59 | if sender.stringValue == "" { 60 | Settings.splitsIOSecretOverride = nil 61 | } else { 62 | Settings.splitsIOSecretOverride = sender.stringValue 63 | } 64 | } 65 | 66 | @IBAction func sioInfoButtonClick(_ sender: NSButton) { 67 | let alert = NSAlert() 68 | alert.informativeText = "The following is persisted in the DB, and is what will be used for auth on next startup:\n\n\n\n\n\n\n\n\n\n\n\nURL: \(Settings.splitsIOURL) \nClientID: \(AppDelegate.splitsioclient)\nSecret: \(AppDelegate.splitsiosecret)" 69 | alert.beginSheetModal(for: self.view.window!) 70 | } 71 | 72 | @IBAction func togglePlaceholderSIO(_ sender: NSButton) { 73 | Settings.placeholderSIO = sender.state.toBool() 74 | } 75 | 76 | } 77 | #endif 78 | -------------------------------------------------------------------------------- /Splitter/Prefs/GeneralPrefsViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GeneralPrefsViewController.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 12/13/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Preferences 11 | 12 | class GeneralPrefsViewController: NSViewController, PreferencePane { 13 | 14 | @IBOutlet var menuBarSwitch: NSSwitch! 15 | @IBOutlet var resetWarningsButton: NSButton! 16 | 17 | @IBAction func resetWarningsClicked(_ sender: Any?) { 18 | for warning in Warning.allCases { 19 | Settings.setWarning(warning, suppresed: false) 20 | } 21 | } 22 | 23 | @IBAction func setAppMode(_ sender: NSSwitch) { 24 | if sender.state == .on { 25 | StatusBarController.setMenuBarMode(true) 26 | NSApp.activate(ignoringOtherApps: true) 27 | } else { 28 | StatusBarController.setMenuBarMode(false) 29 | } 30 | 31 | } 32 | @objc func popover(_ sender: Any) { 33 | NSApp.activate(ignoringOtherApps: true) 34 | } 35 | var preferencePaneTitle: String = "General" 36 | 37 | var preferencePaneIdentifier: Preferences.PaneIdentifier = .general 38 | 39 | override var nibName: NSNib.Name? { "GeneralPrefsViewController" } 40 | 41 | var toolbarItemIcon: NSImage { 42 | if #available(macOS 11.0, *), let img = NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil) { 43 | return img 44 | } else { 45 | return NSImage(named: NSImage.preferencesGeneralName)! 46 | } 47 | } 48 | 49 | override func viewDidLoad() { 50 | super.viewDidLoad() 51 | // Do view setup here. 52 | preferredContentSize = NSSize(width: self.view.frame.size.width, height: self.view.frame.size.height) 53 | NotificationCenter.default.addObserver(forName: .menuBarModeChanged, object: nil, queue: nil, using: { _ in 54 | self.setMenuBarToggle() 55 | }) 56 | setMenuBarToggle() 57 | } 58 | 59 | func setMenuBarToggle() { 60 | if Settings.menuBarMode { 61 | menuBarSwitch.state = .on 62 | } else { 63 | menuBarSwitch.state = .off 64 | } 65 | } 66 | 67 | } 68 | 69 | extension AppDelegate { 70 | @objc func statusAction(_ sender: Any?) { 71 | NSApp.activate(ignoringOtherApps: true) 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Splitter/Prefs/Hotkeys/GlobalKeybindPrefs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // GlobalKeybindPrefs.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 1/13/20. 6 | // Uses code from https://dev.to/mitchartemis/creating-a-global-configurable-shortcut-for-macos-apps-in-swift-25e9 7 | // Copyright © 2020 Michael Berk. All rights reserved. 8 | // 9 | 10 | import Foundation 11 | 12 | class GlobalKeybindPreferences: Codable, CustomStringConvertible { 13 | // let function : Bool 14 | let control : Bool 15 | let command : Bool 16 | let shift : Bool 17 | let option : Bool 18 | let capsLock : Bool 19 | let carbonFlags : UInt32 20 | let characters : String? 21 | let keyCode : UInt32 22 | 23 | init (control: Bool, command: Bool, shift: Bool, option: Bool, capsLock: Bool, carbonFlags: UInt32, characters: String?, keyCode: UInt32) { 24 | self.control = control 25 | self.command = command 26 | self.shift = shift 27 | self.option = option 28 | self.capsLock = capsLock 29 | self.carbonFlags = carbonFlags 30 | self.characters = characters 31 | self.keyCode = keyCode 32 | } 33 | 34 | var description: String { 35 | 36 | //the reason why I have to make a copy of self.function is explained later on 37 | // var function = self.function 38 | 39 | 40 | 41 | var stringBuilder = "" 42 | 43 | if self.control { 44 | stringBuilder += modifierChars.control.rawValue 45 | } 46 | if self.option { 47 | stringBuilder += modifierChars.option.rawValue 48 | } 49 | if self.shift { 50 | stringBuilder += modifierChars.shift.rawValue 51 | } 52 | if self.command { 53 | stringBuilder += modifierChars.command.rawValue 54 | } 55 | 56 | if self.capsLock { 57 | stringBuilder += "⇪" 58 | } 59 | 60 | if var characters = self.characters { 61 | let intKC = Int(keyCode) 62 | switch intKC { 63 | case 36: 64 | characters = "⏎" 65 | case 48: 66 | characters = "⇥" 67 | case 49: 68 | characters = "␣" 69 | // function = false 70 | case 51: 71 | characters = "⌫" 72 | case 115: 73 | characters = "↖" 74 | case 116: 75 | characters = "⇞" 76 | case 117: 77 | characters = "⌦" 78 | // function = false 79 | case 119: 80 | characters = "↘" 81 | case 121: 82 | characters = "⇟" 83 | 84 | //For some reason, when pressing an arrow key, it thinks that fn is being pressed, so we correct it. It's actually not possible to do fn+[arrow key] because it turns into one of the pgUp/pgDown/home/end keys listed above, so this shouldn't cause false positives. 85 | case 123: 86 | characters = "←" 87 | // function = false 88 | case 124: 89 | characters = "→" 90 | // function = false 91 | case 125: 92 | characters = "↓" 93 | // function = false 94 | case 126: 95 | characters = "↑" 96 | // function = false 97 | default: break 98 | } 99 | 100 | stringBuilder += characters.uppercased() 101 | 102 | } 103 | 104 | // if function { 105 | // stringBuilder = "Fn" + stringBuilder 106 | // } 107 | return "\(stringBuilder)" 108 | } 109 | } 110 | 111 | enum modifierChars: String { 112 | case command = "⌘" 113 | case option = "⌥" 114 | case control = "⌃" 115 | case shift = "⇧" 116 | } 117 | -------------------------------------------------------------------------------- /Splitter/Prefs/Hotkeys/KeybindHandlers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeybindHandlers.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 2/16/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | ///Stuff for handling keybind actions 12 | extension AppDelegate { 13 | ///Brings the current window to the front. Intended for use with keybinds. 14 | func frontHandler() { 15 | NSApplication.shared.orderedWindows.forEach({ (window) in 16 | if let mainWindow = window as? MainWindow { 17 | mainWindow.hidesOnDeactivate = false 18 | NSApplication.shared.activate(ignoringOtherApps: true) 19 | mainWindow.makeKeyAndOrderFront(nil) 20 | mainWindow.makeKey() 21 | 22 | } 23 | }) 24 | } 25 | 26 | //TODO: Document why this exists 27 | func startSplitHandler() { 28 | if let vc = viewController { 29 | vc.startSplitTimer() 30 | } 31 | } 32 | 33 | func pauseHandler() { 34 | if let vc = viewController { 35 | vc.pauseResumeTimer() 36 | } 37 | } 38 | 39 | func prevHandler() { 40 | if let vc = viewController { 41 | vc.run.timer.previousSplit() 42 | } 43 | } 44 | func skipHandler() { 45 | if let vc = viewController { 46 | vc.run.timer.skipSplit() 47 | } 48 | } 49 | func stopHandler() { 50 | if let vc = viewController { 51 | vc.cancelRun() 52 | } 53 | } 54 | func cancelRunHandler() { 55 | if let vc = viewController { 56 | vc.cancelRun() 57 | } 58 | } 59 | 60 | func showInfoHandler() { 61 | if let vc = viewController { 62 | vc.displayInfoPopover(self) 63 | } 64 | } 65 | func showLayoutEditorHandler() { 66 | if let vc = viewController { 67 | vc.showLayoutEditor() 68 | } 69 | } 70 | func showSplitsEditorHandler() { 71 | if let vc = viewController { 72 | vc.displaySplitsEditor() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Splitter/Prefs/PrefPanes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrefPanes.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 1/1/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import Preferences 11 | 12 | extension Preferences.PaneIdentifier { 13 | static let general = Self("general") 14 | static let advanced = Self("advanced") 15 | static let hotkeys = Self("hotkeys") 16 | static let debug = Self("debug") 17 | static let splitsIO = Self("splitsIO") 18 | } 19 | -------------------------------------------------------------------------------- /Splitter/SetupForUITesting.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SetupForUITesting.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 10/25/22. 6 | // Copyright © 2022 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | extension AppDelegate { 11 | 12 | var isUITesting: Bool { 13 | return CommandLine.arguments.contains("-uiTesting") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Splitter/Splitter-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | // 2 | // Splitter-Bridging-Header.h 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 2/6/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | #ifndef Splitter_Bridging_Header_h 10 | #define Splitter_Bridging_Header_h 11 | //#import "LiveSplit Core/livesplit_core.h" 12 | #import 13 | #import 14 | 15 | #endif /* Splitter_Bridging_Header_h */ 16 | -------------------------------------------------------------------------------- /Splitter/Splitter.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-unsigned-executable-memory 8 | 9 | com.apple.security.cs.disable-library-validation 10 | 11 | com.apple.security.files.user-selected.read-write 12 | 13 | com.apple.security.network.client 14 | 15 | keychain-access-groups 16 | 17 | $(AppIdentifierPrefix)com.mibe.Splitter 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Splitter/SplitterDocLSS.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/SplitterDocLSS.icns -------------------------------------------------------------------------------- /Splitter/SplitterDocSplitsio.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/Splitter/SplitterDocSplitsio.icns -------------------------------------------------------------------------------- /Splitter/StatusBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StatusBarController.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 12/13/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class StatusBarController: NSObject { 12 | 13 | var statusItem: NSStatusItem! 14 | var statusMenu: NSMenu! 15 | 16 | var quitItem: NSMenuItem { 17 | let quit = NSMenuItem(title: "Quit", action: #selector(quitMenuItem(_:)), keyEquivalent: "q") 18 | quit.keyEquivalentModifierMask = [.command] 19 | return quit 20 | } 21 | 22 | var turnOffOverlayItem = NSMenuItem(title: "Turn off Overlay Mode", action: #selector(turnOffOverlayAction(_:)), keyEquivalent: "") 23 | 24 | func setupItem() { 25 | let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength) 26 | statusMenu = NSMenu(title: "PopMenu") 27 | self.statusItem = item 28 | if let button = item.button { 29 | let img = NSImage(named: "menuBarIcon") 30 | button.image = img! 31 | button.imageScaling = .scaleProportionallyDown 32 | button.target = self 33 | button.action = #selector(clickItem(_:)) 34 | item.button?.sendAction(on: [.leftMouseUp, .rightMouseUp]) 35 | } 36 | 37 | 38 | statusMenu.delegate = self 39 | let menuItems = [quitItem, .separator(), turnOffOverlayItem, .separator()] 40 | for item in menuItems { 41 | item.target = self 42 | statusMenu.addItem(item) 43 | } 44 | let mainMenu = NSApp.mainMenu! 45 | for item in mainMenu.items { 46 | let copy = item.copy() as! NSMenuItem 47 | statusMenu.addItem(copy) 48 | } 49 | 50 | 51 | 52 | NotificationCenter.default.addObserver(forName: .menuBarModeChanged, object: nil, queue: nil, using: { _ in 53 | self.toggleVisible() 54 | }) 55 | Self.setMenuBarMode(Settings.menuBarMode) 56 | } 57 | 58 | func toggleVisible() { 59 | if Settings.menuBarMode { 60 | statusItem.isVisible = true 61 | } else { 62 | statusItem.isVisible = false 63 | } 64 | } 65 | 66 | @objc func quitMenuItem(_ sender: Any?) { 67 | NSApp.terminate(nil) 68 | } 69 | @objc func turnOffOverlayAction(_ sender: Any?) { 70 | Self.setMenuBarMode(false) 71 | } 72 | 73 | @objc func clickItem(_ sender: Any?) { 74 | let event = NSApp.currentEvent! 75 | if event.type == .rightMouseUp { 76 | statusItem.menu = statusMenu 77 | statusItem.button?.performClick(nil) 78 | } else { 79 | NSApp.activate(ignoringOtherApps: true) 80 | //Always 1 window - status item 81 | AppDelegate.shared?.newWindowIfNone() 82 | } 83 | } 84 | 85 | static func setMenuBarMode(_ bool: Bool) { 86 | Settings.menuBarMode = bool 87 | let pol: NSApplication.ActivationPolicy = bool ? .accessory : .regular 88 | NSApp.setActivationPolicy(pol) 89 | } 90 | } 91 | 92 | extension StatusBarController: NSMenuDelegate { 93 | func menuDidClose(_ menu: NSMenu) { 94 | statusItem.menu = nil 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Splitter/TimerCore/Fontable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Fontable.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 12/22/22. 6 | // Copyright © 2022 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | protocol Fontable { 12 | func setFont() 13 | func setFontObserver() 14 | var run: SplitterRun! {get} 15 | ///Initial font size for the control 16 | /// 17 | ///- NOTE: The default implementation does not set this. The `NSControl` implemention _does_ set this when ``setFont()-75x2j`` is run. 18 | var defaultFontSize: CGFloat? {get set} 19 | ///Option to opt in/out of Fontable behaviors 20 | var fontable: Bool {get set} 21 | ///Determines if the control responds to Splitter's font size adjustment 22 | var fixedFontSize: Bool {get} 23 | } 24 | 25 | extension Fontable { 26 | func setFontObserver() { 27 | NotificationCenter.default.addObserver(forName: .fontChanged, object: run, queue: nil, using: { notification in 28 | if fontable { 29 | setFont() 30 | } 31 | }) 32 | } 33 | var fixedFontSize: Bool { 34 | return false 35 | } 36 | } 37 | 38 | //Need separate extension for each; I tried making it into a single protocol, but it wouldn't work. 39 | 40 | extension NSCell { 41 | func setFont(run: SplitterRun) { 42 | if let csize = self.font?.pointSize { 43 | let font = run.fontManager.getTextFont(fixedFontSize: false, defaultSize: csize) 44 | self.font = font 45 | } 46 | } 47 | } 48 | 49 | extension Fontable where Self: NSCell { 50 | func setFont() { 51 | self.setFont(run: run) 52 | } 53 | } 54 | 55 | extension Fontable where Self: NSControl { 56 | func setFont() { 57 | self.setFont(run: run) 58 | } 59 | } 60 | 61 | extension NSControl { 62 | @objc func setFont(run: SplitterRun) { 63 | if var control = self as? Fontable { 64 | if control.defaultFontSize == nil { 65 | if let size = self.font?.pointSize { 66 | control.defaultFontSize = size 67 | } else { 68 | control.defaultFontSize = 12 69 | } 70 | } 71 | let defaultSize = control.defaultFontSize! 72 | self.font = run.fontManager.getTextFont(fixedFontSize: control.fixedFontSize, defaultSize: defaultSize) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Splitter/TimerCore/ZippyFontDecoding.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZippyFontDecoding.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 4/11/22. 6 | // Copyright © 2022 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import LiveSplitKit 11 | import ZippyJSON 12 | 13 | fileprivate struct FontWrapper: Codable { 14 | var Font: LiveSplitFont 15 | } 16 | 17 | public extension SettingValueRef { 18 | func toFont() -> LiveSplitFont { 19 | let value = self.asJson() 20 | let data = value.data(using: .utf8)! 21 | let decoded = try! ZippyJSONDecoder().decode(FontWrapper.self, from: data) 22 | return decoded.Font 23 | 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /Splitter/UI/Controls/EditableSegmentIconView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditableSegmentIconView.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 1/26/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class EditableSegmentIconView: ThemedImage { 12 | var delegate: EditableSegmentIconViewDelegate! 13 | var row: Int! 14 | 15 | required init?(coder: NSCoder) { 16 | super.init(coder: coder) 17 | setup() 18 | } 19 | override init(frame frameRect: NSRect) { 20 | super.init(frame: frameRect) 21 | setup() 22 | } 23 | 24 | func setup() { 25 | self.target = self 26 | self.action = #selector(imageChanged(_:)) 27 | } 28 | 29 | func setPlaceholderImage() { 30 | if self.image == nil { 31 | self.image = .gameControllerIcon 32 | } 33 | } 34 | 35 | @objc func imageChanged(_ sender: EditableSegmentIconView) { 36 | let image = sender.image 37 | delegate.iconPicked(image, for: row) 38 | setPlaceholderImage() 39 | } 40 | 41 | ///Used in right-click menu 42 | @IBAction func removeImage(_ sender: Any?) { 43 | image = nil 44 | } 45 | ///Used in right-click menu 46 | @IBAction func chooseImageMenuItem(_ sender: Any?) { 47 | setImage() 48 | } 49 | } 50 | extension EditableSegmentIconView { 51 | 52 | override func mouseDown(with event: NSEvent) { 53 | if event.type == .leftMouseDown, event.clickCount > 1 { 54 | setImage() 55 | } 56 | } 57 | func pictureFileDialog() -> NSOpenPanel{ 58 | let dialog = NSOpenPanel(); 59 | dialog.title = "Choose an image file" 60 | dialog.showsResizeIndicator = true 61 | dialog.showsHiddenFiles = false 62 | dialog.canChooseDirectories = false 63 | dialog.canCreateDirectories = false 64 | dialog.allowsMultipleSelection = false 65 | dialog.allowedFileTypes = ["png"] 66 | return dialog 67 | } 68 | 69 | /// Prompts the user to select an image for the split icon 70 | func setImage() { 71 | let dialog = pictureFileDialog() 72 | 73 | let response = dialog.runModal() 74 | if response == .OK { 75 | let result = dialog.url 76 | 77 | if (result != nil) { 78 | let imageFile = try? Data(contentsOf: result!) 79 | let myImage = NSImage(data: imageFile!) 80 | delegate.iconPicked(myImage, for: row) 81 | } 82 | } 83 | } 84 | } 85 | protocol EditableSegmentIconViewDelegate { 86 | func iconPicked(_ icon: NSImage?, for row: Int) 87 | } 88 | -------------------------------------------------------------------------------- /Splitter/UI/Controls/SplitterColorWell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitterColorWell.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 6/30/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | @IBDesignable 11 | ///A control that displays a color value and lets the user change that color value. 12 | class SplitterColorWell: NSColorWell { 13 | var customAction: (SplitterColorWell) -> () = {_ in} 14 | 15 | @objc func colorWellAction(_ sender: SplitterColorWell) { 16 | customAction(sender) 17 | } 18 | 19 | convenience init(action: @escaping (SplitterColorWell) -> ()) { 20 | self.init() 21 | self.target = self 22 | self.action = #selector(colorWellAction(_:)) 23 | self.customAction = action 24 | } 25 | 26 | @IBInspectable var allowsOpacity: Bool = true 27 | 28 | ///Place to save the existing shared `showsAlpha` setting 29 | private var sharedOpacity: Bool? 30 | 31 | override func activate(_ exclusive: Bool) { 32 | if allowsOpacity { 33 | NSColorPanel.shared.showsAlpha = true 34 | } else { 35 | NSColorPanel.shared.showsAlpha = false 36 | } 37 | super.activate(exclusive) 38 | } 39 | override func deactivate() { 40 | if let shared = sharedOpacity { 41 | NSColorPanel.shared.showsAlpha = shared 42 | } 43 | super.deactivate() 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /Splitter/UI/Import/DownloadWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DownloadWindowController.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 8/2/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SplitsIOKit 11 | class DownloadWindowController: NSWindowController, NSWindowDelegate { 12 | static var windowOpened: Bool = false 13 | @IBOutlet weak var searchField: NSSearchField! 14 | @IBOutlet weak var accountButton: AccountButtonView! 15 | var account: SplitsIORunner? = Settings.splitsIOUser { 16 | didSet { 17 | accountButton.account = self.account 18 | accountButton.setAccountLabel() 19 | accountButton.setAccountImage() 20 | } 21 | } 22 | 23 | 24 | override func windowDidLoad() { 25 | super.windowDidLoad() 26 | window?.delegate = self 27 | if let vc = window?.contentViewController as? DownloadViewController { 28 | vc.sField = searchField 29 | } 30 | if let _ = account { accountButton.reloadData() } 31 | NotificationCenter.default.addObserver(forName: .splitsIOLogin, object: nil, queue: nil, using: {_ in 32 | self.getUser() 33 | }) 34 | NotificationCenter.default.addObserver(forName: .splitsIOLogout, object: nil, queue: nil, using: { notification in 35 | 36 | Settings.splitsIOUser = nil 37 | self.account = nil 38 | }) 39 | 40 | } 41 | func windowWillClose(_ notification: Notification) { 42 | Self.windowOpened = false 43 | } 44 | override func windowWillLoad() { 45 | super.windowWillLoad() 46 | Self.windowOpened = true 47 | } 48 | 49 | 50 | func getUser() { 51 | try? SplitsIOKit.shared.getCurrentUser(completion: { runner in 52 | self.account = runner 53 | 54 | }) 55 | } 56 | 57 | 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Splitter/UI/LoadingViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingViewController.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 8/18/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class LoadingViewController: NSViewController, LoadableNib { 12 | @IBOutlet weak var contentView: NSView! 13 | 14 | @IBOutlet weak var imageView: NSImageView! 15 | @IBOutlet weak var labelView: NSTextField! 16 | @IBOutlet weak var loadingBar: NSProgressIndicator! 17 | 18 | 19 | override func viewWillAppear() { 20 | super.viewWillAppear() 21 | self.imageView.image = NSImage(named: NSImage.applicationIconName) 22 | loadingBar.startAnimation(nil) 23 | 24 | } 25 | override func viewDidLoad() { 26 | super.viewDidLoad() 27 | 28 | // Do view setup here. 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /Splitter/UI/NSViewExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NSViewExtensions.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 7/28/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | extension NSView { 12 | /// Returns the first constraint with the given identifier, if available. 13 | /// 14 | /// - Parameter identifier: The constraint identifier. 15 | func constraintWithIdentifier(_ identifier: String) -> NSLayoutConstraint? { 16 | 17 | return self.constraints.first { $0.identifier == identifier } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Splitter/UI/NibLoadable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NibLoadable.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 6/25/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | ///The newer, better Nib loading protocol 12 | protocol NibLoadable { 13 | static var nibName: String? { get } 14 | static func createFromNib(in bundle: Bundle) -> Self? 15 | } 16 | 17 | extension NibLoadable where Self: NSView { 18 | 19 | static var nibName: String? { 20 | return String(describing: self) 21 | } 22 | 23 | static func createFromNib(in bundle: Bundle = Bundle.main) -> Self? { 24 | guard let nibName = nibName else { return nil } 25 | var topLevelArray: NSArray? = nil 26 | bundle.loadNibNamed(NSNib.Name(nibName), owner: self, topLevelObjects: &topLevelArray) 27 | guard let results = topLevelArray else { return nil } 28 | let views = Array(results).filter { $0 is Self } 29 | return views.last as? Self 30 | } 31 | } 32 | 33 | extension NibLoadable where Self: NSViewController { 34 | 35 | static var nibName: String? { 36 | return String(describing: self) 37 | } 38 | 39 | static func createFromNib(in bundle: Bundle = Bundle.main) -> Self? { 40 | guard let nibName = nibName else { return nil } 41 | var topLevelArray: NSArray? = nil 42 | bundle.loadNibNamed(NSNib.Name(nibName), owner: self, topLevelObjects: &topLevelArray) 43 | guard let results = topLevelArray else { return nil } 44 | let views = Array(results).filter { $0 is Self } 45 | return views.last as? Self 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Splitter/UI/Splits Editor/SplitsEditorOutlineView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitsEditorOutlineView.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 6/29/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import LiveSplitKit 11 | 12 | class SplitsEditorOutlineView: NSOutlineView { 13 | var editor: RunEditor! 14 | var editorState: RunEditorState { 15 | editor.getState() 16 | } 17 | 18 | override func selectRowIndexes(_ indexes: IndexSet, byExtendingSelection extend: Bool) { 19 | for row in 0.. Bool { 69 | editorState.segments?[row].selected.bool() ?? false 70 | } 71 | 72 | var splitsDelegate: SplitsEditorOutlineViewDelegate { 73 | delegate as! SplitsEditorOutlineViewDelegate 74 | } 75 | 76 | ///Used to make the segment icons selectable, and so that clicking a text field both selects the row and begins editing 77 | override func validateProposedFirstResponder(_ responder: NSResponder, for event: NSEvent?) -> Bool { 78 | if let view = responder as? NSView, 79 | let sup = view.superview { 80 | let row = self.row(for: sup) 81 | 82 | //Make sure that the text field handles selecting the row and then becomes first responder 83 | if view is SplitsEditorTextField { 84 | return true 85 | } 86 | if row > -1, selectedRowIndexes.contains(row) { 87 | return true 88 | } 89 | } 90 | return super.validateProposedFirstResponder(responder, for: event) 91 | } 92 | } 93 | 94 | ///This protocol doesn't do anything special right now, but I made it at one point, and it could save time in the future, so it's here. 95 | @objc 96 | protocol SplitsEditorOutlineViewDelegate: NSOutlineViewDelegate {} 97 | -------------------------------------------------------------------------------- /Splitter/UI/Splits Editor/SplitsEditorTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitsEditorTextField.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 6/30/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class TableViewTextField: NSTextField { 12 | var column: NSUserInterfaceItemIdentifier! 13 | 14 | var row: Int! 15 | 16 | override func validateProposedFirstResponder(_ responder: NSResponder, for event: NSEvent?) -> Bool { 17 | return true 18 | } 19 | } 20 | 21 | class SplitsEditorTextField: TableViewTextField { 22 | 23 | var outlineView: SplitsEditorOutlineView! 24 | ///Previous of the text field before the user edited it 25 | /// 26 | ///This is used to prevent changing text in LiveSplit if text didn't change, in ``SplitsEditorViewController/controlTextDidEndEditing(_:)`` 27 | var previousValue: String! 28 | 29 | ///Make sure that clicking text field selects the segment in the segment editor 30 | override func becomeFirstResponder() -> Bool { 31 | let sup = super.becomeFirstResponder() 32 | if !outlineView.selectedRowIndexes.contains(row) { 33 | outlineView.selectRowIndexes(.init(integer: row), byExtendingSelection: false) 34 | } 35 | return sup 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Splitter/UI/Splits.ioAccount/AccountButton/AccountButtonView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountButtonViewController.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 8/11/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SplitsIOKit 11 | 12 | @IBDesignable 13 | class AccountButtonView: NSView, LoadableNib { 14 | @IBOutlet var contentView: NSView! 15 | 16 | @IBOutlet var accountLabel: NSTextField! 17 | @IBOutlet var accountIcon: NSImageView! 18 | var account: SplitsIORunner? = Settings.splitsIOUser { 19 | didSet { 20 | Settings.splitsIOUser = account 21 | setAccountLabel() 22 | setAccountImage() 23 | } 24 | } 25 | func reloadData() { 26 | setAccountLabel() 27 | setAccountImage() 28 | } 29 | func setAccountLabel() { 30 | #if DEBUG 31 | if Settings.placeholderSIO { 32 | accountLabel.stringValue = "Splitter" 33 | return 34 | } 35 | #endif 36 | if let account = account { 37 | accountLabel.stringValue = account.displayName 38 | } else { 39 | accountLabel.stringValue = "Sign In" 40 | } 41 | } 42 | 43 | 44 | func setAccountImage() { 45 | if let account = account, let avatarURL = URL(string: account.avatar) { 46 | let avatar = NSImage(byReferencing: avatarURL) 47 | accountIcon.image = avatar 48 | } else { 49 | accountIcon.image = NSImage(named: NSImage.userName) 50 | } 51 | } 52 | 53 | override func draw(_ dirtyRect: NSRect) { 54 | super.draw(dirtyRect) 55 | 56 | } 57 | required init?(coder: NSCoder) { 58 | super.init(coder: coder) 59 | loadViewFromNib() 60 | let click = NSClickGestureRecognizer(target: self, action: #selector(clicked(_:))) 61 | self.addGestureRecognizer(click) 62 | //For some reason, I need to manually do this so it doesn't show the placeholder when the view first appears if you're already logged in 63 | 64 | account = Settings.splitsIOUser 65 | } 66 | 67 | 68 | 69 | @objc func clicked(_ sender: Any) { 70 | let aView = AccountViewController() 71 | let pop = NSPopover() 72 | pop.contentViewController = aView 73 | pop.behavior = .semitransient 74 | // pop.contentSize = aView.currentView?.view.frame.size 75 | pop.show(relativeTo: accountLabel.frame, of: self, preferredEdge: .minY) 76 | 77 | NotificationCenter.default.addObserver(forName: .splitsIOLogout, object: nil, queue: nil, using: { _ in 78 | pop.close() 79 | }) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /Splitter/UI/Splits.ioAccount/AccountProfileViewController/AccountProfileView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AccountViewController.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 8/12/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SplitsIOKit 11 | 12 | 13 | class AccountProfileView: NSViewController, AccountSubview { 14 | var accountController: AccountViewController! 15 | 16 | @IBOutlet var contentView: NSView! 17 | 18 | @IBOutlet weak var avatarView: NSImageView! 19 | @IBOutlet weak var usernameField: NSTextField! 20 | @IBOutlet weak var twitchUsernameLabel: NSTextField! 21 | @IBOutlet weak var twitchUsernameField: NSTextField! 22 | 23 | @IBOutlet weak var logoutButton: NSButton! 24 | 25 | @IBAction func logoutButtonClick(_ sender: NSButton) { 26 | logout() 27 | } 28 | override var view: NSView { 29 | get { 30 | return contentView 31 | } 32 | set { 33 | contentView = newValue 34 | } 35 | } 36 | 37 | 38 | func setAccount(account: SplitsIORunner) { 39 | usernameField.stringValue = account.displayName 40 | let hasTwitch = account.twitchName != nil 41 | twitchUsernameField.isHidden = !hasTwitch 42 | twitchUsernameLabel.isHidden = !hasTwitch 43 | twitchUsernameField.stringValue = account.twitchName ?? "" 44 | if let avatarURL = URL(string: account.avatar) { 45 | let avatar = NSImage(byReferencing: avatarURL) 46 | avatarView.image = avatar 47 | } else { 48 | avatarView.image = #imageLiteral(resourceName: "splitsio") 49 | } 50 | } 51 | func logout() { 52 | accountController.logout() 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Splitter/UI/Splits.ioAccount/Login/LoginView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 8/11/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import SplitsIOKit 11 | 12 | class LoginView: NSViewController, AccountSubview { 13 | var accountController: AccountViewController! 14 | 15 | override var view: NSView { 16 | get { 17 | return contentView 18 | } 19 | set { 20 | contentView = newValue 21 | } 22 | } 23 | 24 | @IBOutlet weak var loginButton: NSButton! 25 | 26 | @IBOutlet var contentView: NSView! 27 | 28 | @IBAction func loginButtonClick(_ sender: NSButton) { 29 | login() 30 | } 31 | 32 | func login() { 33 | accountController.login() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Components/KeyValueComponent/KeyValueComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SumOfBestComponent.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 4/20/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class KeyValueComponent: NSStackView, SplitterComponent, NibLoadable { 12 | var run: SplitterRun! 13 | var customSpacing: CGFloat? = nil 14 | var key: KeyValueComponentType! 15 | private var componentIndex: Int! 16 | private var refreshUITimer = Timer() 17 | internal override func awakeAfter(using coder: NSCoder) -> Any? { 18 | return instantiateView() // You need to add this line to load view 19 | } 20 | 21 | static func instantiateView(with run: SplitterRun, _ viewController: ViewController, type: KeyValueComponentType, layoutIndex: Int) -> KeyValueComponent { 22 | let row: KeyValueComponent = KeyValueComponent.instantiateView() 23 | row.viewController = viewController 24 | row.run = run 25 | row.key = type 26 | row.componentIndex = layoutIndex 27 | row.initialization() 28 | return row 29 | } 30 | 31 | private func initialization() { 32 | textField.run = run 33 | keyField.run = run 34 | run.updateFunctions.append(updateUI) 35 | self.updateFields() 36 | } 37 | 38 | func updateUI() { 39 | if !self.isHidden { 40 | self.updateFields() 41 | } 42 | } 43 | 44 | func updateFields() { 45 | let state = run.getLayoutState() 46 | let kvs = state.componentAsKeyValue(componentIndex) 47 | keyField.stringValue = kvs.key() 48 | textField.stringValue = kvs.value() 49 | } 50 | 51 | var viewController: ViewController! 52 | 53 | var isSelected: Bool = false { 54 | didSet { 55 | didSetSelected() 56 | } 57 | } 58 | 59 | @IBOutlet var textField: ThemedTextField! 60 | @IBOutlet var keyField: ThemedTextField! 61 | 62 | 63 | } 64 | 65 | enum KeyValueComponentType: String, Codable, CaseIterable { 66 | case sumOfBest = "Sum of Best Segments" 67 | case previousSegment = "Previous Segment" 68 | case totalPlaytime = "Total Playtime" 69 | case currentSegment = "Segment Time" 70 | case possibleTimeSave = "Possible Time Save" 71 | 72 | var componentType: SplitterComponentType{ 73 | switch self { 74 | case .sumOfBest: 75 | return .sumOfBest 76 | case .previousSegment: 77 | return .previousSegment 78 | case .totalPlaytime: 79 | return .totalPlaytime 80 | case .currentSegment: 81 | return .segment 82 | case .possibleTimeSave: 83 | return .possibleTimeSave 84 | } 85 | } 86 | 87 | init(from decoder: Decoder) throws { 88 | let str: String = try decoder.decodeSingleValue() 89 | if str == "Sum of Best Segments" { 90 | self = .sumOfBest 91 | return 92 | } 93 | if str == "Previous Segment" || str == "Live Segment" { 94 | self = .previousSegment 95 | return 96 | } 97 | if str == "Current Segment" { 98 | self = .currentSegment 99 | return 100 | } 101 | if str == "Possible Time Save" { 102 | self = .possibleTimeSave 103 | return 104 | } 105 | throw DecodingError.typeMismatch(Self.Type.self, .init(codingPath: decoder.codingPath, debugDescription: "Can't decode")) 106 | 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Components/PrevNextRow/PrevNextRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrevNextRow.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 2/10/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | @IBDesignable 12 | class PrevNextRow: NSStackView, NibLoadable, SplitterComponent { 13 | var run: SplitterRun! 14 | 15 | var customSpacing: CGFloat? = nil 16 | 17 | static func instantiateView(run: SplitterRun, viewController: ViewController) -> PrevNextRow { 18 | let row: PrevNextRow = PrevNextRow.instantiateView() 19 | row.run = run 20 | row.viewController = viewController 21 | row.initialization() 22 | return row 23 | } 24 | 25 | internal override func awakeAfter(using coder: NSCoder) -> Any? { 26 | return instantiateView() // You need to add this line to load view 27 | } 28 | 29 | internal override func awakeFromNib() { 30 | super.awakeFromNib() 31 | } 32 | private func initialization() { 33 | self.prevButton.run = self.run 34 | self.nextButton.run = self.run 35 | NotificationCenter.default.addObserver(forName: .timerStateChanged, object: run.timer, queue: nil, using: { notification in 36 | guard let timerState = notification.userInfo?["timerState"] as? TimerState else {return} 37 | var buttonsEnabled = false 38 | if timerState == .running { 39 | buttonsEnabled = true 40 | } 41 | self.prevButton.isEnabled = buttonsEnabled 42 | self.nextButton.isEnabled = buttonsEnabled 43 | self.nextButton.baseTitle = self.run.nextButtonTitle 44 | }) 45 | NotificationCenter.default.addObserver(forName: .splitChanged, object: run.timer, queue: nil, using: { notification in 46 | self.nextButton.baseTitle = self.run.nextButtonTitle 47 | }) 48 | } 49 | 50 | 51 | 52 | @IBOutlet var prevButton: ThemedButton! 53 | @IBOutlet var nextButton: ThemedButton! 54 | @IBOutlet var contentView: NSView! 55 | 56 | var viewController: ViewController! 57 | 58 | @IBAction func prevButtonClick(_ sender: NSButton?) { 59 | run.timer.previousSplit() 60 | } 61 | @IBAction func nextButtonClick(_ sender: NSButton?) { 62 | run.timer.splitOrStart() 63 | } 64 | var isSelected = false { 65 | didSet { 66 | self.didSetSelected() 67 | } 68 | } 69 | 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Components/SegmentComponent/SegmentComponent.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SegmentComponent.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 7/14/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Components/Shared/ComponentState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentState.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 7/5/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | ///Codable representation of a component's state 12 | /// 13 | ///This is used mostly for the `appearance.json` file to include the list of components, in order. 14 | ///``ComponentState`` can be used to store certain state information that is not supported in LiveSplitCore, or where it's too difficult to. 15 | ///To save/load state, see ``ComponentState/properties`` 16 | struct ComponentState: Codable { 17 | var type: SplitterComponentType 18 | 19 | //TODO: Finish Docs for this 20 | ///Contains the state information for the component 21 | /// 22 | ///Use this to save component state information to `appearance.json`, or to load component state information from it. 23 | /// 24 | ///Each value has a key and value 25 | var properties: [String: Codable] 26 | 27 | init(type: SplitterComponentType, properties: [String: Codable]) { 28 | self.type = type 29 | self.properties = properties 30 | } 31 | 32 | init(from decoder: Decoder) throws { 33 | self.type = try decoder.decode("type") 34 | self.properties = try decoder.decode("properties", as: [String: JSONAny].self) 35 | } 36 | func encode(to encoder: Encoder) throws { 37 | try encoder.encode(type, for: "type") 38 | let propMap: [String: JSONAny] = try properties.mapValues({ 39 | let e = try $0.encoded() 40 | return try e.decoded(as: JSONAny.self, using: JSONDecoder()) 41 | 42 | }) 43 | try encoder.encode(propMap, for: "properties") 44 | } 45 | 46 | /// Get the value of a state property 47 | /// - Parameters: 48 | /// - key: The key for which to get the value for 49 | /// - type: The type of value to return 50 | /// - Returns: If the state contains a property of the given type with the given key, it will return the value of that property. Otherwise, it returns `nil`. 51 | func getProperty(with key: String, of type: PropType.Type) -> PropType? where PropType: Decodable { 52 | return (properties[key] as? JSONAny)?.value as? PropType 53 | 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Components/Shared/SplitterComponentType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitterComponentType.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 7/5/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | enum SplitterComponentType: Int, Codable, CaseIterable { 11 | case title 12 | case splits 13 | case tableOptions 14 | case time 15 | case start 16 | case prevNext 17 | case sumOfBest 18 | case previousSegment 19 | case totalPlaytime 20 | case segment 21 | case possibleTimeSave 22 | 23 | var displayTitle: String { 24 | switch self { 25 | case .title: 26 | return "Title" 27 | case .splits: 28 | return "Splits" 29 | case .tableOptions: 30 | return "Options Row" 31 | case .time: 32 | return "Time" 33 | case .start: 34 | return "Start/Delete Buttons" 35 | case .prevNext: 36 | return "Prev/Next Buttons" 37 | case .sumOfBest: 38 | return "Sum of Best" 39 | case .previousSegment: 40 | return "Previous Segment" 41 | case .totalPlaytime: 42 | return "Total Playtime" 43 | case .segment: 44 | return "Current Segment" 45 | case .possibleTimeSave: 46 | return "Possible Time Save" 47 | } 48 | 49 | } 50 | 51 | var componentType: SplitterComponent.Type { 52 | switch self { 53 | case .title: 54 | return TitleComponent.self 55 | case .splits: 56 | return SplitsComponent.self 57 | case .tableOptions: 58 | return OptionsRow.self 59 | case .time: 60 | return TimeRow.self 61 | case .start: 62 | return StartRow.self 63 | case .prevNext: 64 | return PrevNextRow.self 65 | case .sumOfBest: 66 | return KeyValueComponent.self 67 | case .previousSegment: 68 | return KeyValueComponent.self 69 | case .totalPlaytime: 70 | return KeyValueComponent.self 71 | case .segment: 72 | return KeyValueComponent.self 73 | case .possibleTimeSave: 74 | return KeyValueComponent.self 75 | } 76 | } 77 | static func FromType(_ type: SplitterComponent) -> SplitterComponentType? { 78 | //Switch won't work here, and I have no idea why 79 | if type is TitleComponent { 80 | return .title 81 | } else if type is SplitsComponent { 82 | return .splits 83 | 84 | } else if type is OptionsRow { 85 | return .tableOptions 86 | } else if type is TimeRow { 87 | return .time 88 | } else if type is StartRow { 89 | return .start 90 | } else if type is PrevNextRow { 91 | return .prevNext 92 | } else if let kvc = type as? KeyValueComponent { 93 | switch kvc.key { 94 | case .sumOfBest: 95 | return .sumOfBest 96 | case .previousSegment: 97 | return .previousSegment 98 | case .totalPlaytime: 99 | return .totalPlaytime 100 | case .currentSegment: 101 | return .segment 102 | case .possibleTimeSave: 103 | return .possibleTimeSave 104 | default: 105 | return nil 106 | } 107 | } 108 | return nil 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Components/SplitsComponent/SplitTableColumnIdentifiers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitTableViewColumnIdentifiers.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 1/26/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | public enum STVColumnID { 13 | static let imageColumn = NSUserInterfaceItemIdentifier("ImageColumn") 14 | static let splitTitleColumn = NSUserInterfaceItemIdentifier("SplitTitle") 15 | static let differenceColumn = NSUserInterfaceItemIdentifier("Difference") 16 | static let currentSplitColumn = NSUserInterfaceItemIdentifier("CurrentSplit") 17 | static let bestSplitColumn = NSUserInterfaceItemIdentifier("B") 18 | static let previousSplitColumn = NSUserInterfaceItemIdentifier("PreviousSplit") 19 | 20 | } 21 | //TODO: Change how column width is saved. It's currently saved in runInfo, where it finds the column with the given ID and sets the width. Problem is, if the user has removed that column, it could crash. 22 | public var colIds: [String: NSUserInterfaceItemIdentifier] = [ 23 | "Icon": STVColumnID.imageColumn, 24 | "Title": STVColumnID.splitTitleColumn, 25 | "Difference": STVColumnID.differenceColumn, 26 | "Time": STVColumnID.currentSplitColumn, 27 | "Personal Best": STVColumnID.bestSplitColumn, 28 | "Previous Split": STVColumnID.previousSplitColumn 29 | ] 30 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Components/SplitsComponent/VerticalCenteredTextFieldCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VerticalCenteredTextFieldCell.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 6/1/23. 6 | // Copyright © 2023 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | ///A Text Field Cell that vertically centers its contents 12 | /// 13 | ///Used for the Splits component 14 | class VerticalCenteredTextFieldCell: NSTextFieldCell { 15 | override func titleRect(forBounds rect: NSRect) -> NSRect { 16 | var titleRect = super.titleRect(forBounds: rect) 17 | 18 | let minimumHeight = self.cellSize(forBounds: rect).height 19 | titleRect.origin.y += (titleRect.height - minimumHeight) / 2 20 | titleRect.size.height = minimumHeight 21 | 22 | return titleRect 23 | } 24 | 25 | override func drawInterior(withFrame cellFrame: NSRect, in controlView: NSView) { 26 | super.drawInterior(withFrame: titleRect(forBounds: cellFrame), in: controlView) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Components/StartRow/StartRow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StartRow.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 3/22/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | @IBDesignable 11 | class StartRow: NSStackView, NibLoadable, SplitterComponent { 12 | var run: SplitterRun! 13 | 14 | var customSpacing: CGFloat? = nil 15 | 16 | static func instantiateView(run: SplitterRun, viewController: ViewController) -> StartRow { 17 | let startRow: StartRow = StartRow.instantiateView() 18 | startRow.run = run 19 | startRow.viewController = viewController 20 | startRow.initialization() 21 | return startRow 22 | } 23 | 24 | internal override func awakeAfter(using coder: NSCoder) -> Any? { 25 | return instantiateView() // You need to add this line to load view 26 | } 27 | 28 | internal override func awakeFromNib() { 29 | super.awakeFromNib() 30 | } 31 | 32 | func initialization() { 33 | startButton.run = run 34 | stopButton.run = run 35 | stopButton.image = nil 36 | if #available(macOS 11.0, *) { 37 | stopButton.image = NSImage(systemSymbolName: "stop.circle.fill", accessibilityDescription: nil) 38 | } else { 39 | stopButton.image = NSImage(named: "stop") 40 | } 41 | 42 | if startButton.acceptsFirstResponder { 43 | startButton.window?.makeFirstResponder(startButton) 44 | } 45 | NotificationCenter.default.addObserver(forName: .timerStateChanged, object: run.timer, queue: nil, using: { notification in 46 | guard let timerState = notification.userInfo?["timerState"] as? TimerState else {return} 47 | switch timerState { 48 | case .paused: 49 | self.startButton.baseTitle = "Resume" 50 | case .running: 51 | self.startButton.baseTitle = "Pause" 52 | case .stopped: 53 | self.startButton.baseTitle = "Start" 54 | } 55 | self.setVisibleButtons() 56 | }) 57 | } 58 | 59 | func setVisibleButtons() { 60 | stopButton.isHidden = shouldStopButtonBeHidden 61 | } 62 | 63 | 64 | @IBOutlet var startButton: ThemedButton! 65 | @IBOutlet var stopButton: ThemedButton! 66 | 67 | var viewController: ViewController! 68 | 69 | 70 | var isSelected = false { 71 | didSet { 72 | self.didSetSelected() 73 | } 74 | } 75 | 76 | @IBAction func startButtonClick(_ sender: Any) { 77 | viewController?.toggleTimer() 78 | } 79 | @IBAction func stopButtonClick(_ sender: Any) { 80 | viewController?.cancelRun() 81 | } 82 | 83 | var shouldTrashCanBeHidden: Bool { 84 | switch run.timer.timerState { 85 | case .stopped: 86 | return false 87 | default: 88 | return true 89 | } 90 | } 91 | 92 | var shouldStopButtonBeHidden: Bool { 93 | stopButton.isEnabled = true 94 | switch run.timer.timerState { 95 | case .stopped: 96 | return true 97 | default: 98 | return false 99 | } 100 | } 101 | 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Components/TimeRow/TimeRowOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TimeRowOptions.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 6/4/23. 6 | // Copyright © 2023 Michael Berk. All rights reserved. 7 | // 8 | import AppKit 9 | import LiveSplitKit 10 | 11 | class TimeRowOptionsController: NSObject { 12 | 13 | init(timeRow: TimeRow) { 14 | self.timeRow = timeRow 15 | super.init() 16 | NotificationCenter.default.addObserver(forName: .fontChanged, object: self.timeRow.run, queue: nil, using: { _ in 17 | // self.fontStack.font = self.timeRow.run.codableLayout.timerFont 18 | }) 19 | } 20 | 21 | func fontChanged(to newFont: LiveSplitFont?) { 22 | self.timeRow.run.fontManager.setTimerFont(to: newFont) 23 | } 24 | 25 | var timeRow: TimeRow! 26 | var fontStack: ComponentOptionsFontStack! 27 | 28 | var showAttemptsLabelButton: ComponentOptionsButton { 29 | ComponentOptionsButton(checkboxWithTitle: "Show Attempts Label", clickAction: { button in 30 | let oldValue = self.timeRow.showAttemptsLabel 31 | self.timeRow.undoableSetting(actionName: "Set Show Attempts Label", oldValue: oldValue, newValue: !oldValue, edit: { comp, value in 32 | comp.showAttemptsLabel = value 33 | button.state = .init(bool: value) 34 | }) 35 | }) 36 | } 37 | 38 | var optionsView: NSView! { 39 | let d = timeRow.defaultComponentOptions() as! ComponentOptionsVstack 40 | let showAttemptsLabelButton = self.showAttemptsLabelButton 41 | //Can't move this outside of class since it needs the label button to exist 42 | let showAttemptsButton = ComponentOptionsButton(checkboxWithTitle: "Show Attempts", clickAction: {button in 43 | let oldValue = self.timeRow.showAttempts 44 | self.timeRow.undoableSetting(actionName: "Set Show Attempts", oldValue: oldValue, newValue: !oldValue, edit: {comp, value in 45 | comp.showAttempts = value 46 | button.state = .init(bool: value) 47 | showAttemptsLabelButton.isEnabled = value 48 | }) 49 | }) 50 | showAttemptsButton.state = .init(bool: timeRow.showAttempts) 51 | 52 | showAttemptsLabelButton.state = .init(bool: timeRow.showAttemptsLabel) 53 | showAttemptsLabelButton.sizeToFit() 54 | showAttemptsLabelButton.isEnabled = timeRow.showAttempts 55 | let bv = NSView(frame: NSRect(x: 0, y: 0, width: 20, height: showAttemptsLabelButton.frame.height)) 56 | NSLayoutConstraint.activate([ 57 | bv.widthAnchor.constraint(equalToConstant: 20) 58 | ]) 59 | bv.setContentHuggingPriority(.required, for: .horizontal) 60 | showAttemptsLabelButton.setContentHuggingPriority(.defaultLow, for: .horizontal) 61 | let attemptsLabelButtonStack = NSStackView(views: [bv, showAttemptsLabelButton]) 62 | attemptsLabelButtonStack.distribution = .equalSpacing 63 | attemptsLabelButtonStack.spacing = 0 64 | attemptsLabelButtonStack.alignment = .leading 65 | attemptsLabelButtonStack.orientation = .horizontal 66 | 67 | d.addArrangedSubview(showAttemptsButton) 68 | d.addArrangedSubview(attemptsLabelButtonStack) 69 | 70 | d.addSeparator() 71 | 72 | fontStack = ComponentOptionsFontStack(title: "Timer Font", fontSize: nil, helpText: "Font used to display the current time.", font: self.timeRow.run.codableLayout.timerFont, onFontChange: fontChanged(to:)) 73 | d.addArrangedSubview(fontStack) 74 | return d 75 | } 76 | 77 | } 78 | extension TimeRow { 79 | var optionsView: NSView! { 80 | optionsController.optionsView 81 | } 82 | } 83 | 84 | 85 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Controls/Button Actions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Button Actions.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 2/4/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Controls/Icon Actions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Icon Actions.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 2/4/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension ViewController { 13 | // MARK: - Icon Actions 14 | 15 | //TODO: Move This to MetadataImage 16 | /// Prompts the user to select a game icon image 17 | /// 18 | ///Used in right-click menu 19 | @IBAction func pictureButtonPressed(_ sender: Any) { 20 | let dialog = pictureFileDialog() 21 | 22 | dialog.beginSheetModal(for: view.window!) { (response) in 23 | if response == .OK { 24 | let result = dialog.url // Pathname of the file 25 | 26 | if (result != nil) { 27 | let imageFile = try? Data(contentsOf: result!) 28 | 29 | let myImage = NSImage(data: imageFile!) 30 | 31 | self.run.gameIcon = myImage 32 | self.gameIconButton.image = self.run.gameIcon 33 | 34 | } 35 | } 36 | } 37 | } 38 | 39 | func pictureFileDialog() -> NSOpenPanel{ 40 | let dialog = NSOpenPanel(); 41 | dialog.title = "Choose an image file" 42 | dialog.showsResizeIndicator = true 43 | dialog.showsHiddenFiles = false 44 | dialog.canChooseDirectories = false 45 | dialog.canCreateDirectories = false 46 | dialog.allowsMultipleSelection = false 47 | dialog.allowedFileTypes = ["png"] 48 | return dialog 49 | } 50 | ///Used in right-click menu for game icon 51 | @IBAction func removeGameIconMenuItem(sender: Any?) { 52 | removeGameIcon(sender: sender) 53 | } 54 | ///Used in right-click menu for game icon 55 | func removeGameIcon(sender: Any?) { 56 | gameIconButton.image = .gameControllerIcon 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Controls/StepperWithNumberField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StepperWithNumberField.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 12/14/23. 6 | // Copyright © 2023 Michael Berk. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | class StepperWithNumberField: NSStackView, NSTextFieldDelegate { 12 | //Based on https://stackoverflow.com/questions/70778911/how-do-i-define-an-action-for-a-custom-nscontrol-in-code 13 | 14 | var numberField: NSTextField! 15 | var stepper: NSStepper! 16 | 17 | var floatValue: Float { 18 | 19 | get { 20 | return stepper.floatValue 21 | } 22 | set { 23 | stepper.floatValue = newValue 24 | numberField.floatValue = newValue 25 | } 26 | } 27 | 28 | convenience init(minValue: Float, maxValue: Float) { 29 | 30 | let numberField = NSTextField() 31 | numberField.autoresizingMask = [.width, .height] 32 | numberField.floatValue = 100 33 | numberField.isEditable = true 34 | numberField.alignment = .right 35 | 36 | let formatter = NumberFormatter() 37 | formatter.allowsFloats = true 38 | formatter.minimum = minValue as NSNumber 39 | formatter.maximum = maxValue as NSNumber 40 | 41 | numberField.formatter = formatter 42 | let stepper = NSStepper() 43 | stepper.minValue = Double(minValue) 44 | stepper.maxValue = Double(maxValue) 45 | stepper.increment = 1 46 | stepper.valueWraps = false 47 | 48 | self.init(views: [numberField, stepper]) 49 | self.orientation = .horizontal 50 | self.numberField = numberField 51 | self.stepper = stepper 52 | 53 | self.numberField.delegate = self 54 | self.stepper.target = self 55 | self.stepper.action = #selector(self.stepperClicked) 56 | } 57 | 58 | required init?(coder: NSCoder) { 59 | fatalError("init(coder:) has not been implemented") 60 | } 61 | 62 | override init(frame frameRect: NSRect) { 63 | super.init(frame: frameRect) 64 | } 65 | 66 | func controlTextDidEndEditing(_ notification: Notification) { 67 | if let nobj = notification.object as? NSTextField { 68 | stepper.floatValue = nobj.floatValue 69 | self.handler?(self) 70 | } 71 | } 72 | 73 | public typealias Handler = (StepperWithNumberField) -> Void 74 | var handler: Handler? 75 | 76 | @objc func stepperClicked(sender: NSStepper) { 77 | numberField.floatValue = stepper.floatValue 78 | self.handler?(self) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Controls/Themeing/MetadataImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MetadataImage.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 7/7/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | ///Game Icon 12 | class MetadataImage: ThemedImage { 13 | override var themeable: Bool { 14 | get { 15 | if run.gameIcon == nil { 16 | return true 17 | } 18 | return false 19 | } 20 | set {} 21 | } 22 | 23 | required init?(coder: NSCoder) { 24 | super.init(coder: coder) 25 | setup() 26 | } 27 | override init(frame frameRect: NSRect) { 28 | super.init(frame: frameRect) 29 | setup() 30 | } 31 | func setup() { 32 | self.target = self 33 | self.action = #selector(imageChanged(_:)) 34 | NotificationCenter.default.addObserver(forName: .gameIconEdited, object: nil, queue: nil, using: { notification in 35 | self.image = self.run.gameIcon 36 | self.setPlaceholderImage() 37 | self.setColor() 38 | }) 39 | } 40 | override func setColor() { 41 | if themeable { 42 | self.contentTintColor = run.textColor 43 | } 44 | } 45 | 46 | func setPlaceholderImage() { 47 | if self.image == nil { 48 | self.image = .gameControllerIcon 49 | } 50 | } 51 | 52 | @objc func imageChanged(_ sender: Any?) { 53 | run.gameIcon = self.image 54 | setPlaceholderImage() 55 | } 56 | 57 | var allowsUpdate = true 58 | 59 | } 60 | extension MetadataImage { 61 | override func mouseDown(with event: NSEvent) { 62 | if event.clickCount > 1 { 63 | self.setImage() 64 | } 65 | } 66 | func pictureFileDialog() -> NSOpenPanel{ 67 | let dialog = NSOpenPanel(); 68 | dialog.title = "Choose an image file" 69 | dialog.showsResizeIndicator = true 70 | dialog.showsHiddenFiles = false 71 | dialog.canChooseDirectories = false 72 | dialog.canCreateDirectories = false 73 | dialog.allowsMultipleSelection = false 74 | dialog.allowedFileTypes = ["png"] 75 | return dialog 76 | } 77 | 78 | func setImage() { 79 | let dialog = pictureFileDialog() 80 | 81 | let response = dialog.runModal() 82 | if response == .OK { 83 | let result = dialog.url 84 | 85 | if (result != nil) { 86 | let imageFile = try? Data(contentsOf: result!) 87 | 88 | let myImage = NSImage(data: imageFile!) 89 | 90 | self.image = myImage 91 | self.imageChanged(nil) 92 | } 93 | } 94 | } 95 | } 96 | 97 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Controls/Themeing/Themeable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Themeable.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 7/7/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | protocol Themeable { 12 | func setColor() 13 | func setColorObserver() 14 | var run: SplitterRun! {get} 15 | var themeable: Bool {get set} 16 | } 17 | extension Themeable { 18 | //TODO: What was this used for? 19 | static var runKey: String { 20 | return "run" 21 | } 22 | func setColorObserver() { 23 | NotificationCenter.default.addObserver(forName: .updateComponentColors, object: run, queue: nil, using: { notification in 24 | if themeable { 25 | setColor() 26 | } 27 | }) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Controls/Themeing/ThemedButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemedButton.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 4/8/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ThemedButton: NSButton, Themeable, Fontable { 12 | 13 | var run: SplitterRun! 14 | var defaultFontSize: CGFloat? 15 | var fixedFontSize: Bool { 16 | return true 17 | } 18 | 19 | override init(frame frameRect: NSRect) { 20 | super.init(frame: frameRect) 21 | setColorObserver() 22 | setFontObserver() 23 | } 24 | required init?(coder: NSCoder) { 25 | super.init(coder: coder) 26 | setColorObserver() 27 | setFontObserver() 28 | } 29 | 30 | func setColor() { 31 | var newImage = self.image 32 | newImage?.isTemplate = true 33 | newImage = newImage?.image(with: run.textColor) 34 | self.image = newImage 35 | self.contentTintColor = run.textColor 36 | self.attributedTitle = NSAttributedString(string: self.title, attributes: [.foregroundColor: run.textColor]) 37 | self.appearance = NSAppearance(named: .darkAqua) 38 | self.shadow = .none 39 | } 40 | 41 | var themeable: Bool = true 42 | var fontable: Bool = true 43 | 44 | override func draw(_ dirtyRect: NSRect) { 45 | super.draw(dirtyRect) 46 | 47 | // Drawing code here. 48 | } 49 | override func mouseDown(with event: NSEvent) { 50 | window?.makeFirstResponder(nil) 51 | super.mouseDown(with: event) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Controls/Themeing/ThemedImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemedImage.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 7/7/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ThemedImage: NSImageView, Themeable { 12 | @IBInspectable var themeable: Bool = true 13 | var run: SplitterRun! 14 | override init(frame frameRect: NSRect) { 15 | super.init(frame: frameRect) 16 | setColorObserver() 17 | } 18 | 19 | required init?(coder: NSCoder) { 20 | super.init(coder: coder) 21 | setColorObserver() 22 | } 23 | func setColor() { 24 | if let image = self.image, image.isTemplate { 25 | self.contentTintColor = run.textColor 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Controls/Themeing/ThemedPopUpButton.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemedPopUpButton.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 4/8/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ThemedPopUpButton: NSPopUpButton, Themeable { 12 | var run: SplitterRun! 13 | var themeable: Bool = true 14 | 15 | override init(frame frameRect: NSRect) { 16 | super.init(frame: frameRect) 17 | setColorObserver() 18 | 19 | } 20 | required init?(coder: NSCoder) { 21 | super.init(coder: coder) 22 | setColorObserver() 23 | } 24 | 25 | func setColor() { 26 | 27 | self.contentTintColor = run.textColor 28 | //not used now, but could be useful in the future for popup buttons with titles instead of images 29 | self.attributedTitle = NSAttributedString(string: self.title, attributes: [.foregroundColor: run.textColor]) 30 | if let menu = menu { 31 | setColors(for: menu, color: run.textColor) 32 | } 33 | self.appearance = NSAppearance(named: .darkAqua) 34 | } 35 | func setColors(for menu: NSMenu, color: NSColor) { 36 | for item in menu.items { 37 | if var newImage = item.image { 38 | newImage.isTemplate = true 39 | newImage = newImage.image(with: color) 40 | item.image = newImage 41 | } 42 | if item.hasSubmenu, let submenu = item.submenu { 43 | setColors(for: submenu, color: color) 44 | } 45 | 46 | } 47 | } 48 | 49 | override func draw(_ dirtyRect: NSRect) { 50 | super.draw(dirtyRect) 51 | 52 | // Drawing code here. 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Controls/Themeing/ThemedTextField.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ThemedTextField.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 4/8/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class ThemedTextField: NSTextField, Themeable, Fontable { 12 | 13 | var run: SplitterRun! 14 | var defaultFontSize: CGFloat? 15 | 16 | @IBInspectable var fontable: Bool = true 17 | @IBInspectable var themeable: Bool = true 18 | @IBInspectable var fixedFontSize: Bool = false 19 | 20 | override init(frame frameRect: NSRect) { 21 | super.init(frame: frameRect) 22 | setColorObserver() 23 | setFontObserver() 24 | } 25 | 26 | required init?(coder: NSCoder) { 27 | super.init(coder: coder) 28 | setColorObserver() 29 | setFontObserver() 30 | } 31 | 32 | func setColor() { 33 | self.textColor = run.textColor 34 | } 35 | 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Options Windows/Layout Editor/ComponentOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ComponentOptions.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 7/5/21. 6 | // Copyright © 2021 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | import LiveSplitKit 11 | 12 | class ComponentOptionsButton: NSButton { 13 | var clickAction: (NSButton) -> () = {_ in} 14 | override func mouseDown(with event: NSEvent) { 15 | super.mouseDown(with: event) 16 | if isEnabled { 17 | clickAction(self) 18 | } 19 | } 20 | 21 | convenience init (checkboxWithTitle title: String, clickAction: @escaping (NSButton) -> ()) { 22 | self.init(checkboxWithTitle: title, target: nil, action: nil) 23 | self.clickAction = clickAction 24 | } 25 | convenience init (title: String, clickAction: @escaping (NSButton) -> ()) { 26 | self.init(title: title, target: nil, action: nil) 27 | self.clickAction = clickAction 28 | } 29 | convenience init(image: NSImage, clickAction: @escaping (NSButton) -> ()) { 30 | self.init(image: image, target: nil, action: nil) 31 | self.clickAction = clickAction 32 | } 33 | } 34 | 35 | class ComponentPopUpButton: NSPopUpButton { 36 | var customAction: (NSPopUpButton) -> () = {_ in} 37 | @objc func selectAction(_ sender: NSPopUpButton) { 38 | customAction(sender) 39 | } 40 | 41 | convenience init(title: String, selectAction: @escaping (NSPopUpButton) -> ()) { 42 | self.init(title: title, target: nil, action: #selector(selectAction(_:))) 43 | self.target = self 44 | self.customAction = selectAction 45 | } 46 | 47 | } 48 | 49 | class ComponentOptionsVstack: NSStackView { 50 | override init(frame frameRect: NSRect) { 51 | super.init(frame: frameRect) 52 | self.orientation = .vertical 53 | self.alignment = .leading 54 | } 55 | 56 | required init?(coder: NSCoder) { 57 | fatalError("init(coder:) has not been implemented") 58 | } 59 | 60 | override var isFlipped: Bool { 61 | return true 62 | } 63 | 64 | var onFontChanged: ((_ sender: NSFontManager?) -> Void) = {_ in} 65 | var fontPanelModes: NSFontPanel.ModeMask = [.standardModes] 66 | 67 | @discardableResult func addSeparator() -> NSView { 68 | let separatorView = NSView() 69 | separatorView.wantsLayer = true 70 | separatorView.layer?.backgroundColor = NSColor.separatorColor.cgColor 71 | self.addArrangedSubview(separatorView) 72 | NSLayoutConstraint.activate([ 73 | separatorView.heightAnchor.constraint(equalToConstant: 1) 74 | ]) 75 | return separatorView 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Options Windows/Layout Editor/DragIndicator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DragIndicator.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 10/21/22. 6 | // Copyright © 2022 Michael Berk. All rights reserved. 7 | // 8 | 9 | import AppKit 10 | 11 | @IBDesignable 12 | class DragIndicator: NSView { 13 | 14 | @IBInspectable var alwaysShowIcon: Bool = false { 15 | didSet { 16 | imageView.isHidden = !alwaysShowIcon 17 | } 18 | } 19 | 20 | var imageView: NSImageView! 21 | 22 | override init(frame frameRect: NSRect) { 23 | super.init(frame: frameRect) 24 | setup() 25 | } 26 | func setup() { 27 | var image: NSImage 28 | 29 | if #available(macOS 11.0, *) { 30 | //macOS ≥ 12 has a different name for the symbol, so we need to use the proper name on macOS 11 31 | var imageName = "line.3.horizontal" 32 | if ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 11 { 33 | imageName = "line.horizontal.3" 34 | } 35 | image = NSImage(systemSymbolName: imageName, accessibilityDescription: nil)! 36 | } else { 37 | image = NSImage(named: "lines")! 38 | } 39 | let imageView = NSImageView(image: image) 40 | self.imageView = imageView 41 | 42 | self.addSubview(imageView) 43 | let trackingArea = NSTrackingArea(rect: bounds, options: [.activeInKeyWindow, .mouseEnteredAndExited], owner: self) 44 | addTrackingArea(trackingArea) 45 | self.imageView.isHidden = !alwaysShowIcon 46 | } 47 | 48 | override func layout() { 49 | super.layout() 50 | if let imageView { 51 | let x = (self.frame.width - 16) * 0.5 52 | let y = (self.frame.height - 9) * 0.5 53 | let f = NSRect(x: x, y: y, width: 16, height: 9) 54 | imageView.frame = f 55 | } 56 | } 57 | 58 | required init?(coder: NSCoder) { 59 | super.init(coder: coder) 60 | setup() 61 | } 62 | 63 | override func mouseEntered(with event: NSEvent) { 64 | super.mouseEntered(with: event) 65 | if !alwaysShowIcon { 66 | self.imageView.isHidden = false 67 | } 68 | } 69 | 70 | override func mouseExited(with event: NSEvent) { 71 | super.mouseExited(with: event) 72 | if !alwaysShowIcon { 73 | self.imageView.isHidden = true 74 | } 75 | } 76 | 77 | override func mouseUp(with event: NSEvent) { 78 | super.mouseUp(with: event) 79 | if !alwaysShowIcon { 80 | self.imageView.isHidden = true 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Options Windows/Layout Editor/LayoutEditorListCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutEditorListCell.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 10/27/22. 6 | // Copyright © 2022 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class LayoutEditorListCell: NSTableCellView { 12 | 13 | @IBOutlet private var dragIndicator: DragIndicator? 14 | 15 | override init(frame frameRect: NSRect) { 16 | super.init(frame: frameRect) 17 | setup() 18 | } 19 | 20 | required init?(coder: NSCoder) { 21 | super.init(coder: coder) 22 | setup() 23 | } 24 | func setup() { 25 | ///See the documentation on ``layoutEditorItemDragged`` for more info on why I call this 26 | NotificationCenter.default.addObserver(forName: .layoutEditorItemDragged, object: nil, queue: nil, using: { notification in 27 | if self.dragIndicator?.alwaysShowIcon == false { 28 | self.dragIndicator?.imageView.isHidden = true 29 | } 30 | }) 31 | } 32 | 33 | override func draw(_ dirtyRect: NSRect) { 34 | super.draw(dirtyRect) 35 | 36 | // Drawing code here. 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/UISettings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UISettings.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 1/7/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | extension ViewController { 13 | 14 | var showHideTitleBarItemText: String { 15 | get { 16 | if titleBarHidden { 17 | return "Show Title Bar" 18 | } else { 19 | return "Hide Title Bar" 20 | } 21 | } 22 | } 23 | ///Sets the window to stay on top, depending on the current setting 24 | func setFloatingWindow() { 25 | if !Settings.menuBarMode { 26 | let id = menuIdentifiers.windowMenu.windowFloat 27 | if let menuItem = NSApp.mainMenu?.item(withIdentifier: id) { 28 | if windowFloat { 29 | view.window?.level = .floating 30 | menuItem.state = .on 31 | } else { 32 | view.window?.level = .normal 33 | menuItem.state = .off 34 | } 35 | } 36 | } 37 | } 38 | 39 | ///Shows or hides the title bar, depending on the current setting 40 | func showHideTitleBar() { 41 | if titleBarHidden { 42 | 43 | view.window?.styleMask.insert(.fullSizeContentView) 44 | view.window?.standardWindowButton(.closeButton)?.isHidden = true 45 | view.window?.standardWindowButton(.miniaturizeButton)?.isHidden = true 46 | view.window?.standardWindowButton(.zoomButton)?.isHidden = true 47 | view.window?.standardWindowButton(.documentIconButton)?.isHidden = true 48 | view.window?.titleVisibility = .hidden 49 | } else { 50 | view.window?.standardWindowButton(.closeButton)?.isHidden = false 51 | view.window?.standardWindowButton(.miniaturizeButton)?.isHidden = false 52 | view.window?.standardWindowButton(.zoomButton)?.isHidden = false 53 | view.window?.standardWindowButton(.documentIconButton)?.isHidden = false 54 | view.window?.titleVisibility = .visible 55 | view.window?.styleMask.remove(.fullSizeContentView) 56 | } 57 | let showHideTitleBarItem = NSApp.mainMenu?.item(withIdentifier: menuIdentifiers.appearanceMenu.hideTitleBar) 58 | showHideTitleBarItem?.title = showHideTitleBarItemText 59 | } 60 | func showHideTitle() { 61 | if hideTitle { 62 | view.window?.titleVisibility = .hidden 63 | } else { 64 | view.window?.titleVisibility = .visible 65 | } 66 | } 67 | 68 | 69 | @IBAction func showHideTitleBarMenuItem(_ sender: Any? ) { 70 | titleBarHidden.toggle() 71 | showHideTitleBar() 72 | } 73 | 74 | var showHideButtonsText: String { 75 | get { 76 | if buttonHidden { 77 | return "Show Buttons" 78 | } else { 79 | return "Hide Buttons" 80 | } 81 | } 82 | } 83 | } 84 | 85 | extension NSWindow { 86 | var titlebarHeight: CGFloat { 87 | if let windowFrameHeight = contentView?.frame.height { 88 | let contentLayoutRectHeight = contentLayoutRect.height 89 | let fullSizeContentViewNoContentAreaHeight = windowFrameHeight - contentLayoutRectHeight 90 | return fullSizeContentViewNoContentAreaHeight 91 | } 92 | return 0 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/WelcomeWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // WelcomeWindowController.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 7/27/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class WelcomeWindowController: NSWindowController { 12 | 13 | override func windowDidLoad() { 14 | super.windowDidLoad() 15 | //Need to close the window controller so that `NSApp.windows` removes the welcome window when closed 16 | NotificationCenter.default.addObserver(forName: NSWindow.willCloseNotification, object: self.window, queue: nil, using: { _ in 17 | self.close() 18 | }) 19 | // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file. 20 | 21 | } 22 | 23 | 24 | 25 | 26 | } 27 | 28 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Windows/BlankWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlankWindowController.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 1/13/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class BlankWindowController: NSWindowController { 12 | 13 | override func windowDidLoad() { 14 | super.windowDidLoad() 15 | 16 | // Implement this method to handle any initialization after your window controller's window has been loaded from its nib file. 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Windows/MainWindow.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainWindow.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 1/13/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | ///This class is needed for the Hotkeys to work. I don't remeber why at the moment. 12 | class MainWindow: NSWindow { 13 | 14 | var observer: Any! 15 | override init(contentRect: NSRect, styleMask style: NSWindow.StyleMask, backing backingStoreType: NSWindow.BackingStoreType, defer flag: Bool) { 16 | super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag) 17 | observer = NSWorkspace.shared.notificationCenter.addObserver(forName: NSWorkspace.activeSpaceDidChangeNotification, object: nil, queue: nil, using: { _ in 18 | if Settings.menuBarMode { 19 | self.orderFront(self) 20 | } 21 | }) 22 | } 23 | 24 | override func setTitleWithRepresentedFilename(_ filename: String) { 25 | super.setTitleWithRepresentedFilename(filename) 26 | 27 | } 28 | 29 | //need to override `close` and remove the observer or the window will reappear after changing spaces in Overlay Mode 30 | override func close() { 31 | super.close() 32 | NSWorkspace.shared.notificationCenter.removeObserver(observer!, name: NSWorkspace.activeSpaceDidChangeNotification, object: nil) 33 | } 34 | } 35 | 36 | 37 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Windows/MainWindowController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainWindowController.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 12/22/19. 6 | // Copyright © 2019 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Cocoa 10 | 11 | class MainWindowController: NSWindowController { 12 | 13 | override func windowDidLoad() { 14 | super.windowDidLoad() 15 | NotificationCenter.default.addObserver(forName: .menuBarModeChanged, object: nil, queue: nil, using: { _ in 16 | self.updateFromMenuBarMode() 17 | }) 18 | updateFromMenuBarMode() 19 | } 20 | 21 | func updateFromMenuBarMode() { 22 | if Settings.menuBarMode { 23 | window?.level = .statusBar 24 | window?.collectionBehavior = .canJoinAllSpaces 25 | } else { 26 | window?.collectionBehavior = .fullScreenAuxiliary 27 | } 28 | 29 | 30 | } 31 | 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Windows/QuickAlert.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QuickAlert.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 1/31/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Cocoa 11 | 12 | class QuickAlert { 13 | 14 | let alert: NSAlert 15 | 16 | init(message: String, image: NSImage?) { 17 | alert = NSAlert() 18 | alert.messageText = message 19 | if let img = image { 20 | alert.icon = img 21 | } 22 | alert.alertStyle = .informational 23 | alert.addButton(withTitle: "OK") 24 | } 25 | 26 | func show() { 27 | alert.runModal() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Splitter/UI/View Controller/Windows/blankWindow.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /Splitter/UpdateController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UpdaterStuff.swift 3 | // Splitter 4 | // 5 | // Created by Michael Berk on 2/14/23. 6 | // Copyright © 2023 Michael Berk. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Defaults 11 | import BasicUpdater 12 | 13 | class UpdateController:NSObject { 14 | static let shared = UpdateController() 15 | 16 | let updater = Updater(projectURL: URL(string: "https://github.com/michaeljberk/Splitter"), shouldUpdateTo: { release in 17 | let regexStr = #"[\d .]*-\d*"# 18 | let tag = release.tagName 19 | guard let tagVerRange = tag.range(of: regexStr, options: .regularExpression) else {return false} 20 | let tagVer = tag[tagVerRange] 21 | let tagVerSplit = tagVer.split(separator: "-") 22 | guard tagVerSplit.count > 1 else {return false} 23 | let newBuildStr = tagVerSplit[1] 24 | guard let newBuildNum = Int(newBuildStr) else {return false} 25 | let currentBuildStr = Bundle.main.infoDictionary?["CFBundleVersion"] as! String 26 | let currentBuildNum = Int(currentBuildStr)! 27 | return newBuildNum > currentBuildNum 28 | }) 29 | 30 | @objc func checkForUpdates() { 31 | updater.checkForUpdates() 32 | } 33 | 34 | func addUpdateCommand() { 35 | let settingsItem = NSApp.mainMenu!.item(withIdentifier: menuIdentifiers.appMenu.settingsMenuItem)! 36 | let appMenu = NSApp.mainMenu!.items[0].submenu! 37 | let settingsIndex = appMenu.index(of: settingsItem) 38 | let updateItem = NSMenuItem(title: "Check for Updates...", action: #selector(checkForUpdates), keyEquivalent: "") 39 | updateItem.target = self 40 | appMenu.insertItem(updateItem, at: settingsIndex + 1) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /SplitterTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 6.1 19 | CFBundleVersion 20 | 328 21 | 22 | 23 | -------------------------------------------------------------------------------- /SplitterTests/Odyssey.json: -------------------------------------------------------------------------------- 1 | {"segments":[{"name":"Cappy","endedAt":{"gametimeMS":70,"realtimeMS":70},"bestDuration":{"gametimeMS":70,"realtimeMS":70}},{"name":"Cascade","endedAt":{"gametimeMS":2032,"realtimeMS":2032},"bestDuration":{"gametimeMS":2032,"realtimeMS":2032}},{"name":"Sand","endedAt":{"gametimeMS":4022,"realtimeMS":4022},"bestDuration":{"gametimeMS":4022,"realtimeMS":4022}},{"name":"Lake","endedAt":{"gametimeMS":5044,"realtimeMS":5044},"bestDuration":{"gametimeMS":5044,"realtimeMS":5044}},{"name":"Wooded","endedAt":{"gametimeMS":6070,"realtimeMS":6070},"bestDuration":{"gametimeMS":6070,"realtimeMS":6070}},{"name":"Cloud","endedAt":{"gametimeMS":7054,"realtimeMS":7054},"bestDuration":{"gametimeMS":7054,"realtimeMS":7054}},{"name":"Lost","endedAt":{"gametimeMS":8072,"realtimeMS":8072},"bestDuration":{"gametimeMS":8072,"realtimeMS":8072}},{"name":"Metro (Night)","endedAt":{"gametimeMS":13045,"realtimeMS":13045},"bestDuration":{"gametimeMS":13045,"realtimeMS":13045}},{"name":"Metro (Day)","endedAt":{"gametimeMS":14060,"realtimeMS":14060},"bestDuration":{"gametimeMS":14060,"realtimeMS":14060}},{"name":"Seaside","endedAt":{"gametimeMS":15021,"realtimeMS":15021},"bestDuration":{"gametimeMS":15021,"realtimeMS":15021}},{"name":"Snow","endedAt":{"gametimeMS":15070,"realtimeMS":15070},"bestDuration":{"gametimeMS":15070,"realtimeMS":15070}},{"name":"Luncheon","endedAt":{"gametimeMS":16003,"realtimeMS":16003},"bestDuration":{"gametimeMS":16003,"realtimeMS":16003}},{"name":"Ruined","endedAt":{"gametimeMS":16032,"realtimeMS":16032},"bestDuration":{"gametimeMS":16032,"realtimeMS":16032}},{"name":"Bowsers","endedAt":{"gametimeMS":16064,"realtimeMS":16064},"bestDuration":{"gametimeMS":16064,"realtimeMS":16064}},{"name":"Moon","endedAt":{"gametimeMS":17000,"realtimeMS":17000},"bestDuration":{"gametimeMS":17000,"realtimeMS":17000}}],"schemaVersion":"v1.0.1","timer":{"website":"https:\/\/mberk.com\/splitter","shortname":"Splitter","longname":"Splitter","version":"v1.0 (44)"},"game":{"longname":"Super Mario Odyssey"},"category":{"longname":"Any %"}} -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/appearance.json: -------------------------------------------------------------------------------- 1 | {"hideButtons":true,"tableColor":{"red":0.20000000298023224,"alpha":1,"blue":0.20000000298023224,"green":0.20000000298023224},"keepOnTop":false,"roundTo":0,"columnSizes":{"Icon":32,"Previous Split":152,"Difference":99.5,"Time":90,"Title":80,"Personal Best":85},"bgColor":{"red":0.10000000149011612,"alpha":1,"blue":0.10000000149011612,"green":0.10000000149011612},"textColor":{"red":1,"alpha":1,"blue":1,"green":1},"windowHeight":805,"hideColumns":{"Icon":false,"Previous Split":false,"Difference":false,"Time":false,"Title":false,"Personal Best":false},"hideTitlebar":false,"diffsLongerColor":{"red":1,"alpha":1,"blue":0,"green":0},"diffsShorterColor":{"red":0,"alpha":1,"blue":0,"green":1},"windowWidth":734,"selectColor":{"red":0.35686275362968445,"alpha":1,"blue":0.70980393886566162,"green":0.29019609093666077}} -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/gameIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/gameIcon.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/runInfo.json: -------------------------------------------------------------------------------- 1 | {"id":"47476FFC-0044-4A04-B8B6-CBEB2D104646","category":"Any %","gameVersion":"3.1","version":"3.1","compareTo":0,"gameRegion":"USA","segments":[{"previousPersonalBestTime":"00:00:01.41","personalBestTime":"00:00:00.70","name":"Cappy","previousTime":"00:00:00.99","currentTime":"00:00:01.22"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:02.32","name":"Cascade","previousTime":"00:00:00.00","currentTime":"00:00:02.32"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:04.22","name":"Sand","previousTime":"00:00:00.00","currentTime":"00:00:04.22"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:05.44","name":"Lake","previousTime":"00:00:00.00","currentTime":"00:00:05.44"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:06.70","name":"Wooded","previousTime":"00:00:00.00","currentTime":"00:00:06.70"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:07.54","name":"Cloud","previousTime":"00:00:00.00","currentTime":"00:00:07.54"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:08.72","name":"Lost","previousTime":"00:00:00.00","currentTime":"00:00:08.72"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:13.45","name":"Metro (Night)","previousTime":"00:00:00.00","currentTime":"00:00:13.45"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:14.60","name":"Metro (Day)","previousTime":"00:00:00.00","currentTime":"00:00:14.60"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:15.21","name":"Seaside","previousTime":"00:00:00.00","currentTime":"00:00:15.21"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:15.70","name":"Snow","previousTime":"00:00:00.00","currentTime":"00:00:15.70"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:16.03","name":"Luncheon","previousTime":"00:00:00.00","currentTime":"00:00:16.03"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:16.32","name":"Ruined","previousTime":"00:00:00.00","currentTime":"00:00:16.32"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:16.64","name":"Bowsers","previousTime":"00:00:00.00","currentTime":"00:00:16.64"},{"previousPersonalBestTime":"00:00:00.00","personalBestTime":"00:00:17.00","name":"Moon","previousTime":"00:00:00.00","currentTime":"01:00:17.00"}],"title":"Super Mario Odyssey","attempts":3,"platform":"Switch","endTime":"2020-03-08T05:20:43Z","startTime":"2020-03-08T05:20:42Z","build":"170"} -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/0.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/1.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/10.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/11.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/12.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/13.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/14.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/2.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/3.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/4.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/5.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/6.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/7.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/8.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey.split/segIcons/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelJBerk/Splitter/5899aabffd0884694827c94827bae2c6e68a3265/SplitterTests/Odyssey.split/segIcons/9.png -------------------------------------------------------------------------------- /SplitterTests/Odyssey2 copy.json: -------------------------------------------------------------------------------- 1 | {"segments":[{"name":"Cappy","endedAt":{"gametimeMS":70,"realtimeMS":70},"bestDuration":{"gametimeMS":70,"realtimeMS":70}},{"name":"Cascade","endedAt":{"gametimeMS":232,"realtimeMS":232},"bestDuration":{"gametimeMS":232,"realtimeMS":232}},{"name":"Sand","endedAt":{"gametimeMS":422,"realtimeMS":422},"bestDuration":{"gametimeMS":422,"realtimeMS":422}},{"name":"Lake","endedAt":{"gametimeMS":544,"realtimeMS":544},"bestDuration":{"gametimeMS":544,"realtimeMS":544}},{"name":"Wooded","endedAt":{"gametimeMS":670,"realtimeMS":670},"bestDuration":{"gametimeMS":670,"realtimeMS":670}},{"name":"Cloud","endedAt":{"gametimeMS":754,"realtimeMS":754},"bestDuration":{"gametimeMS":754,"realtimeMS":754}},{"name":"Lost","endedAt":{"gametimeMS":872,"realtimeMS":872},"bestDuration":{"gametimeMS":872,"realtimeMS":872}},{"name":"Metro (Night)","endedAt":{"gametimeMS":1345,"realtimeMS":1345},"bestDuration":{"gametimeMS":1345,"realtimeMS":1345}},{"name":"Metro (Day)","endedAt":{"gametimeMS":1460,"realtimeMS":1460},"bestDuration":{"gametimeMS":1460,"realtimeMS":1460}},{"name":"Seaside","endedAt":{"gametimeMS":1521,"realtimeMS":1521},"bestDuration":{"gametimeMS":1521,"realtimeMS":1521}},{"name":"Snow","endedAt":{"gametimeMS":1570,"realtimeMS":1570},"bestDuration":{"gametimeMS":1570,"realtimeMS":1570}},{"name":"Luncheon","endedAt":{"gametimeMS":1603,"realtimeMS":1603},"bestDuration":{"gametimeMS":1603,"realtimeMS":1603}},{"name":"Ruined","endedAt":{"gametimeMS":1632,"realtimeMS":1632},"bestDuration":{"gametimeMS":1632,"realtimeMS":1632}},{"name":"Bowsers","endedAt":{"gametimeMS":1664,"realtimeMS":1664},"bestDuration":{"gametimeMS":1664,"realtimeMS":1664}},{"name":"Moon","endedAt":{"gametimeMS":1700,"realtimeMS":1700},"bestDuration":{"gametimeMS":1700,"realtimeMS":1700}}],"additionalProperties":false,"_schemaVersion":"v1.0.1","timer":{"website":"https:\/\/mberk.com\/splitter","shortname":"Splitter","longname":"Splitter","version":"v1.0 (61)"},"game":{"longname":"Super Mario Odyssey"},"category":{"longname":"Any %"}} -------------------------------------------------------------------------------- /SplitterTests/Odyssey2.json: -------------------------------------------------------------------------------- 1 | {"segments":[{"name":"Cappy","endedAt":{"gametimeMS":70,"realtimeMS":70},"bestDuration":{"gametimeMS":70,"realtimeMS":70}},{"name":"Cascade","endedAt":{"gametimeMS":232,"realtimeMS":232},"bestDuration":{"gametimeMS":232,"realtimeMS":232}},{"name":"Sand","endedAt":{"gametimeMS":422,"realtimeMS":422},"bestDuration":{"gametimeMS":422,"realtimeMS":422}},{"name":"Lake","endedAt":{"gametimeMS":544,"realtimeMS":544},"bestDuration":{"gametimeMS":544,"realtimeMS":544}},{"name":"Wooded","endedAt":{"gametimeMS":670,"realtimeMS":670},"bestDuration":{"gametimeMS":670,"realtimeMS":670}},{"name":"Cloud","endedAt":{"gametimeMS":754,"realtimeMS":754},"bestDuration":{"gametimeMS":754,"realtimeMS":754}},{"name":"Lost","endedAt":{"gametimeMS":872,"realtimeMS":872},"bestDuration":{"gametimeMS":872,"realtimeMS":872}},{"name":"Metro (Night)","endedAt":{"gametimeMS":1345,"realtimeMS":1345},"bestDuration":{"gametimeMS":1345,"realtimeMS":1345}},{"name":"Metro (Day)","endedAt":{"gametimeMS":1460,"realtimeMS":1460},"bestDuration":{"gametimeMS":1460,"realtimeMS":1460}},{"name":"Seaside","endedAt":{"gametimeMS":1521,"realtimeMS":1521},"bestDuration":{"gametimeMS":1521,"realtimeMS":1521}},{"name":"Snow","endedAt":{"gametimeMS":1570,"realtimeMS":1570},"bestDuration":{"gametimeMS":1570,"realtimeMS":1570}},{"name":"Luncheon","endedAt":{"gametimeMS":1603,"realtimeMS":1603},"bestDuration":{"gametimeMS":1603,"realtimeMS":1603}},{"name":"Ruined","endedAt":{"gametimeMS":1632,"realtimeMS":1632},"bestDuration":{"gametimeMS":1632,"realtimeMS":1632}},{"name":"Bowsers","endedAt":{"gametimeMS":1664,"realtimeMS":1664},"bestDuration":{"gametimeMS":1664,"realtimeMS":1664}},{"name":"Moon","endedAt":{"gametimeMS":1700,"realtimeMS":1700},"bestDuration":{"gametimeMS":1700,"realtimeMS":1700}}],"schemaVersion":"v1.0.1","timer":{"website":"https:\/\/mberk.com\/splitter","shortname":"Splitter","longname":"Splitter","version":"v1.0 (61)"},"game":{"longname":"Super Mario Odyssey"},"category":{"longname":"Any %"}} -------------------------------------------------------------------------------- /SplitterTests/SplitterTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitterTests.swift 3 | // SplitterTests 4 | // 5 | // Created by Michael Berk on 7/14/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Splitter 11 | import Files 12 | import SwiftyJSON 13 | 14 | class SplitterTests: XCTestCase { 15 | 16 | //Path to the SplitterTests folder 17 | fileprivate let testPath = URL(fileURLWithPath: #file).pathComponents.dropLast().joined(separator: "/").dropFirst() //.pathComponents 18 | //.prefix(while: { $0 != "Tests" }).joined(separator: "/").dropFirst() 19 | 20 | var viewController: ViewController? 21 | 22 | override func setUp() { 23 | // Put setup code here. This method is called before the invocation of each test method in the class. 24 | self.viewController = ViewController() 25 | } 26 | // func testGlobalHotkeysSetup() { 27 | // let globalHotkeySetting = Settings.enableGlobalHotkeys 28 | // if let app = NSApp.delegate as? AppDelegate { 29 | // let hotkey1Paused = app.keybinds[0].hotkey?.isPaused 30 | // XCTAssert((hotkey1Paused != globalHotkeySetting)) 31 | // } 32 | // } 33 | 34 | 35 | func testOpenSplit() { 36 | let path = String(testPath + "/Odyssey.split") 37 | let url = URL(fileURLWithPath: path) 38 | let doc = try? Document(contentsOf: url, ofType: "Split file") 39 | let dc = NSDocumentController.shared 40 | dc.addDocument(doc!) 41 | } 42 | 43 | func testOpenSplitFromExternalVolume() { 44 | if let blueS = try? Folder(path: "/Volumes/Blue Shell") { 45 | 46 | let path = "/Volumes/Blue Shell/Odyssey.split" 47 | let url = URL(fileURLWithPath: path) 48 | let doc = try? Document(contentsOf: url, ofType: "Split File") 49 | let dc = NSDocumentController.shared 50 | dc.addDocument(doc!) 51 | } 52 | // dc.addDocument(doc!) 53 | 54 | 55 | } 56 | 57 | func testImportSplitsIO() { 58 | let path = String(testPath + "/Odyssey.json") 59 | if let jsonFile = try? File(path: path), let data = try? jsonFile.read() { 60 | let sDoc = SplitsIODoc() 61 | try? sDoc.read(from: data, ofType: "Splits.io File") 62 | sDoc.makeWindowControllers() 63 | } 64 | } 65 | 66 | 67 | func testImportLiveSplit() { 68 | //TODO: New LiveSplit import test 69 | } 70 | 71 | 72 | 73 | override func tearDown() { 74 | // Put teardown code here. This method is called after the invocation of each test method in the class. 75 | } 76 | 77 | func testExample() { 78 | // This is an example of a functional test case. 79 | // Use XCTAssert and related functions to verify your tests produce the correct results. 80 | } 81 | 82 | func testPerformanceExample() { 83 | // This is an example of a performance test case. 84 | measure { 85 | // Put the code you want to measure the time of here. 86 | } 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /SplitterTests/TestAttempts.json: -------------------------------------------------------------------------------- 1 | {"segments":[{"name":"1","endedAt":{"gametimeMS":0,"realtimeMS":0},"bestDuration":{"gametimeMS":0,"realtimeMS":0}}],"schemaVersion":"v1.0.1","timer":{"website":"https:\/\/mberk.com\/splitter","shortname":"Splitter","longname":"Splitter","version":"v1.0 (55)"},"game":{"longname":"as"},"category":{"longname":""}} -------------------------------------------------------------------------------- /SplitterTests/TestPlan.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "D38F000E-4FF6-447B-8A7F-5935169DB9A5", 5 | "name" : "Configuration 1", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | 13 | }, 14 | "testTargets" : [ 15 | { 16 | "target" : { 17 | "containerPath" : "container:Splitter.xcodeproj", 18 | "identifier" : "4A71054824BE267300FA9127", 19 | "name" : "SplitterTests" 20 | } 21 | }, 22 | { 23 | "target" : { 24 | "containerPath" : "container:Splitter.xcodeproj", 25 | "identifier" : "4AF9E8BE24CE448B00DFD5BE", 26 | "name" : "SplitterUITests" 27 | } 28 | } 29 | ], 30 | "version" : 1 31 | } 32 | -------------------------------------------------------------------------------- /SplitterTests/Untitled2.json: -------------------------------------------------------------------------------- 1 | {"segments":[{"name":"s1","endedAt":{"gametimeMS":20,"realtimeMS":20},"bestDuration":{"gametimeMS":20,"realtimeMS":20}}],"_schemaVersion":"v1.0.1","timer":{"website":"https:\/\/mberk.com\/splitter","shortname":"Splitter","longname":"Splitter","version":"v1.0 (61)"},"game":{"longname":"thing"},"category":{"longname":""}} -------------------------------------------------------------------------------- /SplitterTests/Untitled3.json: -------------------------------------------------------------------------------- 1 | {"segments":[{"name":"1","endedAt":{"gametimeMS":0,"realtimeMS":0},"bestDuration":{"gametimeMS":0,"realtimeMS":0}}],"schemaVersion":"v1.0.1","timer":{"website":"https:\/\/mberk.com\/splitter","shortname":"Splitter","longname":"Splitter","version":"v1.0 (55)"},"game":{"longname":"adasdas"},"category":{"longname":""}} -------------------------------------------------------------------------------- /SplitterUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 6.1 19 | CFBundleVersion 20 | 328 21 | 22 | 23 | -------------------------------------------------------------------------------- /SplitterUITests/SplitterUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SplitterUITests.swift 3 | // SplitterUITests 4 | // 5 | // Created by Michael Berk on 3/23/20. 6 | // Copyright © 2020 Michael Berk. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Splitter 11 | 12 | class SplitterUITests: XCTestCase { 13 | 14 | var app: XCUIApplication! 15 | 16 | override func setUp() { 17 | // Put setup code here. This method is called before the invocation of each test method in the class. 18 | 19 | app = XCUIApplication() 20 | app.launch() 21 | 22 | 23 | 24 | // In UI tests it is usually best to stop immediately when a failure occurs. 25 | continueAfterFailure = false 26 | 27 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 28 | } 29 | 30 | override func tearDown() { 31 | // Put teardown code here. This method is called after the invocation of each test method in the class. 32 | } 33 | 34 | func testLaunchPerformance() { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /SplitterUITests/UITest Sample Files/LongList.split/appearance.json: -------------------------------------------------------------------------------- 1 | {"components":[{"type":0,"properties":{"isHidden":false,"afterSpacing":5,"showInfoButton":true}},{"type":1,"properties":{"isHidden":false,"hideTitleColumn":false,"hideHScroll":true,"columnWidths":["32.0","314.5","100.0","100.0"],"hideIconColumn":false,"hideVScroll":true,"showHeader":true,"afterSpacing":7}},{"type":2,"properties":{"isHidden":false,"showLabel":true,"afterSpacing":5}},{"type":3,"properties":{"showAttempts":true,"showAttemptsLabel":true,"isHidden":false,"afterSpacing":5}},{"type":4,"properties":{"isHidden":false,"afterSpacing":5}},{"type":5,"properties":{"afterSpacing":5,"isHidden":false}},{"type":6,"properties":{"isHidden":true,"afterSpacing":5}},{"type":7,"properties":{"isHidden":true,"afterSpacing":5}},{"type":8,"properties":{"afterSpacing":5,"isHidden":true}},{"type":9,"properties":{"isHidden":true,"afterSpacing":5}},{"type":10,"properties":{"afterSpacing":5,"isHidden":true}}],"keepOnTop":false,"windowHeight":656,"hideTitlebar":false,"selectColor":{"red":0.35686275362968445,"alpha":1,"blue":0.70980393886566162,"green":0.29019609093666077},"windowWidth":431,"hideTitle":false,"hideButtons":false} -------------------------------------------------------------------------------- /SplitterUITests/UITest Sample Files/LongList.split/layout.lsl: -------------------------------------------------------------------------------- 1 | {"components":[{"Title":{"background":{"Vertical":[[1.0,1.0,1.0,0.13],[1.0,1.0,1.0,0.0]]},"text_color":null,"show_game_name":true,"show_category_name":true,"show_finished_runs_count":false,"show_attempt_count":true,"text_alignment":"Auto","display_as_single_line":false,"display_game_icon":true,"show_region":false,"show_platform":false,"show_variables":true}},{"Splits":{"background":{"Alternating":[[0.2,0.2,0.2,1.0],[0.2,0.2,0.2,1.0]]},"visual_split_count":0,"split_preview_count":1,"show_thin_separators":true,"separator_last_split":true,"always_show_last_split":true,"fill_with_blank_space":true,"display_two_rows":false,"current_split_gradient":{"Vertical":[[0.2,0.4509804,0.95686275,1.0],[0.08235294,0.20784314,0.45490196,1.0]]},"split_time_accuracy":"Seconds","segment_time_accuracy":"Hundredths","delta_time_accuracy":"Tenths","delta_drop_decimals":true,"show_column_labels":true,"columns":[{"name":"Time","start_with":"ComparisonTime","update_with":"SplitTime","update_trigger":"OnEndingSegment","comparison_override":null,"timing_method":null},{"name":"+/−","start_with":"Empty","update_with":"Delta","update_trigger":"Contextual","comparison_override":null,"timing_method":null}]}},{"Timer":{"background":"Transparent","timing_method":null,"height":60,"color_override":null,"show_gradient":true,"digits_format":"SingleDigitSeconds","accuracy":"Hundredths","is_segment_timer":false}},{"PreviousSegment":{"background":{"Vertical":[[1.0,1.0,1.0,0.06],[1.0,1.0,1.0,0.005]]},"comparison_override":null,"display_two_rows":false,"label_color":null,"drop_decimals":true,"accuracy":"Tenths","show_possible_time_save":false}},{"SumOfBest":{"background":{"Vertical":[[1.0,1.0,1.0,0.06],[1.0,1.0,1.0,0.005]]},"display_two_rows":false,"label_color":null,"value_color":null,"accuracy":"Seconds"}},{"TotalPlaytime":{"background":{"Vertical":[[1.0,1.0,1.0,0.06],[1.0,1.0,1.0,0.005]]},"display_two_rows":false,"show_days":true,"label_color":null,"value_color":null}},{"SegmentTime":{"background":{"Vertical":[[1.0,1.0,1.0,0.06],[1.0,1.0,1.0,0.005]]},"comparison_override":null,"display_two_rows":false,"label_color":null,"value_color":null,"accuracy":"Hundredths"}},{"PossibleTimeSave":{"background":{"Vertical":[[1.0,1.0,1.0,0.06],[1.0,1.0,1.0,0.005]]},"comparison_override":null,"display_two_rows":false,"total_possible_time_save":false,"label_color":null,"value_color":null,"accuracy":"Hundredths"}}],"general":{"direction":"Vertical","timer_font":null,"times_font":null,"text_font":null,"background":{"Plain":[0.1,0.1,0.1,1.0]},"best_segment_color":[1.0,0.8333334,0.0,1.0],"ahead_gaining_time_color":[0.0,0.8,0.21333352,1.0],"ahead_losing_time_color":[0.38000003,0.82000005,0.49733347,1.0],"behind_gaining_time_color":[0.82000005,0.38000003,0.38000003,1.0],"behind_losing_time_color":[0.8,0.0,0.0,1.0],"not_running_color":[0.67,0.67,0.67,1.0],"personal_best_color":[0.08000004,0.64733326,1.0,1.0],"paused_color":[0.48,0.48,0.48,1.0],"thin_separators_color":[1.0,1.0,1.0,0.09],"separators_color":[1.0,1.0,1.0,0.35],"text_color":[1.0,1.0,1.0,1.0]}} -------------------------------------------------------------------------------- /SplitterUITests/UITest Sample Files/LongList.split/runInfo.json: -------------------------------------------------------------------------------- 1 | {"version":"5.0","build":"285","id":"2794AA4D-2404-42B6-87B3-6625078306E7"} -------------------------------------------------------------------------------- /SplitterUITests/UITest Sample Files/LongList.split/splits.lss: -------------------------------------------------------------------------------- 1 | Personal Best00:00:00.00000000123456789101112 -------------------------------------------------------------------------------- /appcenter-post-clone.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | CUR_COCOAPODS_VER=`sed -n -e 's/^COCOAPODS: \([0-9.]*\)/\1/p' Podfile.lock` 3 | ENV_COCOAPODS_VER=`pod --version` 4 | 5 | # check if not the same version, reinstall cocoapods version to current project's 6 | if [ $CUR_COCOAPODS_VER != $ENV_COCOAPODS_VER ]; 7 | then 8 | echo "Uninstalling all CocoaPods versions" 9 | sudo gem uninstall cocoapods --all --executables 10 | echo "Installing CocoaPods version $CUR_COCOAPODS_VER" 11 | sudo gem install cocoapods -v $CUR_COCOAPODS_VER 12 | else 13 | echo "CocoaPods version is suitable for the project" 14 | fi; -------------------------------------------------------------------------------- /appdmg.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Splitter", 3 | "contents": [ 4 | { "x": 448, "y": 344, "type": "link", "path": "/Applications" }, 5 | { "x": 192, "y": 344, "type": "file", "path": "Splitter.app" } 6 | ] 7 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Splitter", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "create-dmg": "^7.0.0" 4 | } 5 | } 6 | --------------------------------------------------------------------------------