From 5ef5fba928721837c2c4e10ba51e8270bdf57ad2 Mon Sep 17 00:00:00 2001 From: Kami Date: Thu, 27 Jun 2024 13:22:52 +1000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Add=20Icon=20progress=20indicator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Loop/Localizable.xcstrings | 101 +++++++++++++- Loop/Luminare/Loop/AboutConfiguration.swift | 2 +- Loop/Luminare/Theming/IconConfiguration.swift | 129 ++++++++++++------ 3 files changed, 187 insertions(+), 45 deletions(-) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index abd80135..701035e8 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -336,6 +336,9 @@ } } } + }, + "%lld Loops left" : { + }, "99 problems, updates ain't one." : { @@ -728,6 +731,9 @@ } } } + }, + "As the loops accumulate, so too will your collection of icons." : { + }, "Beggars can't be... updaters." : { @@ -1653,7 +1659,7 @@ } }, "Default notification content" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1978,9 +1984,15 @@ } } } + }, + "Each loop you complete plants the seeds for icons to grow." : { + }, "Engage! ...in the current version, it's the latest." : { + }, + "Every loop brings you closer to the treasure that awaits." : { + }, "Excluded Apps" : { "localizations" : { @@ -2630,6 +2642,9 @@ } } } + }, + "Icon Locked" : { + }, "Icon Name: Black" : { "extractionState" : "extracted_with_value", @@ -3472,6 +3487,9 @@ }, "In a galaxy far, far away... still no updates!" : { + }, + "In due time, this icon shall be revealed to you." : { + }, "Include development versions" : { "localizations" : { @@ -3675,6 +3693,12 @@ }, "Just a small town app, same old version" : { + }, + "Keep looping, and this icon will be yours in no time." : { + + }, + "Keep up the good work, and this icon will be your reward." : { + }, "Keybindings" : { "localizations" : { @@ -3875,6 +3899,12 @@ } } } + }, + "Like the moon's phases, your icons will reveal themselves in cycles of loops." : { + + }, + "Locked" : { + }, "Loop" : { "extractionState" : "stale", @@ -3916,6 +3946,12 @@ } } } + }, + "Loop after loop, your dedication carves the key to success." : { + + }, + "Loop around the obstacles; your reward is just beyond them." : { + }, "Loop is in its prime!" : { @@ -4294,6 +4330,9 @@ } } } + }, + "Not yet, but you're closer than you were yesterday!" : { + }, "Nothing to cycle through" : { "localizations" : { @@ -4421,6 +4460,9 @@ } } } + }, + "OK" : { + }, "One does not simply update Loop." : { @@ -4507,6 +4549,12 @@ } } } + }, + "Patience is a virtue, and your key to this icon." : { + + }, + "Patience, young looper, this icon is not far away." : { + }, "Permissions" : { "localizations" : { @@ -6037,6 +6085,9 @@ } } } + }, + "Some icons are worth the wait, don't you think?" : { + }, "Stage Manager" : { "localizations" : { @@ -6117,6 +6168,9 @@ } } } + }, + "Stay curious, and soon this icon will be within your reach." : { + }, "Stay sharp, more intel coming soon!" : { "localizations" : { @@ -6400,6 +6454,15 @@ } } } + }, + "The icons are not just unlocked; they're earned, loop by loop." : { + + }, + "The icons await, hidden behind the veil of loops yet to be made." : { + + }, + "The journey of a thousand loops begins with a single step." : { + }, "The odds are ever in your favor, no updates today!" : { @@ -6486,6 +6549,9 @@ } } } + }, + "Think of each loop as a riddle, solving the mystery of the locked icon." : { + }, "This app is more up to date than my diary entries!" : { @@ -6529,6 +6595,18 @@ } } } + }, + "This icon is like a fine wine, it needs more time." : { + + }, + "This icon is locked, but your potential is not!" : { + + }, + "This icon is reserved for the most dedicated loopers." : { + + }, + "This icon is still under wraps, stay tuned!" : { + }, "This is not the update you're looking for!" : { "localizations" : { @@ -6729,6 +6807,9 @@ } } } + }, + "Unlocking this icon is just a matter of time and loops." : { + }, "Updates? In this economy?" : { @@ -7035,6 +7116,9 @@ }, "We've misplaced the 'Update' button. Oops!" : { + }, + "Who do you think you are, trying to access these top secret icons?" : { + }, "Width" : { "localizations" : { @@ -9440,6 +9524,9 @@ }, "Winter is coming. Updates aren't yet." : { + }, + "With each loop, the lock on this icon weakens." : { + }, "X" : { "localizations" : { @@ -9600,6 +9687,9 @@ } } } + }, + "You don’t have that yet!" : { + }, "You're cruising on the latest tech!" : { @@ -9806,9 +9896,18 @@ } } } + }, + "You've looped... uhh... I... lost count..." : { + + }, + "Your journey is not yet complete, this icon awaits at the end." : { + }, "Your Loop is loopier than ever, no updates found!" : { + }, + "Your persistence in looping is the master key to all icons." : { + } }, "version" : "1.0" diff --git a/Loop/Luminare/Loop/AboutConfiguration.swift b/Loop/Luminare/Loop/AboutConfiguration.swift index 9ce5d8d8..157f0638 100644 --- a/Loop/Luminare/Loop/AboutConfiguration.swift +++ b/Loop/Luminare/Loop/AboutConfiguration.swift @@ -166,7 +166,7 @@ struct AboutConfigurationView: View { Text( model.isHoveringOverVersionCopier ? "Version \(Bundle.main.appVersion ?? "Unknown") (\(Bundle.main.appBuild ?? 0))" - : "You've looped \(timesLooped) times!" + : (timesLooped >= 1_000_000 ? "You've looped... uhh... I... lost count..." : "You've looped \(timesLooped) times!") ) .contentTransition(.numericText(countsDown: !model.isHoveringOverVersionCopier)) .animation(.smooth(duration: 0.25), value: model.isHoveringOverVersionCopier) diff --git a/Loop/Luminare/Theming/IconConfiguration.swift b/Loop/Luminare/Theming/IconConfiguration.swift index 9a93a5da..9c3f1038 100644 --- a/Loop/Luminare/Theming/IconConfiguration.swift +++ b/Loop/Luminare/Theming/IconConfiguration.swift @@ -10,53 +10,46 @@ import Luminare import SwiftUI class IconConfigurationModel: ObservableObject { - let suggestNewIconLink = URL(string: "https://github.com/MrKai77/Loop/issues/new/choose")! + static let suggestNewIconLink = URL(string: "https://github.com/MrKai77/Loop/issues/new/choose")! - @Published var currentIcon = Defaults[.currentIcon] { + @Published var currentIcon: String = Defaults[.currentIcon] { didSet { - Defaults[.currentIcon] = currentIcon - - DispatchQueue.main.async { + if oldValue != currentIcon { + Defaults[.currentIcon] = currentIcon IconManager.refreshCurrentAppIcon() } } } - @Published var showDockIcon = Defaults[.showDockIcon] { + @Published var showDockIcon: Bool = Defaults[.showDockIcon] { didSet { - Defaults[.showDockIcon] = showDockIcon + if oldValue != showDockIcon { + Defaults[.showDockIcon] = showDockIcon + } } } - @Published var notificationWhenIconUnlocked = Defaults[.notificationWhenIconUnlocked] { + @Published var notificationWhenIconUnlocked: Bool = Defaults[.notificationWhenIconUnlocked] { didSet { - Defaults[.notificationWhenIconUnlocked] = notificationWhenIconUnlocked - - if notificationWhenIconUnlocked { - let notficationBody: String = .init( - localized: .init( - "Default notification content", - defaultValue: "You will now be notified when you unlock a new icon." - ) - ) - AppDelegate.sendNotification(Bundle.main.appName, notficationBody) - - let areNotificationsEnabled = AppDelegate.areNotificationsEnabled() + if oldValue != notificationWhenIconUnlocked { + Defaults[.notificationWhenIconUnlocked] = notificationWhenIconUnlocked + handleNotificationChange() + } + } + } - if !areNotificationsEnabled { - notificationWhenIconUnlocked = false - userDisabledNotificationsAlert() - } + private func handleNotificationChange() { + if notificationWhenIconUnlocked { + AppDelegate.sendNotification(Bundle.main.appName, "You will now be notified when you unlock a new icon.") + if !AppDelegate.areNotificationsEnabled() { + notificationWhenIconUnlocked = false + userDisabledNotificationsAlert() } } } private func userDisabledNotificationsAlert() { - guard - let window = LuminareManager.window - else { - return - } + guard let window = LuminareManager.window else { return } let alert = NSAlert() alert.messageText = "\(Bundle.main.appName)'s notification permissions are currently disabled." alert.informativeText = "Please turn them on in System Settings." @@ -65,29 +58,61 @@ class IconConfigurationModel: ObservableObject { alert.beginSheetModal(for: window) { modalResponse in if modalResponse == .alertFirstButtonReturn { - NSWorkspace.shared.open( - URL(string: "x-apple.systempreferences:com.apple.Notifications-Settings.extension")! - ) + NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.Notifications-Settings.extension")!) } } } + + func nextIconUnlockLoopCount(timesLooped: Int) -> Int? { + Icon.all.first { $0.unlockTime > timesLooped }?.unlockTime + } } struct IconConfigurationView: View { @Environment(\.openURL) var openURL @StateObject private var model = IconConfigurationModel() + @Default(.timesLooped) var timesLooped + @State private var showingLockedAlert = false + @State private var selectedLockedMessage = LocalizedStringKey("") + + let lockedMessages: [LocalizedStringKey] = [ + "You don’t have that yet!", + "Who do you think you are, trying to access these top secret icons?", + "Patience is a virtue, and your key to this icon.", + "This icon is locked, but your potential is not!", + "Keep looping, and this icon will be yours in no time.", + "This icon is still under wraps, stay tuned!", + "Some icons are worth the wait, don't you think?", + "Not yet, but you're closer than you were yesterday!", + "Unlocking this icon is just a matter of time and loops.", + "This icon is like a fine wine, it needs more time.", + "Stay curious, and soon this icon will be within your reach.", + "Keep up the good work, and this icon will be your reward.", + "This icon is reserved for the most dedicated loopers.", + "Your journey is not yet complete, this icon awaits at the end.", + "In due time, this icon shall be revealed to you.", + "Patience, young looper, this icon is not far away.", + "The journey of a thousand loops begins with a single step.", + "Every loop brings you closer to the treasure that awaits.", + "With each loop, the lock on this icon weakens.", + "Loop after loop, your dedication carves the key to success.", + "The icons are not just unlocked; they're earned, loop by loop.", + "As the loops accumulate, so too will your collection of icons.", + "Think of each loop as a riddle, solving the mystery of the locked icon.", + "Your persistence in looping is the master key to all icons.", + "Loop around the obstacles; your reward is just beyond them.", + "Each loop you complete plants the seeds for icons to grow.", + "Like the moon's phases, your icons will reveal themselves in cycles of loops.", + "The icons await, hidden behind the veil of loops yet to be made." + ] var body: some View { LuminareSection(showDividers: false) { LuminarePicker( elements: Icon.all, selection: Binding( - get: { - IconManager.currentAppIcon - }, - set: { - model.currentIcon = $0.iconName - } + get: { IconManager.currentAppIcon }, + set: { model.currentIcon = $0.iconName } ), roundBottom: false ) { icon in @@ -100,21 +125,39 @@ struct IconConfigurationView: View { } else { VStack(alignment: .center) { Spacer() - Image(._18PxLock) - + if let nextUnlockCount = model.nextIconUnlockLoopCount(timesLooped: timesLooped), + nextUnlockCount == icon.unlockTime { + Text("\(nextUnlockCount - timesLooped) Loops left") + .font(.caption) + .foregroundColor(.secondary) + } else { + Text("Locked") + .font(.caption) + .foregroundColor(.secondary) + } Spacer() } + .contentShape(Rectangle()) + .onTapGesture { + selectedLockedMessage = lockedMessages.randomElement() ?? "" + showingLockedAlert = true + } } } .aspectRatio(1, contentMode: .fit) + .alert(isPresented: $showingLockedAlert) { + Alert( + title: Text("Icon Locked"), + message: Text(selectedLockedMessage), + dismissButton: .default(Text("OK")) + ) + } } - Button("Suggest new icon") { - openURL(model.suggestNewIconLink) + openURL(IconConfigurationModel.suggestNewIconLink) } } - LuminareSection("Options") { LuminareToggle("Show in dock", isOn: $model.showDockIcon) LuminareToggle("Notify when unlocking new icons", isOn: $model.notificationWhenIconUnlocked)