diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 index 7cd56d0..f461f8c --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,28 @@ -## .DS_Store +# macOS .DS_Store +.AppleDouble +.LSOverride -## No Pods folder +# Xcode +xcuserdata/ +*.xcuserstate +*.xcscmblueprint +DerivedData/ +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +# Build output +build/ +*.pock + +# Swift Package Manager +.build/ +.swiftpm/ + +# CocoaPods (not used — project uses SPM) +Podfile Podfile.lock Pods/ - -## Userdata -Status.xcodeproj/xcuserdata/ -Status.xcworkspace/xcuserdata/ +*.xcworkspace diff --git a/Podfile b/Podfile deleted file mode 100644 index 71cd85e..0000000 --- a/Podfile +++ /dev/null @@ -1,16 +0,0 @@ -# -# Podfile -# Status -# -# Created by Pierluigi Galdi on 18/01/2020. -# Copyright © 2020 Pierluigi Galdi. All rights reserved. -# - -target 'Status' do - # Comment the next line if you don't want to use dynamic frameworks - use_frameworks! - - # Pods for Status - pod 'PockKit' - -end diff --git a/README.md b/README.md old mode 100644 new mode 100755 index f2bdb41..392419b --- a/README.md +++ b/README.md @@ -1,6 +1,83 @@ -# Status Widget -Always keep an eye on system information, like what time is it, or maybe WiFi status or Battery status. Configure Status items visibility from settings. +# **Status V2** — Pock Widget · Touch Bar GIF + Status Bar -# Preview - +![Touch Bar Preview](images/TouchBar-Preview-PockStatusV2Widget.gif) +> ### **GIF Supporting** +> Display an animated GIF directly on your MacBook Touch Bar — alongside clock, WiFi, battery, and input source. + +A [Pock](https://pock.app) widget that lets you **show any animated GIF on your Touch Bar**. Load it from a local file or a URL, control the size and scaling, and preview it live — all from the widget preferences. Perfect if you want to add a custom animation, a meme, a logo, or any GIF to your Touch Bar. + +--- + +## Features + +- **Animated GIF on Touch Bar** — display any `.gif` from a file or URL on the right edge of your Touch Bar +- **Width control** — resize the GIF from 30 to 300 pt +- **Scaling modes** — Fit, Fill (crop), or Stretch +- **Live preview** — see your GIF at real Touch Bar height before applying +- **Clock** — customizable date & time format +- **WiFi** — signal strength indicator +- **Battery** — icon and/or percentage +- **Input Source** — current keyboard language + +--- + +## Requirements + +- MacBook with Touch Bar +- macOS 10.15 or later +- [Pock](https://pock.app) installed + +--- + +## Installation + +1. Download the latest `.pock` file from [Releases](../../releases/tag/v1.0.0) +2. Double-click it — Pock will install it automatically +3. Open Pock → Widgets Manager and enable **Status V2** +4. Open preferences → enable **Show GIF** → paste a URL or choose a file + +--- + +## Building from Source + +No CocoaPods required. The project uses **Swift Package Manager**. + +1. Clone the repo +2. Open `Status.xcodeproj` in Xcode +3. Xcode will automatically resolve the SPM dependencies ([PockKit](https://github.com/pock/pockkit), [TinyConstraints](https://github.com/roberthein/TinyConstraints)) +4. Build (`⌘B`) — the `.pock` bundle appears in the Products group +5. Double-click the built `.pock` to install + +--- + +## Preferences + +![Settings](images/Settings.png) + +Open **Pock → Widgets Manager → Status V2** to configure: + +| Setting | Description | +|---|---| +| Language / Input Source | Toggle the input source indicator | +| WiFi Signal | Toggle the WiFi signal icon | +| Battery | Toggle battery display; choose icon and/or percentage | +| Date & Time | Toggle the clock; set a custom date/time format | +| **Show GIF** | Enable animated GIF on the right edge of the Touch Bar | +| Source | Load GIF from a local **file** or a **URL** | +| Width | Set the GIF display width (30–300 pt) | +| Scaling | Fit / Fill (crop) / Stretch | +| Preview | Live preview of your GIF at Touch Bar height | + +--- + +## Keywords + +`touch bar gif` · `touchbar gif` · `add gif to touch bar` · `pock widget gif` · `macbook touch bar animation` · `custom touch bar` · `touch bar customization` · `pock gif widget` · `animated touch bar` · `touch bar status widget` + +--- + +## Credits + +Based on the original [status-widget](https://github.com/pock/status-widget) by [Pierluigi Galdi](https://github.com/pigigaldi). +GIF integration and Status V2 by [EvyatarEshkar](https://github.com/EvyatarEshkar). diff --git a/Status.xcodeproj/project.pbxproj b/Status.xcodeproj/project.pbxproj old mode 100644 new mode 100755 index 43772c3..481860d --- a/Status.xcodeproj/project.pbxproj +++ b/Status.xcodeproj/project.pbxproj @@ -18,12 +18,13 @@ 3401774C23D325DD00369905 /* StatusWidgetPreferencePane.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3401774923D325DD00369905 /* StatusWidgetPreferencePane.swift */; }; 34E6A27E2558706B002693EA /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 34E6A27D2558706B002693EA /* Media.xcassets */; }; 34E6A281255874FA002693EA /* SLangItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E6A280255874FA002693EA /* SLangItem.swift */; }; - C1D0E7696EB2EC07C7557D36 /* Pods_Status.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3712FB53353E8D66C89087DD /* Pods_Status.framework */; }; + AA000005000000050000005A /* SGifItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000006000000060000006A /* SGifItem.swift */; }; + AA000003000000030000003C /* PockKit in Frameworks */ = {isa = PBXBuildFile; productRef = AA000001000000010000001A /* PockKit */; }; + AA000004000000040000004C /* TinyConstraints in Frameworks */ = {isa = PBXBuildFile; productRef = AA000002000000020000002A /* TinyConstraints */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 3401772723D3231D00369905 /* Status.pock */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Status.pock; sourceTree = BUILT_PRODUCTS_DIR; }; - 3401772A23D3231D00369905 /* Podfile */ = {isa = PBXFileReference; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = ""; }; 3401772C23D3231D00369905 /* StatusWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusWidget.swift; sourceTree = ""; }; 3401772E23D3231D00369905 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3401773423D323CD00369905 /* StatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusItem.swift; sourceTree = ""; }; @@ -40,9 +41,7 @@ 3401774923D325DD00369905 /* StatusWidgetPreferencePane.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusWidgetPreferencePane.swift; sourceTree = ""; }; 34E6A27D2558706B002693EA /* Media.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Media.xcassets; sourceTree = ""; }; 34E6A280255874FA002693EA /* SLangItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SLangItem.swift; sourceTree = ""; }; - 3712FB53353E8D66C89087DD /* Pods_Status.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Status.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 9347816283C4AB2AF67D68D4 /* Pods-Status.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Status.release.xcconfig"; path = "Target Support Files/Pods-Status/Pods-Status.release.xcconfig"; sourceTree = ""; }; - CB926D07BCC33E6C4950E6CD /* Pods-Status.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Status.debug.xcconfig"; path = "Target Support Files/Pods-Status/Pods-Status.debug.xcconfig"; sourceTree = ""; }; + AA000006000000060000006A /* SGifItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SGifItem.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -50,7 +49,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - C1D0E7696EB2EC07C7557D36 /* Pods_Status.framework in Frameworks */, + AA000003000000030000003C /* PockKit in Frameworks */, + AA000004000000040000004C /* TinyConstraints in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -62,8 +62,6 @@ children = ( 3401772923D3231D00369905 /* Status */, 3401772823D3231D00369905 /* Products */, - 43D9A762481AC806E3D29B23 /* Pods */, - E407CFE0DC443983050B23C8 /* Frameworks */, ); sourceTree = ""; }; @@ -78,7 +76,6 @@ 3401772923D3231D00369905 /* Status */ = { isa = PBXGroup; children = ( - 3401772A23D3231D00369905 /* Podfile */, 3401772E23D3231D00369905 /* Info.plist */, 3401772B23D3231D00369905 /* Sources */, ); @@ -101,6 +98,7 @@ isa = PBXGroup; children = ( 3401773623D3241000369905 /* SClockItem.swift */, + AA000006000000060000006A /* SGifItem.swift */, 34E6A280255874FA002693EA /* SLangItem.swift */, 3401773723D3241000369905 /* SPowerItem.swift */, 3401773823D3241000369905 /* SWifiItem.swift */, @@ -127,23 +125,6 @@ path = StatusWidgetPreferencePane; sourceTree = ""; }; - 43D9A762481AC806E3D29B23 /* Pods */ = { - isa = PBXGroup; - children = ( - CB926D07BCC33E6C4950E6CD /* Pods-Status.debug.xcconfig */, - 9347816283C4AB2AF67D68D4 /* Pods-Status.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; - E407CFE0DC443983050B23C8 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 3712FB53353E8D66C89087DD /* Pods_Status.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -151,7 +132,6 @@ isa = PBXNativeTarget; buildConfigurationList = 3401773123D3231D00369905 /* Build configuration list for PBXNativeTarget "Status" */; buildPhases = ( - 1BC8629D672E307759B16C56 /* [CP] Check Pods Manifest.lock */, 3401772223D3231D00369905 /* Sources */, 3401772323D3231D00369905 /* Frameworks */, 3401772423D3231D00369905 /* Resources */, @@ -162,6 +142,10 @@ dependencies = ( ); name = Status; + packageProductDependencies = ( + AA000001000000010000001A /* PockKit */, + AA000002000000020000002A /* TinyConstraints */, + ); productName = Status; productReference = 3401772723D3231D00369905 /* Status.pock */; productType = "com.apple.product-type.bundle"; @@ -194,6 +178,10 @@ tr, ); mainGroup = 3401771D23D3231D00369905; + packageReferences = ( + BB000001000000010000001B /* XCRemoteSwiftPackageReference "pockkit" */, + BB000002000000020000002B /* XCRemoteSwiftPackageReference "TinyConstraints" */, + ); productRefGroup = 3401772823D3231D00369905 /* Products */; projectDirPath = ""; projectRoot = ""; @@ -208,37 +196,13 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 3401774B23D325DD00369905 /* StatusWidgetPreferencePane.xib in Resources */, 34E6A27E2558706B002693EA /* Media.xcassets in Resources */, - 3401774A23D325DD00369905 /* StatusWidgetPreferencePane.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1BC8629D672E307759B16C56 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Status-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; 3401772523D3231D00369905 /* Copy widget to destination folder */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -264,6 +228,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + AA000005000000050000005A /* SGifItem.swift in Sources */, 3401773923D3241000369905 /* SClockItem.swift in Sources */, 3401773523D323CD00369905 /* StatusItem.swift in Sources */, 3401774C23D325DD00369905 /* StatusWidgetPreferencePane.swift in Sources */, @@ -351,7 +316,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -403,7 +368,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.13; + MACOSX_DEPLOYMENT_TARGET = 10.15; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -412,19 +377,16 @@ }; 3401773223D3231D00369905 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = CB926D07BCC33E6C4950E6CD /* Pods-Status.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_IDENTITY = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 788D9WZ9Z3; INFOPLIST_FILE = Status/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Application\\ Support/Pock/Widgets"; MARKETING_VERSION = 1.1; - PRODUCT_BUNDLE_IDENTIFIER = com.pigigaldi.pock.Status; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.evyatareshkar.pock.StatusV2; + PRODUCT_MODULE_NAME = Status; + PRODUCT_NAME = StatusV2; SWIFT_VERSION = 5.0; WRAPPER_EXTENSION = pock; }; @@ -432,19 +394,16 @@ }; 3401773323D3231D00369905 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9347816283C4AB2AF67D68D4 /* Pods-Status.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; - CODE_SIGN_IDENTITY = "Developer ID Application"; - CODE_SIGN_STYLE = Manual; + CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 788D9WZ9Z3; INFOPLIST_FILE = Status/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Application\\ Support/Pock/Widgets"; MARKETING_VERSION = 1.1; - PRODUCT_BUNDLE_IDENTIFIER = com.pigigaldi.pock.Status; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; + PRODUCT_BUNDLE_IDENTIFIER = com.evyatareshkar.pock.StatusV2; + PRODUCT_MODULE_NAME = Status; + PRODUCT_NAME = StatusV2; SWIFT_VERSION = 5.0; WRAPPER_EXTENSION = pock; }; @@ -472,6 +431,38 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + BB000001000000010000001B /* XCRemoteSwiftPackageReference "pockkit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pock/pockkit.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.3.1; + }; + }; + BB000002000000020000002B /* XCRemoteSwiftPackageReference "TinyConstraints" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/roberthein/TinyConstraints.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 4.0.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + AA000001000000010000001A /* PockKit */ = { + isa = XCSwiftPackageProductDependency; + package = BB000001000000010000001B /* XCRemoteSwiftPackageReference "pockkit" */; + productName = PockKit; + }; + AA000002000000020000002A /* TinyConstraints */ = { + isa = XCSwiftPackageProductDependency; + package = BB000002000000020000002B /* XCRemoteSwiftPackageReference "TinyConstraints" */; + productName = TinyConstraints; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 3401771E23D3231D00369905 /* Project object */; } diff --git a/Status.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Status.xcodeproj/project.xcworkspace/contents.xcworkspacedata old mode 100644 new mode 100755 diff --git a/Status.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Status.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist old mode 100644 new mode 100755 diff --git a/Status.xcodeproj/xcuserdata/Pierluigi.xcuserdatad/xcschemes/xcschememanagement.plist b/Status.xcodeproj/xcuserdata/Pierluigi.xcuserdatad/xcschemes/xcschememanagement.plist deleted file mode 100644 index 02d34ce..0000000 --- a/Status.xcodeproj/xcuserdata/Pierluigi.xcuserdatad/xcschemes/xcschememanagement.plist +++ /dev/null @@ -1,14 +0,0 @@ - - - - - SchemeUserState - - Status.xcscheme_^#shared#^_ - - orderHint - 3 - - - - diff --git a/Status.xcworkspace/contents.xcworkspacedata b/Status.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index dd7380a..0000000 --- a/Status.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/Status.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Status.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/Status.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Status/Info.plist b/Status/Info.plist old mode 100644 new mode 100755 index 36479d2..113fc7f --- a/Status/Info.plist +++ b/Status/Info.plist @@ -11,7 +11,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - $(PRODUCT_NAME) + Status V2 CFBundlePackageType BNDL CFBundleShortVersionString @@ -21,10 +21,10 @@ NSHumanReadableCopyright Copyright © 2020 Pierluigi Galdi. All rights reserved. NSPrincipalClass - Status.StatusWidget + Status.StatusV2Widget PKWidgetAuthor - Pierluigi Galdi + EvyatarEshkar PKWidgetPreferenceClass - Status.StatusWidgetPreferencePane + Status.StatusV2PreferencePane diff --git a/Status/Sources/Items/SClockItem.swift b/Status/Sources/Items/SClockItem.swift old mode 100644 new mode 100755 diff --git a/Status/Sources/Items/SGifItem.swift b/Status/Sources/Items/SGifItem.swift new file mode 100644 index 0000000..11b0bea --- /dev/null +++ b/Status/Sources/Items/SGifItem.swift @@ -0,0 +1,113 @@ +// +// SGifItem.swift +// Status +// + +import Foundation +import AppKit + +internal class SGifItem: StatusItem { + + // MARK: - StatusItem + + var enabled: Bool { return Preferences[.shouldShowGif] } + var title: String { return "gif" } + var view: NSView { return imageView } + + func action() { } + + func didLoad() { + applyDimensions() + applyScaling() + reload() + } + + func didUnload() { + imageView.image = nil + } + + // MARK: - UI + + private let imageView: NSImageView = { + let iv = NSImageView() + iv.translatesAutoresizingMaskIntoConstraints = false + iv.animates = true + iv.wantsLayer = true + iv.layer?.cornerRadius = 4 + iv.layer?.masksToBounds = true + return iv + }() + + private var widthConstraint: NSLayoutConstraint? + private var heightConstraint: NSLayoutConstraint? + + init() { + widthConstraint = imageView.widthAnchor.constraint(equalToConstant: 60) + heightConstraint = imageView.heightAnchor.constraint(equalToConstant: 30) + widthConstraint?.isActive = true + heightConstraint?.isActive = true + didLoad() + } + + deinit { didUnload() } + + // MARK: - Reload + + func reload() { + applyDimensions() + applyScaling() + + let sourceType: Int = Preferences[.gifSourceType] + if sourceType == 0 { + let path: String = Preferences[.gifFilePath] + guard !path.isEmpty else { return } + loadFromFile(path) + } else { + let urlStr: String = Preferences[.gifURLString] + guard !urlStr.isEmpty, let url = URL(string: urlStr) else { return } + loadFromURL(url) + } + } + + // MARK: - Helpers + + private func applyDimensions() { + let w: Double = Preferences[.gifWidth] + widthConstraint?.constant = CGFloat(w) + } + + private func applyScaling() { + let mode: Int = Preferences[.gifScalingMode] + switch mode { + case 1: + imageView.imageScaling = .scaleAxesIndependently + imageView.layer?.contentsGravity = .resizeAspectFill + case 2: + imageView.imageScaling = .scaleAxesIndependently + imageView.layer?.contentsGravity = .resize + default: + imageView.imageScaling = .scaleProportionallyUpOrDown + imageView.layer?.contentsGravity = .resizeAspect + } + } + + private func loadFromFile(_ path: String) { + guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let image = NSImage(data: data) else { return } + DispatchQueue.main.async { [weak self] in + self?.imageView.image = image + self?.imageView.animates = true + } + } + + private func loadFromURL(_ url: URL) { + let req = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 30) + URLSession.shared.dataTask(with: req) { [weak self] data, _, _ in + guard let data, let image = NSImage(data: data) else { return } + DispatchQueue.main.async { + self?.imageView.image = image + self?.imageView.animates = true + } + }.resume() + } +} diff --git a/Status/Sources/Items/SLangItem.swift b/Status/Sources/Items/SLangItem.swift old mode 100644 new mode 100755 diff --git a/Status/Sources/Items/SPowerItem.swift b/Status/Sources/Items/SPowerItem.swift old mode 100644 new mode 100755 index e6f8b50..b37b9aa --- a/Status/Sources/Items/SPowerItem.swift +++ b/Status/Sources/Items/SPowerItem.swift @@ -112,7 +112,7 @@ internal class SPowerItem: StatusItem { iconName = "powerEmpty" buildBatteryIcon(withValue: value) } - iconView.image = Bundle(for: StatusWidget.self).image(forResource: iconName) + iconView.image = Bundle(for: StatusV2Widget.self).image(forResource: iconName) iconView.isHidden = false }else { iconView.isHidden = true diff --git a/Status/Sources/Items/SWifiItem.swift b/Status/Sources/Items/SWifiItem.swift old mode 100644 new mode 100755 index bc58af6..dd9aeb7 --- a/Status/Sources/Items/SWifiItem.swift +++ b/Status/Sources/Items/SWifiItem.swift @@ -64,7 +64,7 @@ internal class SWifiItem: StatusItem { let c = code - 1 icon = "wifi\(c > 4 ? 4 : c)" } - self.iconView.image = Bundle(for: StatusWidget.self).image(forResource: icon) + self.iconView.image = Bundle(for: StatusV2Widget.self).image(forResource: icon) } } diff --git a/Status/Sources/Media.xcassets/Contents.json b/Status/Sources/Media.xcassets/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerEmpty.imageset/BatteryEmpty.pdf b/Status/Sources/Media.xcassets/powerEmpty.imageset/BatteryEmpty.pdf old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerEmpty.imageset/Contents.json b/Status/Sources/Media.xcassets/powerEmpty.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerIsCharged.imageset/BatteryCharged.pdf b/Status/Sources/Media.xcassets/powerIsCharged.imageset/BatteryCharged.pdf old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerIsCharged.imageset/Contents.json b/Status/Sources/Media.xcassets/powerIsCharged.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerIsCharging.imageset/BatteryCharging-1.pdf b/Status/Sources/Media.xcassets/powerIsCharging.imageset/BatteryCharging-1.pdf old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerIsCharging.imageset/Contents.json b/Status/Sources/Media.xcassets/powerIsCharging.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerLeft.imageset/BatteryLevelCapB-L.pdf b/Status/Sources/Media.xcassets/powerLeft.imageset/BatteryLevelCapB-L.pdf old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerLeft.imageset/Contents.json b/Status/Sources/Media.xcassets/powerLeft.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerMiddle.imageset/BatteryLevelCapB-M.pdf b/Status/Sources/Media.xcassets/powerMiddle.imageset/BatteryLevelCapB-M.pdf old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerMiddle.imageset/Contents.json b/Status/Sources/Media.xcassets/powerMiddle.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerRight.imageset/BatteryLevelCapB-R.pdf b/Status/Sources/Media.xcassets/powerRight.imageset/BatteryLevelCapB-R.pdf old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/powerRight.imageset/Contents.json b/Status/Sources/Media.xcassets/powerRight.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi0.imageset/AirPort0.png b/Status/Sources/Media.xcassets/wifi0.imageset/AirPort0.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi0.imageset/AirPort0@2x.png b/Status/Sources/Media.xcassets/wifi0.imageset/AirPort0@2x.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi0.imageset/Contents.json b/Status/Sources/Media.xcassets/wifi0.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi1.imageset/AirPort1.png b/Status/Sources/Media.xcassets/wifi1.imageset/AirPort1.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi1.imageset/AirPort1@2x.png b/Status/Sources/Media.xcassets/wifi1.imageset/AirPort1@2x.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi1.imageset/Contents.json b/Status/Sources/Media.xcassets/wifi1.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi2.imageset/AirPort2.png b/Status/Sources/Media.xcassets/wifi2.imageset/AirPort2.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi2.imageset/AirPort2@2x.png b/Status/Sources/Media.xcassets/wifi2.imageset/AirPort2@2x.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi2.imageset/Contents.json b/Status/Sources/Media.xcassets/wifi2.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi3.imageset/AirPort3.png b/Status/Sources/Media.xcassets/wifi3.imageset/AirPort3.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi3.imageset/AirPort3@2x.png b/Status/Sources/Media.xcassets/wifi3.imageset/AirPort3@2x.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi3.imageset/Contents.json b/Status/Sources/Media.xcassets/wifi3.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi4.imageset/AirPort4.png b/Status/Sources/Media.xcassets/wifi4.imageset/AirPort4.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi4.imageset/AirPort4@2x.png b/Status/Sources/Media.xcassets/wifi4.imageset/AirPort4@2x.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifi4.imageset/Contents.json b/Status/Sources/Media.xcassets/wifi4.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifiOff.imageset/AirPortOff.png b/Status/Sources/Media.xcassets/wifiOff.imageset/AirPortOff.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifiOff.imageset/AirPortOff@2x.png b/Status/Sources/Media.xcassets/wifiOff.imageset/AirPortOff@2x.png old mode 100644 new mode 100755 diff --git a/Status/Sources/Media.xcassets/wifiOff.imageset/Contents.json b/Status/Sources/Media.xcassets/wifiOff.imageset/Contents.json old mode 100644 new mode 100755 diff --git a/Status/Sources/Preferences/Preferences.swift b/Status/Sources/Preferences/Preferences.swift old mode 100644 new mode 100755 index 879b622..686c6e0 --- a/Status/Sources/Preferences/Preferences.swift +++ b/Status/Sources/Preferences/Preferences.swift @@ -14,6 +14,7 @@ extension NSNotification.Name { internal struct Preferences { internal enum Keys: String { + // Status items case shouldShowLangItem case shouldShowWifiItem case shouldShowPowerItem @@ -21,25 +22,32 @@ internal struct Preferences { case shouldShowBatteryPercentage case shouldShowDateItem case timeFormatTextField + // GIF + case shouldShowGif + case gifSourceType // Int — 0 = file, 1 = url + case gifFilePath // String + case gifURLString // String + case gifWidth // Double + case gifScalingMode // Int — 0 = fit, 1 = fill, 2 = stretch } + static subscript(_ key: Keys) -> T { get { guard let value = UserDefaults.standard.value(forKey: key.rawValue) as? T else { switch key { - case .shouldShowLangItem: - return false as! T - case .shouldShowWifiItem: - return true as! T - case .shouldShowPowerItem: - return true as! T - case .shouldShowBatteryIcon: - return true as! T - case .shouldShowBatteryPercentage: - return false as! T - case .shouldShowDateItem: - return true as! T - case .timeFormatTextField: - return "EE dd MMM HH:mm" as! T + case .shouldShowLangItem: return false as! T + case .shouldShowWifiItem: return true as! T + case .shouldShowPowerItem: return true as! T + case .shouldShowBatteryIcon: return true as! T + case .shouldShowBatteryPercentage: return false as! T + case .shouldShowDateItem: return true as! T + case .timeFormatTextField: return "EE dd MMM HH:mm" as! T + case .shouldShowGif: return false as! T + case .gifSourceType: return 1 as! T // URL + case .gifFilePath: return "" as! T + case .gifURLString: return "" as! T + case .gifWidth: return 60.0 as! T + case .gifScalingMode: return 0 as! T // fit } } return value @@ -48,13 +56,20 @@ internal struct Preferences { UserDefaults.standard.setValue(newValue, forKey: key.rawValue) } } + static func reset() { - Preferences[.shouldShowLangItem] = false - Preferences[.shouldShowWifiItem] = true - Preferences[.shouldShowPowerItem] = true - Preferences[.shouldShowBatteryIcon] = true - Preferences[.shouldShowBatteryPercentage] = true - Preferences[.shouldShowDateItem] = true - Preferences[.timeFormatTextField] = "EE dd MMM HH:mm" + Preferences[.shouldShowLangItem] = false + Preferences[.shouldShowWifiItem] = true + Preferences[.shouldShowPowerItem] = true + Preferences[.shouldShowBatteryIcon] = true + Preferences[.shouldShowBatteryPercentage] = false + Preferences[.shouldShowDateItem] = true + Preferences[.timeFormatTextField] = "EE dd MMM HH:mm" + Preferences[.shouldShowGif] = false + Preferences[.gifSourceType] = 1 + Preferences[.gifFilePath] = "" + Preferences[.gifURLString] = "" + Preferences[.gifWidth] = 60.0 + Preferences[.gifScalingMode] = 0 } } diff --git a/Status/Sources/Preferences/StatusWidgetPreferencePane/Base.lproj/StatusWidgetPreferencePane.xib b/Status/Sources/Preferences/StatusWidgetPreferencePane/Base.lproj/StatusWidgetPreferencePane.xib deleted file mode 100644 index 5c98a2f..0000000 --- a/Status/Sources/Preferences/StatusWidgetPreferencePane/Base.lproj/StatusWidgetPreferencePane.xib +++ /dev/null @@ -1,172 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Status/Sources/Preferences/StatusWidgetPreferencePane/StatusWidgetPreferencePane.swift b/Status/Sources/Preferences/StatusWidgetPreferencePane/StatusWidgetPreferencePane.swift old mode 100644 new mode 100755 index 40e1724..131196d --- a/Status/Sources/Preferences/StatusWidgetPreferencePane/StatusWidgetPreferencePane.swift +++ b/Status/Sources/Preferences/StatusWidgetPreferencePane/StatusWidgetPreferencePane.swift @@ -1,79 +1,411 @@ // // StatusWidgetPreferencePane.swift -// Pock +// Status // -// Created by Pierluigi Galdi on 30/03/2019. -// Copyright © 2019 Pierluigi Galdi. All rights reserved. +// Programmatic rewrite — drops XIB, adds GIF settings section. // import Cocoa import PockKit +import UniformTypeIdentifiers + +// Named differently from any XIB file to prevent nib auto-loading +class StatusV2PreferencePane: NSViewController, NSTextFieldDelegate, PKWidgetPreference { + + static var nibName: NSNib.Name = NSNib.Name("") + + // MARK: Status controls + private weak var showLangCheck: NSButton! + private weak var showWifiCheck: NSButton! + private weak var showPowerCheck: NSButton! + private weak var showBatteryIconCheck: NSButton! + private weak var showBatteryPctCheck: NSButton! + private weak var showDateCheck: NSButton! + private weak var timeFormatField: NSTextField! + + // MARK: GIF controls + private weak var showGifCheck: NSButton! + private weak var gifContentStack: NSView! + private weak var gifSourceControl: NSSegmentedControl! + private weak var gifURLRow: NSView! + private weak var gifURLField: NSTextField! + private weak var gifFileRow: NSView! + private weak var gifFileField: NSTextField! + private weak var gifWidthSlider: NSSlider! + private weak var gifWidthLabel: NSTextField! + private weak var gifScalePopup: NSPopUpButton! + private weak var gifPreviewImageView: NSImageView! + private weak var gifPreviewWidthConstraint: NSLayoutConstraint! + + // MARK: - Init + + override init(nibName: NSNib.Name?, bundle: Bundle?) { + super.init(nibName: nil, bundle: nil) + } + required init?(coder: NSCoder) { super.init(coder: coder) } + + override func loadView() { + self.view = buildUI() + } -class StatusWidgetPreferencePane: NSViewController, NSTextFieldDelegate, PKWidgetPreference { - - static var nibName: NSNib.Name = "StatusWidgetPreferencePane" - - /// UI - @IBOutlet weak var showLangItem: NSButton! - @IBOutlet weak var showWifiItem: NSButton! - @IBOutlet weak var showPowerItem: NSButton! - @IBOutlet weak var showBatteryIconItem: NSButton! - @IBOutlet weak var showBatteryPercentageItem: NSButton! - @IBOutlet weak var showDateItem: NSButton! - @IBOutlet weak var timeFormatTextField: NSTextField! - func reset() { Preferences.reset() - loadCheckboxState() NotificationCenter.default.post(name: .shouldReloadStatusWidget, object: nil) + self.view = buildUI() } - - override func viewDidLoad() { - super.viewDidLoad() - self.view.superview?.wantsLayer = true - self.view.wantsLayer = true - self.loadCheckboxState() - self.timeFormatTextField.delegate = self - self.timeFormatTextField.stringValue = Preferences[.timeFormatTextField] - } - - private func loadCheckboxState() { - self.showLangItem.state = Preferences[.shouldShowLangItem] ? .on : .off - self.showWifiItem.state = Preferences[.shouldShowWifiItem] ? .on : .off - self.showPowerItem.state = Preferences[.shouldShowPowerItem] ? .on : .off - self.showBatteryIconItem.state = Preferences[.shouldShowBatteryIcon] ? .on : .off - self.showBatteryPercentageItem.state = Preferences[.shouldShowBatteryPercentage] ? .on : .off - self.showDateItem.state = Preferences[.shouldShowDateItem] ? .on : .off - } - - @IBAction func didChangeCheckboxValue(_ checkbox: NSButton) { - var key: Preferences.Keys - switch checkbox.tag { - case 0: - key = .shouldShowLangItem - case 1: - key = .shouldShowWifiItem - case 2: - key = .shouldShowPowerItem - case 21: - key = .shouldShowBatteryIcon - case 22: - key = .shouldShowBatteryPercentage - case 3: - key = .shouldShowDateItem - default: - return + + // MARK: - Build UI + + private func buildUI() -> NSView { + let root = NSStackView() + root.orientation = .vertical + root.alignment = .leading + root.spacing = 5 + root.edgeInsets = NSEdgeInsets(top: 14, left: 16, bottom: 14, right: 16) + root.translatesAutoresizingMaskIntoConstraints = false + root.widthAnchor.constraint(greaterThanOrEqualToConstant: 340).isActive = true + + // ── STATUS ITEMS ───────────────────────────────────────────────────── + root.addArrangedSubview(sectionLabel("Status Items")) + + let langCk = makeCheckbox("Language / Input Source", tag: 0, + on: Preferences[.shouldShowLangItem]) + showLangCheck = langCk + root.addArrangedSubview(langCk) + + let wifiCk = makeCheckbox("WiFi Signal", tag: 1, + on: Preferences[.shouldShowWifiItem]) + showWifiCheck = wifiCk + root.addArrangedSubview(wifiCk) + + let powerCk = makeCheckbox("Battery", tag: 2, + on: Preferences[.shouldShowPowerItem]) + showPowerCheck = powerCk + root.addArrangedSubview(powerCk) + + // Battery sub-options (indented) + let battSub = NSStackView() + battSub.orientation = .vertical + battSub.alignment = .leading + battSub.spacing = 3 + battSub.edgeInsets.left = 18 + let iconCk = makeCheckbox("Show icon", tag: 21, + on: Preferences[.shouldShowBatteryIcon]) + showBatteryIconCheck = iconCk + let pctCk = makeCheckbox("Show percentage", tag: 22, + on: Preferences[.shouldShowBatteryPercentage]) + showBatteryPctCheck = pctCk + battSub.addArrangedSubview(iconCk) + battSub.addArrangedSubview(pctCk) + root.addArrangedSubview(battSub) + + let dateCk = makeCheckbox("Date & Time", tag: 3, + on: Preferences[.shouldShowDateItem]) + showDateCheck = dateCk + root.addArrangedSubview(dateCk) + + // Time format + let tf = NSTextField() + tf.stringValue = Preferences[.timeFormatTextField] + tf.placeholderString = "EE dd MMM HH:mm" + tf.delegate = self + timeFormatField = tf + + let helpBtn = NSButton(title: "", target: self, action: #selector(openTimeFormatHelp)) + helpBtn.bezelStyle = .helpButton + + let tfInner = NSStackView(views: [tf, helpBtn]) + tfInner.spacing = 6 + let tfRow = makeRow("Format:", tfInner) + tfRow.edgeInsets.left = 18 + root.addArrangedSubview(tfRow) + + // ── SEPARATOR ──────────────────────────────────────────────────────── + root.addArrangedSubview(makeSeparator()) + + // ── GIF ────────────────────────────────────────────────────────────── + root.addArrangedSubview(sectionLabel("GIF")) + + let shouldShowGif: Bool = Preferences[.shouldShowGif] + let gifToggle = NSButton(checkboxWithTitle: "Show GIF on right edge", + target: self, action: #selector(gifToggleChanged)) + gifToggle.state = shouldShowGif ? .on : .off + showGifCheck = gifToggle + root.addArrangedSubview(gifToggle) + + // GIF content stack — shown only when toggle is on + let gifStack = NSStackView() + gifStack.orientation = .vertical + gifStack.alignment = .leading + gifStack.spacing = 5 + gifStack.edgeInsets.left = 8 + gifStack.isHidden = !shouldShowGif + gifContentStack = gifStack + + // Source selector: File | URL + let sourceCtrl = NSSegmentedControl( + labels: ["File", "URL"], + trackingMode: .selectOne, + target: self, + action: #selector(gifSourceChanged) + ) + let savedSource: Int = Preferences[.gifSourceType] + sourceCtrl.selectedSegment = savedSource + gifSourceControl = sourceCtrl + gifStack.addArrangedSubview(makeRow("Source:", sourceCtrl)) + + // URL row + let urlF = NSTextField() + let savedURLStr: String = Preferences[.gifURLString] + urlF.stringValue = savedURLStr + urlF.placeholderString = "https://example.com/animation.gif" + urlF.target = self + urlF.action = #selector(gifURLChanged) + gifURLField = urlF + let urlRow = makeRow("URL:", urlF) + gifURLRow = urlRow + gifStack.addArrangedSubview(urlRow) + + // File row + let fileF = NSTextField() + let savedPath: String = Preferences[.gifFilePath] + fileF.stringValue = savedPath + fileF.placeholderString = "No file selected" + fileF.isEditable = false + fileF.isSelectable = false + gifFileField = fileF + + let chooseBtn = NSButton(title: "Choose…", target: self, action: #selector(gifChooseFile)) + let fileInner = NSStackView(views: [fileF, chooseBtn]) + fileInner.spacing = 6 + let fileRow = makeRow("File:", fileInner) + gifFileRow = fileRow + gifStack.addArrangedSubview(fileRow) + + // Width slider + let savedW: Double = Preferences[.gifWidth] + let wSlider = NSSlider(value: savedW, minValue: 30, maxValue: 300, + target: self, action: #selector(gifWidthChanged)) + wSlider.controlSize = .small + gifWidthSlider = wSlider + let wLabel = NSTextField(labelWithString: "\(Int(savedW)) pt") + wLabel.font = NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .regular) + gifWidthLabel = wLabel + let wInner = NSStackView(views: [wSlider, wLabel]) + wInner.spacing = 6 + gifStack.addArrangedSubview(makeRow("Width:", wInner)) + + // Scaling popup + let scaleP = NSPopUpButton() + scaleP.addItems(withTitles: ["Fit", "Fill (crop)", "Stretch"]) + let savedScale: Int = Preferences[.gifScalingMode] + scaleP.selectItem(at: savedScale) + scaleP.target = self + scaleP.action = #selector(gifScalingChanged) + gifScalePopup = scaleP + gifStack.addArrangedSubview(makeRow("Scaling:", scaleP)) + + // Preview (Touch Bar height) + let piv = NSImageView() + piv.wantsLayer = true + piv.layer?.cornerRadius = 4 + piv.layer?.masksToBounds = true + piv.imageScaling = .scaleProportionallyUpOrDown + piv.animates = true + piv.translatesAutoresizingMaskIntoConstraints = false + gifPreviewImageView = piv + + let previewBox = NSView() + previewBox.translatesAutoresizingMaskIntoConstraints = false + previewBox.wantsLayer = true + previewBox.layer?.backgroundColor = NSColor.controlBackgroundColor.cgColor + previewBox.layer?.cornerRadius = 4 + previewBox.addSubview(piv) + + let pwc = previewBox.widthAnchor.constraint(equalToConstant: CGFloat(savedW)) + gifPreviewWidthConstraint = pwc + NSLayoutConstraint.activate([ + piv.leadingAnchor.constraint(equalTo: previewBox.leadingAnchor), + piv.trailingAnchor.constraint(equalTo: previewBox.trailingAnchor), + piv.topAnchor.constraint(equalTo: previewBox.topAnchor), + piv.bottomAnchor.constraint(equalTo: previewBox.bottomAnchor), + previewBox.heightAnchor.constraint(equalToConstant: 30), + pwc + ]) + gifStack.addArrangedSubview(makeRow("Preview:", previewBox)) + + root.addArrangedSubview(gifStack) + + // Apply initial source row visibility and preview + refreshGIFSourceRows() + refreshGIFPreview() + + return root + } + + // MARK: - Status Actions + + @objc private func checkboxChanged(_ sender: NSButton) { + let key: Preferences.Keys + switch sender.tag { + case 0: key = .shouldShowLangItem + case 1: key = .shouldShowWifiItem + case 2: key = .shouldShowPowerItem + case 21: key = .shouldShowBatteryIcon + case 22: key = .shouldShowBatteryPercentage + case 3: key = .shouldShowDateItem + default: return } - Preferences[key] = checkbox.state == .on - NotificationCenter.default.post(name: .shouldReloadStatusWidget, object: nil) + Preferences[key] = sender.state == .on + NotificationCenter.default.post(name: .shouldReloadStatusWidget, object: nil) } - - @IBAction func openTimeFormatHelpURL(_ sender: NSButton) { + + @objc private func openTimeFormatHelp() { guard let url = URL(string: "https://www.mowglii.com/itsycal/datetime.html") else { return } NSWorkspace.shared.open(url) } - + + // Fires on every keystroke — SClockItem's 1-second timer picks it up automatically func controlTextDidChange(_ obj: Notification) { - Preferences[.timeFormatTextField] = timeFormatTextField.stringValue + guard let field = obj.object as? NSTextField, field === timeFormatField else { return } + Preferences[.timeFormatTextField] = field.stringValue + } + + // MARK: - GIF Actions + + @objc private func gifToggleChanged() { + let on = showGifCheck.state == .on + Preferences[.shouldShowGif] = on + gifContentStack.isHidden = !on + NotificationCenter.default.post(name: .shouldReloadStatusWidget, object: nil) + } + + @objc private func gifSourceChanged() { + Preferences[.gifSourceType] = gifSourceControl.selectedSegment + refreshGIFSourceRows() + refreshGIFPreview() + NotificationCenter.default.post(name: .shouldReloadStatusWidget, object: nil) + } + + @objc private func gifURLChanged() { + let text = gifURLField.stringValue.trimmingCharacters(in: .whitespacesAndNewlines) + Preferences[.gifURLString] = text + refreshGIFPreview() + NotificationCenter.default.post(name: .shouldReloadStatusWidget, object: nil) + } + + @objc private func gifChooseFile() { + let panel = NSOpenPanel() + if #available(macOS 12.0, *) { + panel.allowedContentTypes = [UTType.gif] + } else { + panel.allowedFileTypes = ["gif"] + } + panel.allowsMultipleSelection = false + panel.canChooseDirectories = false + panel.begin { [weak self] result in + guard let self, result == .OK, let url = panel.url else { return } + Preferences[.gifFilePath] = url.path + self.gifFileField.stringValue = url.path + self.refreshGIFPreview() + NotificationCenter.default.post(name: .shouldReloadStatusWidget, object: nil) + } + } + + @objc private func gifWidthChanged() { + let w = gifWidthSlider.doubleValue + Preferences[.gifWidth] = w + gifWidthLabel.stringValue = "\(Int(w)) pt" + gifPreviewWidthConstraint.constant = CGFloat(w) + NotificationCenter.default.post(name: .shouldReloadStatusWidget, object: nil) + } + + @objc private func gifScalingChanged() { + let mode = gifScalePopup.indexOfSelectedItem + Preferences[.gifScalingMode] = mode + if let piv = gifPreviewImageView { applyScaling(mode, to: piv) } + NotificationCenter.default.post(name: .shouldReloadStatusWidget, object: nil) + } + + // MARK: - GIF Helpers + + private func refreshGIFSourceRows() { + let isURL = gifSourceControl.selectedSegment == 1 + gifURLRow.isHidden = !isURL + gifFileRow.isHidden = isURL + } + + private func refreshGIFPreview() { + guard let piv = gifPreviewImageView else { return } + let isURL = gifSourceControl.selectedSegment == 1 + let mode = gifScalePopup.indexOfSelectedItem + + if isURL { + let str: String = Preferences[.gifURLString] + guard let url = URL(string: str) else { return } + URLSession.shared.dataTask(with: url) { [weak self, weak piv] data, _, _ in + guard let self, let piv, let data, let img = NSImage(data: data) else { return } + DispatchQueue.main.async { + piv.image = img + piv.animates = true + self.applyScaling(mode, to: piv) + } + }.resume() + } else { + let path: String = Preferences[.gifFilePath] + guard !path.isEmpty, + let data = try? Data(contentsOf: URL(fileURLWithPath: path)), + let img = NSImage(data: data) else { return } + piv.image = img + piv.animates = true + applyScaling(mode, to: piv) + } + } + + private func applyScaling(_ mode: Int, to iv: NSImageView) { + switch mode { + case 1: + iv.imageScaling = .scaleAxesIndependently + iv.layer?.contentsGravity = .resizeAspectFill + case 2: + iv.imageScaling = .scaleAxesIndependently + iv.layer?.contentsGravity = .resize + default: + iv.imageScaling = .scaleProportionallyUpOrDown + iv.layer?.contentsGravity = .resizeAspect + } + } + + // MARK: - UI Factory + + private func makeCheckbox(_ title: String, tag: Int, on: Bool) -> NSButton { + let btn = NSButton(checkboxWithTitle: title, target: self, action: #selector(checkboxChanged(_:))) + btn.tag = tag + btn.state = on ? .on : .off + return btn + } + + private func sectionLabel(_ text: String) -> NSTextField { + let lbl = NSTextField(labelWithString: text.uppercased()) + lbl.font = NSFont.systemFont(ofSize: 10, weight: .semibold) + lbl.textColor = NSColor.secondaryLabelColor + return lbl + } + + private func makeRow(_ label: String, _ control: NSView) -> NSStackView { + let lbl = NSTextField(labelWithString: label) + lbl.font = NSFont.systemFont(ofSize: 12, weight: .semibold) + lbl.setContentHuggingPriority(.defaultHigh, for: .horizontal) + let row = NSStackView(views: [lbl, control]) + row.orientation = .horizontal + row.alignment = .firstBaseline + row.spacing = 6 + return row + } + + private func makeSeparator() -> NSBox { + let sep = NSBox() + sep.boxType = .separator + return sep } } diff --git a/Status/Sources/Preferences/StatusWidgetPreferencePane/en.lproj/StatusWidgetPreferencePane.strings b/Status/Sources/Preferences/StatusWidgetPreferencePane/en.lproj/StatusWidgetPreferencePane.strings deleted file mode 100644 index 718b221..0000000 --- a/Status/Sources/Preferences/StatusWidgetPreferencePane/en.lproj/StatusWidgetPreferencePane.strings +++ /dev/null @@ -1,24 +0,0 @@ - -/* Class = "NSTextFieldCell"; title = "EE dd MMM HH:mm"; ObjectID = "6HU-p8-WWc"; */ -"6HU-p8-WWc.title" = "EE dd MMM HH:mm"; - -/* Class = "NSTextFieldCell"; title = "Time Format:"; ObjectID = "6tU-S4-ovU"; */ -"6tU-S4-ovU.title" = "Time Format:"; - -/* Class = "NSButtonCell"; title = "Show Power item"; ObjectID = "OGc-Cn-VS5"; */ -"OGc-Cn-VS5.title" = "Show Power item"; - -/* Class = "NSButton"; ibShadowedToolTip = "Time Format Help"; ObjectID = "QDl-u9-yz3"; */ -"QDl-u9-yz3.ibShadowedToolTip" = "Time Format Help"; - -/* Class = "NSButtonCell"; title = "Show Battery Icon"; ObjectID = "S9t-Wy-9IV"; */ -"S9t-Wy-9IV.title" = "Show Battery Icon"; - -/* Class = "NSButtonCell"; title = "Show Date item"; ObjectID = "Zze-FZ-vdx"; */ -"Zze-FZ-vdx.title" = "Show Date item"; - -/* Class = "NSButtonCell"; title = "Show Wifi item"; ObjectID = "c9g-yA-umL"; */ -"c9g-yA-umL.title" = "Show Wifi item"; - -/* Class = "NSButtonCell"; title = "Show Battery Percentage"; ObjectID = "zlO-nx-U7P"; */ -"zlO-nx-U7P.title" = "Show Battery Percentage"; diff --git a/Status/Sources/Preferences/StatusWidgetPreferencePane/it.lproj/StatusWidgetPreferencePane.strings b/Status/Sources/Preferences/StatusWidgetPreferencePane/it.lproj/StatusWidgetPreferencePane.strings deleted file mode 100644 index 1744f36..0000000 --- a/Status/Sources/Preferences/StatusWidgetPreferencePane/it.lproj/StatusWidgetPreferencePane.strings +++ /dev/null @@ -1,24 +0,0 @@ - -/* Class = "NSTextFieldCell"; title = "EE dd MMM HH:mm"; ObjectID = "6HU-p8-WWc"; */ -"6HU-p8-WWc.title" = "EE dd MMM HH:mm"; - -/* Class = "NSTextFieldCell"; title = "Time Format:"; ObjectID = "6tU-S4-ovU"; */ -"6tU-S4-ovU.title" = "Time Format:"; - -/* Class = "NSButtonCell"; title = "Show Power item"; ObjectID = "OGc-Cn-VS5"; */ -"OGc-Cn-VS5.title" = "Mostra informazioni sulla batteria"; - -/* Class = "NSButton"; ibShadowedToolTip = "Time Format Help"; ObjectID = "QDl-u9-yz3"; */ -"QDl-u9-yz3.ibShadowedToolTip" = "Time Format Help"; - -/* Class = "NSButtonCell"; title = "Show Battery Icon"; ObjectID = "S9t-Wy-9IV"; */ -"S9t-Wy-9IV.title" = "Mostra icona"; - -/* Class = "NSButtonCell"; title = "Show Date item"; ObjectID = "Zze-FZ-vdx"; */ -"Zze-FZ-vdx.title" = "Mostra la data e l'ora attuale"; - -/* Class = "NSButtonCell"; title = "Show Wifi item"; ObjectID = "c9g-yA-umL"; */ -"c9g-yA-umL.title" = "Mostra informaziioni sulla connettività Wi-Fi"; - -/* Class = "NSButtonCell"; title = "Show Battery Percentage"; ObjectID = "zlO-nx-U7P"; */ -"zlO-nx-U7P.title" = "Mostra percentuale di carica"; diff --git a/Status/Sources/Preferences/StatusWidgetPreferencePane/tr.lproj/StatusWidgetPreferencePane.strings b/Status/Sources/Preferences/StatusWidgetPreferencePane/tr.lproj/StatusWidgetPreferencePane.strings deleted file mode 100644 index 929d812..0000000 --- a/Status/Sources/Preferences/StatusWidgetPreferencePane/tr.lproj/StatusWidgetPreferencePane.strings +++ /dev/null @@ -1,24 +0,0 @@ - -/* Class = "NSTextFieldCell"; title = "EE dd MMM HH:mm"; ObjectID = "6HU-p8-WWc"; */ -"6HU-p8-WWc.title" = "EE dd MMM HH:mm"; - -/* Class = "NSTextFieldCell"; title = "Time Format:"; ObjectID = "6tU-S4-ovU"; */ -"6tU-S4-ovU.title" = "Time Format:"; - -/* Class = "NSButtonCell"; title = "Show Power item"; ObjectID = "OGc-Cn-VS5"; */ -"OGc-Cn-VS5.title" = "Güç Ögelerini Göster"; - -/* Class = "NSButton"; ibShadowedToolTip = "Time Format Help"; ObjectID = "QDl-u9-yz3"; */ -"QDl-u9-yz3.ibShadowedToolTip" = "Time Format Help"; - -/* Class = "NSButtonCell"; title = "Show Battery Icon"; ObjectID = "S9t-Wy-9IV"; */ -"S9t-Wy-9IV.title" = "Batarya İkonunu Göster"; - -/* Class = "NSButtonCell"; title = "Show Date item"; ObjectID = "Zze-FZ-vdx"; */ -"Zze-FZ-vdx.title" = "Tarihi Göster"; - -/* Class = "NSButtonCell"; title = "Show Wifi item"; ObjectID = "c9g-yA-umL"; */ -"c9g-yA-umL.title" = "Wi-Fi'ı Göster"; - -/* Class = "NSButtonCell"; title = "Show Battery Percentage"; ObjectID = "zlO-nx-U7P"; */ -"zlO-nx-U7P.title" = "Batarya Yüzdesini Göster"; diff --git a/Status/Sources/Preferences/StatusWidgetPreferencePane/zh-Hans.lproj/StatusWidgetPreferencePane.strings b/Status/Sources/Preferences/StatusWidgetPreferencePane/zh-Hans.lproj/StatusWidgetPreferencePane.strings deleted file mode 100644 index 2ccd3e4..0000000 --- a/Status/Sources/Preferences/StatusWidgetPreferencePane/zh-Hans.lproj/StatusWidgetPreferencePane.strings +++ /dev/null @@ -1,24 +0,0 @@ - -/* Class = "NSTextFieldCell"; title = "EE dd MMM HH:mm"; ObjectID = "6HU-p8-WWc"; */ -"6HU-p8-WWc.title" = "EE dd MMM HH:mm"; - -/* Class = "NSTextFieldCell"; title = "Time Format:"; ObjectID = "6tU-S4-ovU"; */ -"6tU-S4-ovU.title" = "时间格式:"; - -/* Class = "NSButtonCell"; title = "Show Power item"; ObjectID = "OGc-Cn-VS5"; */ -"OGc-Cn-VS5.title" = "显示电池"; - -/* Class = "NSButton"; ibShadowedToolTip = "Time Format Help"; ObjectID = "QDl-u9-yz3"; */ -"QDl-u9-yz3.ibShadowedToolTip" = "时间格式帮助"; - -/* Class = "NSButtonCell"; title = "Show Battery Icon"; ObjectID = "S9t-Wy-9IV"; */ -"S9t-Wy-9IV.title" = "显示电池图标"; - -/* Class = "NSButtonCell"; title = "Show Date item"; ObjectID = "Zze-FZ-vdx"; */ -"Zze-FZ-vdx.title" = "显示时间"; - -/* Class = "NSButtonCell"; title = "Show Wifi item"; ObjectID = "c9g-yA-umL"; */ -"c9g-yA-umL.title" = "显示Wifi"; - -/* Class = "NSButtonCell"; title = "Show Battery Percentage"; ObjectID = "zlO-nx-U7P"; */ -"zlO-nx-U7P.title" = "显示电池百分比"; diff --git a/Status/Sources/Preferences/StatusWidgetPreferencePane/zh-Hant.lproj/StatusWidgetPreferencePane.strings b/Status/Sources/Preferences/StatusWidgetPreferencePane/zh-Hant.lproj/StatusWidgetPreferencePane.strings deleted file mode 100644 index f2d632d..0000000 --- a/Status/Sources/Preferences/StatusWidgetPreferencePane/zh-Hant.lproj/StatusWidgetPreferencePane.strings +++ /dev/null @@ -1,24 +0,0 @@ - -/* Class = "NSTextFieldCell"; title = "EE dd MMM HH:mm"; ObjectID = "6HU-p8-WWc"; */ -"6HU-p8-WWc.title" = "EE dd MMM HH:mm"; - -/* Class = "NSTextFieldCell"; title = "Time Format:"; ObjectID = "6tU-S4-ovU"; */ -"6tU-S4-ovU.title" = "時間格式:"; - -/* Class = "NSButtonCell"; title = "Show Power item"; ObjectID = "OGc-Cn-VS5"; */ -"OGc-Cn-VS5.title" = "顯示電量"; - -/* Class = "NSButton"; ibShadowedToolTip = "Time Format Help"; ObjectID = "QDl-u9-yz3"; */ -"QDl-u9-yz3.ibShadowedToolTip" = "時間格式幫助"; - -/* Class = "NSButtonCell"; title = "Show Battery Icon"; ObjectID = "S9t-Wy-9IV"; */ -"S9t-Wy-9IV.title" = "顯示電池圖示"; - -/* Class = "NSButtonCell"; title = "Show Date item"; ObjectID = "Zze-FZ-vdx"; */ -"Zze-FZ-vdx.title" = "顯示時間"; - -/* Class = "NSButtonCell"; title = "Show Wifi item"; ObjectID = "c9g-yA-umL"; */ -"c9g-yA-umL.title" = "顯示 Wi-Fi"; - -/* Class = "NSButtonCell"; title = "Show Battery Percentage"; ObjectID = "zlO-nx-U7P"; */ -"zlO-nx-U7P.title" = "顯示電池百分比"; diff --git a/Status/Sources/StatusItem.swift b/Status/Sources/StatusItem.swift old mode 100644 new mode 100755 index b373d09..967ad28 --- a/Status/Sources/StatusItem.swift +++ b/Status/Sources/StatusItem.swift @@ -7,6 +7,7 @@ // import Foundation +import AppKit import PockKit class StatusItemView: PKView { diff --git a/Status/Sources/StatusWidget.swift b/Status/Sources/StatusWidget.swift index c605d27..908ce07 100755 --- a/Status/Sources/StatusWidget.swift +++ b/Status/Sources/StatusWidget.swift @@ -12,110 +12,106 @@ import PockKit import TinyConstraints extension NSImage { - /// Returns an NSImage snapshot of the passed view in 2x resolution. - convenience init?(frame: NSRect, view: NSView) { - guard let bitmapRep = view.bitmapImageRepForCachingDisplay(in: frame) else { - return nil - } - self.init() - view.cacheDisplay(in: frame, to: bitmapRep) - addRepresentation(bitmapRep) - bitmapRep.size = frame.size - } + /// Returns an NSImage snapshot of the passed view. + convenience init?(frame: NSRect, view: NSView) { + guard let bitmapRep = view.bitmapImageRepForCachingDisplay(in: frame) else { return nil } + self.init() + view.cacheDisplay(in: frame, to: bitmapRep) + addRepresentation(bitmapRep) + bitmapRep.size = frame.size + } } -class StatusWidget: PKWidget { - - static var identifier: String = "StatusWidget" - var customizationLabel: String = "Status" - var view: NSView! - - private var stackView: NSStackView { - return view as! NSStackView - } - private var loadedItems: [StatusItem] = [] - - var imageForCustomization: NSImage { - let stackView = NSStackView(frame: .zero) - stackView.orientation = .horizontal - stackView.alignment = .centerY - stackView.distribution = .fill - stackView.spacing = 8 - if Preferences[.shouldShowLangItem] { - stackView.addArrangedSubview(SLangItem().view) - } - if Preferences[.shouldShowWifiItem] { - stackView.addArrangedSubview(SWifiItem().view) - } - if Preferences[.shouldShowPowerItem] { - stackView.addArrangedSubview(SPowerItem().view) - } - if Preferences[.shouldShowDateItem] { - stackView.addArrangedSubview(SClockItem().view) - } - return NSImage(frame: NSRect(origin: .zero, size: stackView.fittingSize), view: stackView) ?? NSImage() - } - - func prepareForCustomization() { - clearItems() - } - +class StatusV2Widget: PKWidget { + + static var identifier: String = "EvyatarEshkar.StatusV2Widget" + var customizationLabel: String = "Status V2" + var view: NSView! + + private var stackView: NSStackView { return view as! NSStackView } + private var loadedItems: [StatusItem] = [] + + // MARK: - PKWidget + + var imageForCustomization: NSImage { + let sv = NSStackView(frame: .zero) + sv.orientation = .horizontal + sv.alignment = .centerY + sv.distribution = .fill + sv.spacing = 8 + if Preferences[.shouldShowLangItem] { sv.addArrangedSubview(SLangItem().view) } + if Preferences[.shouldShowWifiItem] { sv.addArrangedSubview(SWifiItem().view) } + if Preferences[.shouldShowPowerItem] { sv.addArrangedSubview(SPowerItem().view) } + if Preferences[.shouldShowDateItem] { sv.addArrangedSubview(SClockItem().view) } + return NSImage(frame: NSRect(origin: .zero, size: sv.fittingSize), view: sv) ?? NSImage() + } + + func prepareForCustomization() { + clearItems() + } + required init() { - view = NSStackView(frame: .zero) - stackView.orientation = .horizontal - stackView.alignment = .centerY - stackView.distribution = .fill - stackView.spacing = 8 + view = NSStackView(frame: .zero) + stackView.orientation = .horizontal + stackView.alignment = .centerY + stackView.distribution = .fill + stackView.spacing = 8 } - + deinit { - clearItems() + clearItems() } - + func viewDidAppear() { - loadStatusElements() - NotificationCenter.default.addObserver(self, selector: #selector(loadStatusElements), name: .shouldReloadStatusWidget, object: nil) + loadStatusElements() + NotificationCenter.default.addObserver( + self, selector: #selector(loadStatusElements), + name: .shouldReloadStatusWidget, object: nil + ) } - + func viewWillDisappear() { clearItems() - NotificationCenter.default.removeObserver(self) + NotificationCenter.default.removeObserver(self) + } + + // MARK: - Loading + + private func clearItems() { + for v in stackView.arrangedSubviews { + stackView.removeArrangedSubview(v) + v.removeFromSuperview() + } + loadedItems.forEach { $0.didUnload() } + loadedItems.removeAll() } - - private func clearItems() { - for view in stackView.arrangedSubviews { - stackView.removeArrangedSubview(view) - view.removeFromSuperview() - } - for item in loadedItems { - item.didUnload() - } - loadedItems.removeAll() - } - + @objc private func loadStatusElements() { - clearItems() - if Preferences[.shouldShowLangItem] { - let item = SLangItem() - loadedItems.append(item) - stackView.addArrangedSubview(item.view) - } - if Preferences[.shouldShowWifiItem] { - let item = SWifiItem() - loadedItems.append(item) - stackView.addArrangedSubview(item.view) - } - if Preferences[.shouldShowPowerItem] { - let item = SPowerItem() - loadedItems.append(item) - stackView.addArrangedSubview(item.view) - } - if Preferences[.shouldShowDateItem] { - let item = SClockItem() - loadedItems.append(item) - stackView.addArrangedSubview(item.view) - } - stackView.height(30) + clearItems() + + if Preferences[.shouldShowLangItem] { + let item = SLangItem(); loadedItems.append(item) + stackView.addArrangedSubview(item.view) + } + if Preferences[.shouldShowWifiItem] { + let item = SWifiItem(); loadedItems.append(item) + stackView.addArrangedSubview(item.view) + } + if Preferences[.shouldShowPowerItem] { + let item = SPowerItem(); loadedItems.append(item) + stackView.addArrangedSubview(item.view) + } + if Preferences[.shouldShowDateItem] { + let item = SClockItem(); loadedItems.append(item) + stackView.addArrangedSubview(item.view) + } + + // GIF — rightmost item + if Preferences[.shouldShowGif] { + let item = SGifItem(); loadedItems.append(item) + stackView.addArrangedSubview(item.view) + } + + stackView.height(30) } - } diff --git a/images/Settings.png b/images/Settings.png new file mode 100644 index 0000000..7677502 Binary files /dev/null and b/images/Settings.png differ diff --git a/images/TouchBar-Preview-PockStatusV2Widget.gif b/images/TouchBar-Preview-PockStatusV2Widget.gif new file mode 100644 index 0000000..636e7fa Binary files /dev/null and b/images/TouchBar-Preview-PockStatusV2Widget.gif differ