Skip to content

Commit

Permalink
🔀 Merge pull request #174 from `MrKai77/171-less-intrusive-prompt-whe…
Browse files Browse the repository at this point in the history
…n-new-icon-is-unlocked`

✨ #171 Less intrusive prompt when new icon is unlocked
  • Loading branch information
MrKai77 authored Jan 16, 2024
2 parents 7f8bdde + faf0cc1 commit 29b4071
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 25 deletions.
4 changes: 4 additions & 0 deletions Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
A84497D22B3B88A6003D4CF3 /* Optional+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A84497D12B3B88A6003D4CF3 /* Optional+Extensions.swift */; };
A848D8A72A8C2F3F00060834 /* LoopManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A848D8A62A8C2F3F00060834 /* LoopManager.swift */; };
A8504D2D2A85832F00C2EFDA /* SoftwareUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8504D2C2A85832F00C2EFDA /* SoftwareUpdater.swift */; };
A859799B2B55FE94009FB067 /* UNNotification+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A859799A2B55FE94009FB067 /* UNNotification+Extensions.swift */; };
A85B560E2AAAD62C00386ACE /* EventMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85B560D2AAAD62C00386ACE /* EventMonitor.swift */; };
A85CB5852ACFA5F700BF63E6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85CB5842ACFA5F700BF63E6 /* AppDelegate.swift */; };
A85FEEBC2AF15CDE00354D79 /* KeybindCustomizationViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85FEEBB2AF15CDE00354D79 /* KeybindCustomizationViewItem.swift */; };
Expand Down Expand Up @@ -113,6 +114,7 @@
A84497D12B3B88A6003D4CF3 /* Optional+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Optional+Extensions.swift"; sourceTree = "<group>"; };
A848D8A62A8C2F3F00060834 /* LoopManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoopManager.swift; sourceTree = "<group>"; };
A8504D2C2A85832F00C2EFDA /* SoftwareUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareUpdater.swift; sourceTree = "<group>"; };
A859799A2B55FE94009FB067 /* UNNotification+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UNNotification+Extensions.swift"; sourceTree = "<group>"; };
A85B560D2AAAD62C00386ACE /* EventMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventMonitor.swift; sourceTree = "<group>"; };
A85CB5842ACFA5F700BF63E6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
A85FEEBB2AF15CDE00354D79 /* KeybindCustomizationViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeybindCustomizationViewItem.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -226,6 +228,7 @@
A8330AC62A3AC19500673C8D /* NSScreen+Extensions.swift */,
A84497D12B3B88A6003D4CF3 /* Optional+Extensions.swift */,
A8330ACE2A3AC1E900673C8D /* View+Extensions.swift */,
A859799A2B55FE94009FB067 /* UNNotification+Extensions.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -502,6 +505,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
A859799B2B55FE94009FB067 /* UNNotification+Extensions.swift in Sources */,
A8EF1F09299C87DF00633440 /* IconManager.swift in Sources */,
A84497D22B3B88A6003D4CF3 /* Optional+Extensions.swift in Sources */,
A848D8A72A8C2F3F00060834 /* LoopManager.swift in Sources */,
Expand Down
99 changes: 98 additions & 1 deletion Loop/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@

import SwiftUI
import Defaults
import UserNotifications

class AppDelegate: NSObject, NSApplicationDelegate {
class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate {

private let loopManager = LoopManager()
private let windowDragManager = WindowDragManager()
Expand All @@ -26,6 +27,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {

// Check & ask for accessibility access
PermissionsManager.Accessibility.requestAccess()
UNUserNotificationCenter.current().delegate = self

AppDelegate.requestNotificationAuthorization()

IconManager.refreshCurrentAppIcon()
loopManager.startObservingKeys()
Expand Down Expand Up @@ -78,4 +82,97 @@ class AppDelegate: NSObject, NSApplicationDelegate {
window.center()
}
}

// ----------
// MARK: - Notifications
// ----------

func userNotificationCenter(
_: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void
) {
if response.actionIdentifier == "setIconAction",
let icon = response.notification.request.content.userInfo["icon"] as? String {
IconManager.setAppIcon(to: icon)
}

completionHandler()
}

// Implementation is necessary to show notifications even when the app has focus!
func userNotificationCenter(
_: UNUserNotificationCenter,
willPresent _: UNNotification,
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
) {
completionHandler([.banner])
}

static func requestNotificationAuthorization() {
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert]
) { (accepted, error) in
if !accepted {
print("User Notification access denied.")
}

if let error = error {
print(error)
}
}
}

private static func registerNotificationCategories() {
let setIconAction = UNNotificationAction(
identifier: "setIconAction",
title: "Set Current Icon",
options: .destructive
)
let notificationCategory = UNNotificationCategory(
identifier: "icon_unlocked",
actions: [setIconAction],
intentIdentifiers: []
)
UNUserNotificationCenter.current().setNotificationCategories([notificationCategory])
}

static func areNotificationsEnabled() -> Bool {
let group = DispatchGroup()
group.enter()

var notificationsEnabled = false

UNUserNotificationCenter.current().getNotificationSettings { notificationSettings in
notificationsEnabled = notificationSettings.authorizationStatus != UNAuthorizationStatus.denied
group.leave()
}

group.wait()
return notificationsEnabled
}

static func sendNotification(_ content: UNMutableNotificationContent) {
let uuidString = UUID().uuidString
let request = UNNotificationRequest(
identifier: uuidString,
content: content,
trigger: nil
)

requestNotificationAuthorization()
registerNotificationCategories()

UNUserNotificationCenter.current().add(request)
}

static func sendNotification(_ title: String, _ body: String) {
let content = UNMutableNotificationContent()

content.title = title
content.body = body
content.categoryIdentifier = UUID().uuidString

AppDelegate.sendNotification(content)
}
}
1 change: 1 addition & 0 deletions Loop/Extensions/Defaults+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ extension Defaults.Keys {
static let launchAtLogin = Key<Bool>("launchAtLogin", default: false)
static let hideMenuBarIcon = Key<Bool>("hideMenuBarIcon", default: false)
static let currentIcon = Key<String>("currentIcon", default: "AppIcon-Classic")
static let notificationWhenIconUnlocked = Key<Bool>("notificationWhenIconUnlocked", default: true)
static let timesLooped = Key<Int>("timesLooped", default: 0)
static let windowSnapping = Key<Bool>("windowSnapping", default: false) // BETA
static let animateWindowResizes = Key<Bool>("animateWindowResizes", default: false) // BETA
Expand Down
39 changes: 39 additions & 0 deletions Loop/Extensions/UNNotification+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// UNNotification+Extensions.swift
// Loop
//
// Created by Kai Azim on 2024-01-15.
//

import SwiftUI
import UserNotifications

// Thanks https://stackoverflow.com/questions/45226847/unnotificationattachment-failing-to-attach-image
extension UNNotificationAttachment {
static func create(_ imgData: NSData) -> UNNotificationAttachment? {
let imageFileIdentifier = UUID().uuidString + ".jpeg"

let fileManager = FileManager.default
let tmpSubFolderName = ProcessInfo.processInfo.globallyUniqueString
let tmpSubFolderURL = NSURL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(
tmpSubFolderName,
isDirectory: true
)

do {
try fileManager.createDirectory(at: tmpSubFolderURL!, withIntermediateDirectories: true, attributes: nil)
let fileURL = tmpSubFolderURL?.appendingPathComponent(imageFileIdentifier)
try imgData.write(to: fileURL!, options: [])
let imageAttachment = try UNNotificationAttachment.init(
identifier: imageFileIdentifier,
url: fileURL!,
options: nil
)
return imageAttachment
} catch let error {
print("error \(error)")
}

return nil
}
}
55 changes: 32 additions & 23 deletions Loop/Managers/IconManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import SwiftUI
import Defaults
import UserNotifications

class IconManager {

Expand All @@ -15,6 +16,15 @@ class IconManager {
var iconName: String
var unlockTime: Int
var unlockMessage: String?

func getName() -> String {
if let name = self.name {
return name
} else {
let prefix = "AppIcon-"
return iconName.replacingOccurrences(of: prefix, with: "")
}
}
}

private static let icons: [Icon] = [
Expand Down Expand Up @@ -44,11 +54,6 @@ class IconManager {
)
]

static func nameWithoutPrefix(name: String) -> String {
let prefix = "AppIcon-"
return name.replacingOccurrences(of: prefix, with: "")
}

static func returnUnlockedIcons() -> [Icon] {
var returnValue: [Icon] = []
for icon in icons where icon.unlockTime <= Defaults[.timesLooped] {
Expand All @@ -60,12 +65,13 @@ class IconManager {
static func setAppIcon(to icon: Icon) {
Defaults[.currentIcon] = icon.iconName
self.refreshCurrentAppIcon()
print("Setting app icon to: \(icon.getName())")
}

let alert = NSAlert()
alert.messageText = "\(Bundle.main.appName)"
alert.informativeText = "Current icon is now \(icon.name ?? nameWithoutPrefix(name: icon.iconName))!"
alert.icon = NSImage(named: icon.iconName)
alert.runModal()
static func setAppIcon(to iconName: String) {
if let targetIcon = icons.first(where: { $0.iconName == iconName }) {
setAppIcon(to: targetIcon)
}
}

// This function is run at startup to set the current icon to the user's set icon.
Expand All @@ -75,25 +81,28 @@ class IconManager {
}

static func checkIfUnlockedNewIcon() {
guard Defaults[.notificationWhenIconUnlocked] else { return }

for icon in icons where icon.unlockTime == Defaults[.timesLooped] {
NSApp.setActivationPolicy(.regular)
let content = UNMutableNotificationContent()

content.title = "Loop"

let alert = NSAlert()
alert.icon = NSImage(named: icon.iconName)
if let message = icon.unlockMessage {
alert.messageText = message
content.body = message
} else {
alert.messageText = "You've unlocked a new icon: \(nameWithoutPrefix(name: icon.iconName))!"
content.body = "You've unlocked a new icon: \(icon.getName())!"
}
alert.informativeText = "Would you like to set this as \(Bundle.main.appName)'s new icon?"
alert.alertStyle = .informational
alert.addButton(withTitle: "Yes").keyEquivalent = "\r"
alert.addButton(withTitle: "No")

let response = alert.runModal()
if response == NSApplication.ModalResponse.alertFirstButtonReturn {
setAppIcon(to: icon)

if let data = NSImage(named: icon.iconName)?.tiffRepresentation,
let attachment = UNNotificationAttachment.create(NSData(data: data)) {
content.attachments = [attachment]
content.userInfo = ["icon": icon.iconName]
}

content.categoryIdentifier = "icon_unlocked"

AppDelegate.sendNotification(content)
}
}

Expand Down
29 changes: 28 additions & 1 deletion Loop/Settings/GeneralSettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct GeneralSettingsView: View {
@Default(.useGradient) var useGradient
@Default(.gradientColor) var gradientColor
@Default(.currentIcon) var currentIcon
@Default(.notificationWhenIconUnlocked) var notificationWhenIconUnlocked
@Default(.timesLooped) var timesLooped
@Default(.animateWindowResizes) var animateWindowResizes
@Default(.windowPadding) var windowPadding
Expand Down Expand Up @@ -112,7 +113,7 @@ struct GeneralSettingsView: View {
ForEach(IconManager.returnUnlockedIcons(), id: \.self) { icon in
HStack {
Image(nsImage: NSImage(named: icon.iconName)!)
Text(icon.name ?? IconManager.nameWithoutPrefix(name: icon.iconName))
Text(icon.getName())
}
.tag(icon.iconName)
}
Expand All @@ -125,6 +126,32 @@ struct GeneralSettingsView: View {
.onChange(of: self.currentIcon) { _ in
IconManager.refreshCurrentAppIcon()
}

Toggle(
"Notify when new icons are unlocked",
isOn: Binding(
get: {
self.notificationWhenIconUnlocked
},
set: {
if $0 {
AppDelegate.sendNotification(
"Loop",
"You will now be notified when you unlock a new icon."
)

self.notificationWhenIconUnlocked = AppDelegate.areNotificationsEnabled()
} else {
self.notificationWhenIconUnlocked = $0
}
}
)
)
.onAppear {
if self.notificationWhenIconUnlocked {
self.notificationWhenIconUnlocked = AppDelegate.areNotificationsEnabled()
}
}
}

Section("Accent Color") {
Expand Down

0 comments on commit 29b4071

Please sign in to comment.