diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2f3986 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +xcuserdata/ +project.xcworkspace + +.DS_Store +._* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..26c0521 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 František Nesveda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dcff657 --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +

Logo of the project

+

Namedays

+
+

A Mac app which displays Czech namedays in the menubar.

+
+

A screenshot of the application

+ +About +----- +*Namedays* is a small utility which shows the current and future Czech namedays in the menubar of your Mac. + +This is a small summer project which I did to get some practice with developing Mac apps. There are a few similar projects available, but they are either abandoned or paid, so I wanted to make a free, simple, open source alternative to replace them. + +Installation +------------ +*Namedays* supports macOS 10.11 El Capitan and higher. + +To install, download the [latest DMG release](https://www.github.com/fnesveda/Namedays/releases/latest), open it and drag *Namedays.app* to your *Applications* folder. + +Usage +----- +The app simply shows the nameday for the current date in the menubar of your Mac. +When you click on the menubar item, a menu opens showing namedays for several days in the future. + +In the app's preferences, accessible from the menu, you can customize the number of future namedays displayed in the menu or control the login item of the app. + +Developing +---------- +The app is a standard Xcode & Swift project without any external dependencies. You can just download or clone the repository and open *src/Namedays.xcodeproj* to start making changes. + +The whole project is written in Swift 4.2, without using any Objective-C bridging headers. + + +### Quirks and perks + +Due to the way the app manages its login item (through [Launch Services](https://developer.apple.com/documentation/coreservices/launch_services)), App Sandbox can't be enabled. This means the app can't be released on the Mac App Store, at least for now. + +*LoginItemManager.swift* contains an implementation of a login item manager for the app. +There is a bunch of login item managers available already, I implemented it in a way that you can bind the property *isEnabled* of the manager to a checkbox in Interface Builder through the included value transformer *LoginItemCheckboxTransformer*. +Unfortunately, Apple deprecated the APIs to access the login item list starting from Sierra, without offering any real alternative. +I hope these will keep working for at least a few more years, but eventually this will need to be rewritten to utilize the Service Management Framework, which will have the side effect of not showing the login item in System Preferences. + +Links +----- +- Project homepage: https://www.nesveda.com/projects/Namedays +- Project repository: https://www.github.com/fnesveda/Namedays + +Licensing +--------- +The code in this project is licensed under the MIT license. +Everybody is welcome to use, change and modify the project as they see fit. diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000..3e915ca Binary files /dev/null and b/docs/images/logo.png differ diff --git a/docs/images/screenshots/main.png b/docs/images/screenshots/main.png new file mode 100644 index 0000000..7045f16 Binary files /dev/null and b/docs/images/screenshots/main.png differ diff --git a/src/Namedays.xcodeproj/project.pbxproj b/src/Namedays.xcodeproj/project.pbxproj new file mode 100644 index 0000000..bc1b088 --- /dev/null +++ b/src/Namedays.xcodeproj/project.pbxproj @@ -0,0 +1,396 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + A406B1C5218F633100325A9F /* StatusMenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A406B1C4218F633100325A9F /* StatusMenuController.swift */; }; + A406B1C8218F652700325A9F /* StatusMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = A406B1CA218F652700325A9F /* StatusMenu.xib */; }; + A406B1D3218F68F400325A9F /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = A406B1D5218F68F400325A9F /* MainMenu.xib */; }; + A41C0C0A2166233C00EF6C52 /* PreferencesWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = A41C0C0C2166233C00EF6C52 /* PreferencesWindow.xib */; }; + A4A4BE7920C6C787003F2DF9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4A4BE7820C6C787003F2DF9 /* AppDelegate.swift */; }; + A4A4BE7B20C6C789003F2DF9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A4A4BE7A20C6C789003F2DF9 /* Assets.xcassets */; }; + A4A4BE8920C6D6D4003F2DF9 /* Nameday.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4A4BE8820C6D6D4003F2DF9 /* Nameday.swift */; }; + A4A4BE8C20C724D9003F2DF9 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4A4BE8A20C724D9003F2DF9 /* PreferencesWindowController.swift */; }; + A4F3B3852164CB1D0055F207 /* LoginItemManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4F3B3842164CB1D0055F207 /* LoginItemManager.swift */; }; + A4F9C0DD2166A8FA00FED626 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = A4F9C0DF2166A8FA00FED626 /* InfoPlist.strings */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A406B1C4218F633100325A9F /* StatusMenuController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusMenuController.swift; sourceTree = ""; }; + A406B1C9218F652700325A9F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/StatusMenu.xib; sourceTree = ""; }; + A406B1CC218F652A00325A9F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/StatusMenu.strings; sourceTree = ""; }; + A406B1CE218F652C00325A9F /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/StatusMenu.strings; sourceTree = ""; }; + A406B1D4218F68F400325A9F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + A406B1D9218F68FB00325A9F /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/MainMenu.strings; sourceTree = ""; }; + A41C0C0B2166233C00EF6C52 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PreferencesWindow.xib; sourceTree = ""; }; + A41C0C0E216693C000EF6C52 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/PreferencesWindow.strings; sourceTree = ""; }; + A41C0C10216699DE00EF6C52 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/PreferencesWindow.strings; sourceTree = ""; }; + A430B4502189D0A600719B1E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A4A4BE7520C6C787003F2DF9 /* Namedays.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Namedays.app; sourceTree = BUILT_PRODUCTS_DIR; }; + A4A4BE7820C6C787003F2DF9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + A4A4BE7A20C6C789003F2DF9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + A4A4BE8820C6D6D4003F2DF9 /* Nameday.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Nameday.swift; sourceTree = ""; }; + A4A4BE8A20C724D9003F2DF9 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; + A4D285A221D950D300515F46 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = ""; }; + A4F3B3842164CB1D0055F207 /* LoginItemManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginItemManager.swift; sourceTree = ""; }; + A4F9C0DE2166A8FA00FED626 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + A4F9C0E02166A8FC00FED626 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A4A4BE7220C6C787003F2DF9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A4A4BE6C20C6C787003F2DF9 = { + isa = PBXGroup; + children = ( + A4A4BE7720C6C787003F2DF9 /* Namedays */, + A4A4BE7620C6C787003F2DF9 /* Products */, + ); + sourceTree = ""; + usesTabs = 1; + }; + A4A4BE7620C6C787003F2DF9 /* Products */ = { + isa = PBXGroup; + children = ( + A4A4BE7520C6C787003F2DF9 /* Namedays.app */, + ); + name = Products; + sourceTree = ""; + }; + A4A4BE7720C6C787003F2DF9 /* Namedays */ = { + isa = PBXGroup; + children = ( + A4A4BE7A20C6C789003F2DF9 /* Assets.xcassets */, + A430B4502189D0A600719B1E /* Info.plist */, + A4F9C0DF2166A8FA00FED626 /* InfoPlist.strings */, + A406B1D5218F68F400325A9F /* MainMenu.xib */, + A4A4BE7820C6C787003F2DF9 /* AppDelegate.swift */, + A4F3B3842164CB1D0055F207 /* LoginItemManager.swift */, + A4A4BE8820C6D6D4003F2DF9 /* Nameday.swift */, + A4A4BE8A20C724D9003F2DF9 /* PreferencesWindowController.swift */, + A41C0C0C2166233C00EF6C52 /* PreferencesWindow.xib */, + A406B1C4218F633100325A9F /* StatusMenuController.swift */, + A406B1CA218F652700325A9F /* StatusMenu.xib */, + ); + path = Namedays; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A4A4BE7420C6C787003F2DF9 /* Namedays */ = { + isa = PBXNativeTarget; + buildConfigurationList = A4A4BE8320C6C789003F2DF9 /* Build configuration list for PBXNativeTarget "Namedays" */; + buildPhases = ( + A4A4BE7120C6C787003F2DF9 /* Sources */, + A4A4BE7220C6C787003F2DF9 /* Frameworks */, + A4A4BE7320C6C787003F2DF9 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Namedays; + productName = Namedays; + productReference = A4A4BE7520C6C787003F2DF9 /* Namedays.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A4A4BE6D20C6C787003F2DF9 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1010; + LastUpgradeCheck = 1010; + TargetAttributes = { + A4A4BE7420C6C787003F2DF9 = { + CreatedOnToolsVersion = 10.1; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 0; + }; + }; + }; + }; + }; + buildConfigurationList = A4A4BE7020C6C787003F2DF9 /* Build configuration list for PBXProject "Namedays" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + cs, + ); + mainGroup = A4A4BE6C20C6C787003F2DF9; + productRefGroup = A4A4BE7620C6C787003F2DF9 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A4A4BE7420C6C787003F2DF9 /* Namedays */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A4A4BE7320C6C787003F2DF9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A4A4BE7B20C6C789003F2DF9 /* Assets.xcassets in Resources */, + A406B1D3218F68F400325A9F /* MainMenu.xib in Resources */, + A406B1C8218F652700325A9F /* StatusMenu.xib in Resources */, + A41C0C0A2166233C00EF6C52 /* PreferencesWindow.xib in Resources */, + A4F9C0DD2166A8FA00FED626 /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A4A4BE7120C6C787003F2DF9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A406B1C5218F633100325A9F /* StatusMenuController.swift in Sources */, + A4F3B3852164CB1D0055F207 /* LoginItemManager.swift in Sources */, + A4A4BE8C20C724D9003F2DF9 /* PreferencesWindowController.swift in Sources */, + A4A4BE8920C6D6D4003F2DF9 /* Nameday.swift in Sources */, + A4A4BE7920C6C787003F2DF9 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + A406B1CA218F652700325A9F /* StatusMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + A406B1C9218F652700325A9F /* Base */, + A406B1CC218F652A00325A9F /* en */, + A406B1CE218F652C00325A9F /* cs */, + ); + name = StatusMenu.xib; + sourceTree = ""; + }; + A406B1D5218F68F400325A9F /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + A406B1D4218F68F400325A9F /* Base */, + A406B1D9218F68FB00325A9F /* cs */, + A4D285A221D950D300515F46 /* en */, + ); + name = MainMenu.xib; + sourceTree = ""; + }; + A41C0C0C2166233C00EF6C52 /* PreferencesWindow.xib */ = { + isa = PBXVariantGroup; + children = ( + A41C0C0B2166233C00EF6C52 /* Base */, + A41C0C0E216693C000EF6C52 /* en */, + A41C0C10216699DE00EF6C52 /* cs */, + ); + name = PreferencesWindow.xib; + sourceTree = ""; + }; + A4F9C0DF2166A8FA00FED626 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + A4F9C0DE2166A8FA00FED626 /* en */, + A4F9C0E02166A8FC00FED626 /* cs */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + A4A4BE8120C6C789003F2DF9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A4A4BE8220C6C789003F2DF9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + A4A4BE8420C6C789003F2DF9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Namedays/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.nesveda.Namedays; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + }; + name = Debug; + }; + A4A4BE8520C6C789003F2DF9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Namedays/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.nesveda.Namedays; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A4A4BE7020C6C787003F2DF9 /* Build configuration list for PBXProject "Namedays" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A4A4BE8120C6C789003F2DF9 /* Debug */, + A4A4BE8220C6C789003F2DF9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A4A4BE8320C6C789003F2DF9 /* Build configuration list for PBXNativeTarget "Namedays" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A4A4BE8420C6C789003F2DF9 /* Debug */, + A4A4BE8520C6C789003F2DF9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A4A4BE6D20C6C787003F2DF9 /* Project object */; +} diff --git a/src/Namedays/AppDelegate.swift b/src/Namedays/AppDelegate.swift new file mode 100644 index 0000000..9121f06 --- /dev/null +++ b/src/Namedays/AppDelegate.swift @@ -0,0 +1,46 @@ +import AppKit + +@NSApplicationMain +class AppDelegate: NSObject, NSApplicationDelegate { + private var statusMenuController: StatusMenuController? + private var preferencesWindowController: PreferencesWindowController? + + func applicationWillFinishLaunching(_ notification: Notification) { + // don't put the "Enter Full Screen" menu item into the View submenu, there are no windows to put to fullscreen + UserDefaults.standard.set(false, forKey: "NSFullScreenMenuItemEverywhere") + // don't put the "Emoji & Symbols" menu item into the Edit submenu, there is no textbox to put emoji in + UserDefaults.standard.set(true, forKey: "NSDisabledCharacterPaletteMenuItem") + } + + func applicationDidFinishLaunching(_ aNotification: Notification) { + self.statusMenuController = StatusMenuController() + self.preferencesWindowController = PreferencesWindowController() + } + + @IBAction private func showPreferencesWindow(_ sender: Any?) { + self.activateApp() + DispatchQueue.main.async { self.preferencesWindowController?.showWindow(sender) } + } + + @IBAction private func showAboutPanel(_ sender: Any?) { + if #available(OSX 10.13, *) { + let link = "https://www.nesveda.com/projects/Namedays" + let credits = NSAttributedString(string: link, attributes: [.link: NSURL(string: link) ?? ""]) + NSApplication.shared.orderFrontStandardAboutPanel(options: [.credits: credits]) + } + else { + NSApplication.shared.orderFrontStandardAboutPanel(sender) + } + } + + // show the main menu and Dock icon + func activateApp() { + NSApplication.shared.setActivationPolicy(.regular) + NSApplication.shared.activate(ignoringOtherApps: true) + } + + // hide the main menu and Dock icon + func deactivateApp() { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150)) { NSApplication.shared.setActivationPolicy(.accessory) } + } +} diff --git a/src/Namedays/Assets.xcassets/AppIcon.appiconset/Contents.json b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..1244dd5 --- /dev/null +++ b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "Icon_16@1x.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "Icon_16@2x.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "Icon_32@1x.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "Icon_32@2x.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "Icon_128@1x.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "Icon_128@2x.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "Icon_256@1x.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "Icon_256@2x.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "Icon_512@1x.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "Icon_512@2x.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_128@1x.png b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_128@1x.png new file mode 100644 index 0000000..38d8697 Binary files /dev/null and b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_128@1x.png differ diff --git a/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_128@2x.png b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_128@2x.png new file mode 100644 index 0000000..d0907da Binary files /dev/null and b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_128@2x.png differ diff --git a/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_16@1x.png b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_16@1x.png new file mode 100644 index 0000000..9431cdf Binary files /dev/null and b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_16@1x.png differ diff --git a/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_16@2x.png b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_16@2x.png new file mode 100644 index 0000000..0df4e2b Binary files /dev/null and b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_16@2x.png differ diff --git a/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_256@1x.png b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_256@1x.png new file mode 100644 index 0000000..d0907da Binary files /dev/null and b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_256@1x.png differ diff --git a/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_256@2x.png b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_256@2x.png new file mode 100644 index 0000000..9dc13d8 Binary files /dev/null and b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_256@2x.png differ diff --git a/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_32@1x.png b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_32@1x.png new file mode 100644 index 0000000..0df4e2b Binary files /dev/null and b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_32@1x.png differ diff --git a/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_32@2x.png b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_32@2x.png new file mode 100644 index 0000000..8c41229 Binary files /dev/null and b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_32@2x.png differ diff --git a/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_512@1x.png b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_512@1x.png new file mode 100644 index 0000000..9dc13d8 Binary files /dev/null and b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_512@1x.png differ diff --git a/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_512@2x.png b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_512@2x.png new file mode 100644 index 0000000..647e87d Binary files /dev/null and b/src/Namedays/Assets.xcassets/AppIcon.appiconset/Icon_512@2x.png differ diff --git a/src/Namedays/Assets.xcassets/Contents.json b/src/Namedays/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/src/Namedays/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/src/Namedays/Base.lproj/MainMenu.xib b/src/Namedays/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..71390ca --- /dev/null +++ b/src/Namedays/Base.lproj/MainMenu.xib @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Namedays/Base.lproj/PreferencesWindow.xib b/src/Namedays/Base.lproj/PreferencesWindow.xib new file mode 100644 index 0000000..efc23d5 --- /dev/null +++ b/src/Namedays/Base.lproj/PreferencesWindow.xib @@ -0,0 +1,165 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Namedays/Base.lproj/StatusMenu.xib b/src/Namedays/Base.lproj/StatusMenu.xib new file mode 100644 index 0000000..7c6f82c --- /dev/null +++ b/src/Namedays/Base.lproj/StatusMenu.xib @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Namedays/Info.plist b/src/Namedays/Info.plist new file mode 100644 index 0000000..72c8af4 --- /dev/null +++ b/src/Namedays/Info.plist @@ -0,0 +1,37 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSApplicationCategoryType + public.app-category.utilities + LSHasLocalizedDisplayName + + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + LSBackgroundOnly + + NSHumanReadableCopyright + Copyright © 2018 František Nesveda. +All rights reserved. + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/src/Namedays/LoginItemManager.swift b/src/Namedays/LoginItemManager.swift new file mode 100644 index 0000000..4fe3f14 --- /dev/null +++ b/src/Namedays/LoginItemManager.swift @@ -0,0 +1,93 @@ +import Foundation + +// Manages the login item for the application +// Used as a singleton via LoginItemManager.shared +// To be utilized by either changing the .isEnabled property directly, +// or by binding to it (possibly using LoginItemCheckboxTransformer as ValueTransformer) +// Manipulates the login item list in System Preferences -> User -> Login Items via a deprecated API +internal class LoginItemManager: NSObject { + internal static let shared = LoginItemManager() + + override private init() { + super.init() + registerValueTransformer() + } + + private func registerValueTransformer() { + ValueTransformer.setValueTransformer(LoginItemCheckboxTransformer(), forName: .loginItemCheckboxTransformerName) + } + + private static func getLoginItemsRef() -> LSSharedFileList? { + return LSSharedFileListCreate(nil, kLSSharedFileListSessionLoginItems.takeRetainedValue(), nil)?.takeRetainedValue() + } + + private static func findLoginItem(loginItemsRef: LSSharedFileList?) -> LSSharedFileListItem? { + if loginItemsRef != nil { + var seedValue: UInt32 = 0 + if let loginItemsList = LSSharedFileListCopySnapshot(loginItemsRef, &seedValue)?.takeRetainedValue() as? [LSSharedFileListItem] { + for item in loginItemsList { + if let pathURL = LSSharedFileListItemCopyResolvedURL(item, 0, nil)?.takeRetainedValue() { + if let path = CFURLCopyPath(pathURL) as NSString? { + if path.hasPrefix(Bundle.main.bundlePath) { + return item + } + } + } + } + } + } + return nil + } + + @objc internal dynamic var isEnabled: Bool { + get { + let loginItemsRef = type(of: self).getLoginItemsRef() + let loginItem = type(of: self).findLoginItem(loginItemsRef: loginItemsRef) + return loginItem != nil + } + set { + let loginItemsRef = type(of: self).getLoginItemsRef() + let loginItem = type(of: self).findLoginItem(loginItemsRef: loginItemsRef) + + if (loginItem != nil) != newValue { + if newValue { + let appURL = URL(fileURLWithPath: Bundle.main.bundlePath) as CFURL + LSSharedFileListInsertItemURL(loginItemsRef, nil, nil, nil, appURL, nil, nil) + } + else { + LSSharedFileListItemRemove(loginItemsRef, loginItem) + } + } + } + } + + internal func updateEnabled() { + self.willChangeValue(forKey: "isEnabled") + self.didChangeValue(forKey: "isEnabled") + } +} + +// A value transformer to be used when binding a checkbox cell to .isEnabled +@objc(LoginItemCheckboxTransformer) +internal class LoginItemCheckboxTransformer: ValueTransformer { + override class func transformedValueClass() -> AnyClass { + return NSNumber.self + } + + override class func allowsReverseTransformation() -> Bool { + return true + } + + override func transformedValue(_ value: Any?) -> Any? { + let boolValue = value as? Bool ?? false + return NSNumber(value: boolValue) + } + + override func reverseTransformedValue(_ value: Any?) -> Any? { + return value as? NSNumber ?? NSNumber(value: 0) + } +} + +extension NSValueTransformerName { + static let loginItemCheckboxTransformerName = NSValueTransformerName(rawValue: LoginItemCheckboxTransformer.className()) +} diff --git a/src/Namedays/Nameday.swift b/src/Namedays/Nameday.swift new file mode 100644 index 0000000..901d588 --- /dev/null +++ b/src/Namedays/Nameday.swift @@ -0,0 +1,399 @@ +// Provides the nameday for any given day of the year +enum Nameday { + static func on(day: Int, month: Int) -> String { + return namedays[month]?[day] ?? "---" + } + + private static let namedays = [ + 1: [ + 1: "Nový rok", + 2: "Karina", + 3: "Radmila", + 4: "Diana", + 5: "Dalimil", + 6: "Tři králové", + 7: "Vilma", + 8: "Čestmír", + 9: "Vladan", + 10: "Břetislav", + 11: "Bohdana", + 12: "Pravoslav", + 13: "Edita", + 14: "Radovan", + 15: "Alice", + 16: "Ctirad", + 17: "Drahoslav", + 18: "Vladislav", + 19: "Doubravka", + 20: "Ilona", + 21: "Běla", + 22: "Slavomír", + 23: "Zdeněk", + 24: "Milena", + 25: "Miloš", + 26: "Zora", + 27: "Ingrid", + 28: "Otýlie", + 29: "Zdislava", + 30: "Robin", + 31: "Marika" + ], + 2: [ + 1: "Hynek a Jasmína", + 2: "Nela", + 3: "Blažej", + 4: "Jarmila", + 5: "Dobromila", + 6: "Vanda", + 7: "Veronika", + 8: "Milada", + 9: "Apolena", + 10: "Mojmír", + 11: "Božena", + 12: "Slavěna", + 13: "Věnceslav", + 14: "Valentýn", + 15: "Jiřina", + 16: "Ljuba", + 17: "Miloslava", + 18: "Gizela", + 19: "Patrik", + 20: "Oldřich", + 21: "Lenka", + 22: "Petr", + 23: "Svatopluk", + 24: "Matěj", + 25: "Liliana", + 26: "Dorota", + 27: "Alexandr", + 28: "Lumír", + 29: "Horymír" + ], + 3: [ + 1: "Bedřich", + 2: "Anežka", + 3: "Kamil", + 4: "Stela", + 5: "Kazimír", + 6: "Miroslav", + 7: "Tomáš", + 8: "Gabriela", + 9: "Františka", + 10: "Viktorie", + 11: "Anděla", + 12: "Řehoř", + 13: "Růžena", + 14: "Matylda a Rút", + 15: "Ida", + 16: "Elena a Herbert", + 17: "Vlastimil", + 18: "Eduard", + 19: "Josef", + 20: "Světlana", + 21: "Radek", + 22: "Leona", + 23: "Ivona", + 24: "Gabriel", + 25: "Marián", + 26: "Emanuel", + 27: "Dita", + 28: "Soňa", + 29: "Taťána", + 30: "Arnošt", + 31: "Kvido" + ], + 4: [ + 1: "Hugo", + 2: "Erika", + 3: "Richard", + 4: "Ivana", + 5: "Miroslava", + 6: "Vendula", + 7: "Hermína", + 8: "Ema", + 9: "Dušan", + 10: "Darja", + 11: "Izabela", + 12: "Julius", + 13: "Aleš", + 14: "Vincenc", + 15: "Anastázie", + 16: "Irena", + 17: "Rudolf", + 18: "Valérie", + 19: "Rostislav", + 20: "Marcela", + 21: "Alexandra", + 22: "Evžénie", + 23: "Vojtěch", + 24: "Jiří", + 25: "Marek", + 26: "Oto", + 27: "Jaroslav", + 28: "Vlastislav", + 29: "Robert", + 30: "Blahoslav" + ], + 5: [ + 1: "Svátek práce", + 2: "Zikmund", + 3: "Alexej", + 4: "Květoslav", + 5: "Klaudie", + 6: "Radoslav", + 7: "Stanislav", + 8: "Den vítězství", + 9: "Ctibor", + 10: "Blažena", + 11: "Svatava", + 12: "Pankrác", + 13: "Servác", + 14: "Bonifác", + 15: "Žofie", + 16: "Přemysl", + 17: "Aneta", + 18: "Nataša", + 19: "Ivo", + 20: "Zbyšek", + 21: "Monika", + 22: "Emil", + 23: "Vladimír", + 24: "Jana", + 25: "Viola", + 26: "Filip", + 27: "Valdemar", + 28: "Vilém", + 29: "Maxim", + 30: "Ferdinand", + 31: "Kamila" + ], + 6: [ + 1: "Laura", + 2: "Jarmil", + 3: "Tamara", + 4: "Dalibor", + 5: "Dobroslav", + 6: "Norbert", + 7: "Iveta", + 8: "Medard", + 9: "Stanislava", + 10: "Gita", + 11: "Bruno", + 12: "Antonie", + 13: "Antonín", + 14: "Roland", + 15: "Vít", + 16: "Zbyněk", + 17: "Adolf", + 18: "Milan", + 19: "Leoš", + 20: "Květa", + 21: "Alois", + 22: "Pavla", + 23: "Zdeňka", + 24: "Jan", + 25: "Ivan", + 26: "Adriana", + 27: "Ladislav", + 28: "Lubomír", + 29: "Petr a Pavel", + 30: "Šárka" + ], + 7: [ + 1: "Jaroslava", + 2: "Patricie", + 3: "Radomír", + 4: "Prokop", + 5: "Cyril a Metoděj", + 6: "Upálení Mistra Jana Husa", + 7: "Bohuslava", + 8: "Nora", + 9: "Drahoslava", + 10: "Libuše", + 11: "Olga", + 12: "Bořek", + 13: "Markéta", + 14: "Karolína", + 15: "Jindřich", + 16: "Luboš", + 17: "Martina", + 18: "Drahomíra", + 19: "Čeněk", + 20: "Ilja", + 21: "Vítězslav", + 22: "Magdaléna", + 23: "Libor", + 24: "Kristýna", + 25: "Jakub", + 26: "Anna", + 27: "Věroslav", + 28: "Viktor", + 29: "Marta", + 30: "Bořivoj", + 31: "Ignác" + ], + 8: [ + 1: "Oskar", + 2: "Gustav", + 3: "Miluše", + 4: "Dominik", + 5: "Kristián", + 6: "Oldřiška", + 7: "Lada", + 8: "Soběslav", + 9: "Roman", + 10: "Vavřinec", + 11: "Zuzana", + 12: "Klára", + 13: "Alena", + 14: "Alan", + 15: "Hana", + 16: "Jáchym", + 17: "Petra", + 18: "Helena", + 19: "Ludvík", + 20: "Bernard", + 21: "Johana", + 22: "Bohuslav", + 23: "Sandra", + 24: "Bartoloměj", + 25: "Radim", + 26: "Luděk", + 27: "Otakar", + 28: "Augustýn", + 29: "Evelína", + 30: "Vladěna", + 31: "Pavlína" + ], + 9: [ + 1: "Linda a Samuel", + 2: "Adéla", + 3: "Bronislav", + 4: "Jindřiška", + 5: "Boris", + 6: "Boleslav", + 7: "Regína", + 8: "Mariana", + 9: "Daniela", + 10: "Irma", + 11: "Denisa", + 12: "Marie", + 13: "Lubor", + 14: "Radka", + 15: "Jolana", + 16: "Ludmila", + 17: "Naděžda", + 18: "Kryštof", + 19: "Zita", + 20: "Oleg", + 21: "Matouš", + 22: "Darina", + 23: "Berta", + 24: "Jaromír", + 25: "Zlata", + 26: "Andrea", + 27: "Jonáš", + 28: "Václav", + 29: "Michal", + 30: "Jeroným" + ], + 10: [ + 1: "Igor", + 2: "Olívie a Oliver", + 3: "Bohumil", + 4: "František", + 5: "Eliška", + 6: "Hanuš", + 7: "Justýna", + 8: "Věra", + 9: "Sára a Štefan", + 10: "Marina", + 11: "Andrej", + 12: "Marcel", + 13: "Renáta", + 14: "Agáta", + 15: "Tereza", + 16: "Havel", + 17: "Hedvika", + 18: "Lukáš", + 19: "Michaela", + 20: "Vendelín", + 21: "Brigita", + 22: "Sabina", + 23: "Teodor", + 24: "Nina", + 25: "Beáta", + 26: "Erik", + 27: "Šarlota", + 28: "Den vzniku ČSR", + 29: "Silvie", + 30: "Tadeáš", + 31: "Štěpánka" + ], + 11: [ + 1: "Felix", + 2: "Tobiáš", + 3: "Hubert", + 4: "Karel", + 5: "Miriam", + 6: "Liběna", + 7: "Saskie", + 8: "Bohumír", + 9: "Bohdan", + 10: "Evžen", + 11: "Martin", + 12: "Benedikt", + 13: "Tibor", + 14: "Sáva", + 15: "Leopold", + 16: "Otmar", + 17: "Mahuléna", + 18: "Romana", + 19: "Alžběta", + 20: "Nikola", + 21: "Albert", + 22: "Cecílie", + 23: "Klement", + 24: "Emílie", + 25: "Kateřina", + 26: "Artur", + 27: "Xenie", + 28: "René", + 29: "Zina", + 30: "Ondřej" + ], + 12: [ + 1: "Iva", + 2: "Blanka", + 3: "Svatoslav", + 4: "Barbora", + 5: "Jitka", + 6: "Mikuláš", + 7: "Ambrož", + 8: "Květoslava", + 9: "Vratislav", + 10: "Julie", + 11: "Dana", + 12: "Simona", + 13: "Lucie", + 14: "Lýdie", + 15: "Radana", + 16: "Albína", + 17: "Daniel", + 18: "Miloslav", + 19: "Ester", + 20: "Dagmar", + 21: "Natálie", + 22: "Šimon", + 23: "Vlasta", + 24: "Adam a Eva", + 25: "1. svátek vánoční", + 26: "Štěpán", + 27: "Žaneta", + 28: "Bohumila", + 29: "Judita", + 30: "David", + 31: "Silvestr" + ] + ] +} diff --git a/src/Namedays/PreferencesWindowController.swift b/src/Namedays/PreferencesWindowController.swift new file mode 100644 index 0000000..2465a4c --- /dev/null +++ b/src/Namedays/PreferencesWindowController.swift @@ -0,0 +1,31 @@ +import AppKit + +// Controls the preferences window +class PreferencesWindowController: NSWindowController, NSWindowDelegate { + override var windowNibName: NSNib.Name? { + return "PreferencesWindow" + } + + @objc dynamic weak var sharedLoginItemManager = LoginItemManager.shared + + override func windowDidLoad() { + super.windowDidLoad() + self.window?.center() + } + + override func showWindow(_ sender: Any?) { + if !(self.window?.isVisible ?? true) { + self.window?.center() + } + super.showWindow(sender) + } + + func windowWillClose(_: Notification) { + (NSApplication.shared.delegate as? AppDelegate)?.deactivateApp() + } + + // recheck if login item is enabled on window updates + func windowDidUpdate(_: Notification) { + LoginItemManager.shared.updateEnabled() + } +} diff --git a/src/Namedays/StatusMenuController.swift b/src/Namedays/StatusMenuController.swift new file mode 100644 index 0000000..d06d3b4 --- /dev/null +++ b/src/Namedays/StatusMenuController.swift @@ -0,0 +1,109 @@ +import AppKit +import Foundation + +// Manages the status menu for the application +class StatusMenuController: NSObject { + @IBOutlet private var statusMenu: NSMenu! + private let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) + private var displayedItems = 0 + + @objc private dynamic var namedaysToDisplay: NSNumber = 7 { + didSet { + updateMenu() + } + } + + @objc private dynamic var showFutureNamedays: NSNumber = 1 { + didSet { + updateMenu() + } + } + + override init() { + super.init() + + // create the status menu + Bundle.main.loadNibNamed("StatusMenu", owner: self, topLevelObjects: nil) + statusItem.menu = statusMenu + + // human friendly name for autosaving the position of the status item + if #available(OSX 10.12, *) { + statusItem.autosaveName = "NamedaysStatusItem" + } + + // set the user defaults and bind them to the right properties + UserDefaults.standard.register(defaults: ["showFutureNamedays": 1, "namedaysToDisplay": 7]) + self.bind(NSBindingName(rawValue: "namedaysToDisplay"), to: NSUserDefaultsController.shared, withKeyPath: "values.namedaysToDisplay", options: nil) + self.bind(NSBindingName(rawValue: "showFutureNamedays"), to: NSUserDefaultsController.shared, withKeyPath: "values.showFutureNamedays", options: nil) + + // update the status menu on each calendar day change + NotificationCenter.default.addObserver(self, selector: #selector(updateMenuFromNotification), name: NSNotification.Name.NSCalendarDayChanged, object: nil) + updateMenu() + } + + @objc + private func updateMenuFromNotification(_: NSNotification) { + DispatchQueue.main.async { self.updateMenu() } + } + + // empty action for binding from menu items, so the names in the menu don't look disabled + @objc + private func noop() {} + + // update the status menu with the namedays for the next few days + private func updateMenu() { + let today = Date() + let calendar = NSCalendar.current + let components = calendar.dateComponents([.day, .month], from: today) + + statusItem.title = Nameday.on(day: components.day ?? 0, month: components.month ?? 0) + statusItem.menu = statusMenu + + // remove the old menu items + for _ in 0.. 0 && namedaysToDisplay.intValue > 0 { + let separator = NSMenuItem.separator() + statusMenu.insertItem(separator, at: 0) + displayedItems += 1 + + // align the date after each nameday to the same offset + var tabStopLocation: CGFloat = 0 + var paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.tabStops = [NSTextTab(textAlignment: .right, location: tabStopLocation)] + + // add the namedays to the menu + for offset in (1...namedaysToDisplay.intValue).reversed() { + if let date = calendar.date(byAdding: DateComponents(day: offset), to: today) { + let components = calendar.dateComponents([.day, .month], from: date) + let (day, month) = (components.day ?? 0, components.month ?? 0) + let name = Nameday.on(day: day, month: month) + let menuItem = NSMenuItem(title: "", action: #selector(self.noop), keyEquivalent: "") + menuItem.attributedTitle = NSAttributedString(string: "\(name)\t \(day). \(month).", attributes: [.paragraphStyle: paragraphStyle]) + menuItem.target = self + statusMenu.insertItem(menuItem, at: 0) + displayedItems += 1 + } + } + + // find the right position for the tab stop so everything fits but it's not too big + var oldMenuSize = CGFloat.infinity + var menuSize = statusMenu.size.width + while menuSize <= oldMenuSize { + tabStopLocation += 10 + paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.tabStops = [NSTextTab(textAlignment: .right, location: tabStopLocation)] + for offset in (0..