diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index abd80135..360505ca 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -728,6 +728,9 @@ } } } + }, + "As the loops accumulate, so too will your collection of icons." : { + }, "Beggars can't be... updaters." : { @@ -1653,7 +1656,7 @@ } }, "Default notification content" : { - "extractionState" : "extracted_with_value", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -1978,9 +1981,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 +2639,9 @@ } } } + }, + "Icon Locked" : { + }, "Icon Name: Black" : { "extractionState" : "extracted_with_value", @@ -3472,6 +3484,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 +3690,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" : { @@ -3876,6 +3897,12 @@ } } }, + "Like the moon's phases, your icons will reveal themselves in cycles of loops." : { + + }, + "Locked" : { + "comment" : "When an app icon is locked" + }, "Loop" : { "extractionState" : "stale", "localizations" : { @@ -3916,9 +3943,37 @@ } } } + }, + "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!" : { + }, + "Loops left to unlock a new icon" : { + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Loop left" + } + }, + "other" : { + "stringUnit" : { + "state" : "new", + "value" : "%lld Loops left" + } + } + } + } + } + } }, "May the Force be with you... next time!" : { "localizations" : { @@ -4294,6 +4349,9 @@ } } } + }, + "Not yet, but you're closer than you were yesterday!" : { + }, "Nothing to cycle through" : { "localizations" : { @@ -4421,6 +4479,9 @@ } } } + }, + "OK" : { + }, "One does not simply update Loop." : { @@ -4507,6 +4568,12 @@ } } } + }, + "Patience is a virtue, and your key to this icon." : { + + }, + "Patience, young looper, this icon is not far away." : { + }, "Permissions" : { "localizations" : { @@ -6037,6 +6104,9 @@ } } } + }, + "Some icons are worth the wait, don't you think?" : { + }, "Stage Manager" : { "localizations" : { @@ -6117,6 +6187,9 @@ } } } + }, + "Stay curious, and soon this icon will be within your reach." : { + }, "Stay sharp, more intel coming soon!" : { "localizations" : { @@ -6400,6 +6473,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 +6568,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 +6614,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 +6826,9 @@ } } } + }, + "Unlocking this icon is just a matter of time and loops." : { + }, "Updates? In this economy?" : { @@ -7035,6 +7135,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 +9543,9 @@ }, "Winter is coming. Updates aren't yet." : { + }, + "With each loop, the lock on this icon weakens." : { + }, "X" : { "localizations" : { @@ -9600,6 +9706,9 @@ } } } + }, + "You don’t have that yet!" : { + }, "You're cruising on the latest tech!" : { @@ -9806,9 +9915,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..ba12c131 100644 --- a/Loop/Luminare/Theming/IconConfiguration.swift +++ b/Loop/Luminare/Theming/IconConfiguration.swift @@ -10,53 +10,79 @@ 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() - } + @Published var showingLockedAlert = false + @Published var selectedLockedMessage: LocalizedStringKey = .init("") + 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." + ] + + 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,12 +91,14 @@ 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 ?? 0 + } } struct IconConfigurationView: View { @@ -82,42 +110,91 @@ struct IconConfigurationView: View { 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 - Group { - if icon.selectable { - Image(nsImage: NSImage(named: icon.iconName)!) - .resizable() - .aspectRatio(contentMode: .fit) - .padding(10) - } else { - VStack(alignment: .center) { - Spacer() - - Image(._18PxLock) - - Spacer() - } + IconVew(model: model, icon: icon) + .aspectRatio(1, contentMode: .fit) + .alert(isPresented: $model.showingLockedAlert) { + Alert( + title: Text("Icon Locked"), + message: Text(model.selectedLockedMessage), + dismissButton: .default(Text("OK")) + ) } - } - .aspectRatio(1, contentMode: .fit) } - 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) } } } + +struct IconVew: View { + @ObservedObject var model: IconConfigurationModel + let icon: Icon + + @State private var hasBeenUnlocked: Bool = false + @Default(.timesLooped) var timesLooped + @State private var nextUnlockCount: Int = -1 + @State private var loopsLeft: Int = -1 + + var body: some View { + ZStack { + if hasBeenUnlocked { + Image(nsImage: NSImage(named: icon.iconName)!) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(10) + .transition(.scale(scale: 0.8).combined(with: .opacity)) + } else { + VStack(alignment: .center) { + Spacer() + Image(._18PxLock) + .foregroundStyle(.secondary) + + Text(nextUnlockCount == icon.unlockTime ? + .init(localized: "Loops left to unlock a new icon", defaultValue: "\(loopsLeft) Loops left") : + .init(localized: "Locked", comment: "When an app icon is locked") + ) + .font(.caption) + .foregroundColor(.secondary) + .contentTransition(.numericText()) + + Spacer() + } + .contentShape(Rectangle()) + .onTapGesture { + model.selectedLockedMessage = model.lockedMessages.randomElement() ?? "" + model.showingLockedAlert = true + } + } + } + .onAppear { + hasBeenUnlocked = icon.selectable + + if !hasBeenUnlocked { + nextUnlockCount = model.nextIconUnlockLoopCount(timesLooped: timesLooped) + loopsLeft = nextUnlockCount - timesLooped + } + } + .onChange(of: timesLooped) { _ in + withAnimation(.smooth(duration: 0.25)) { + hasBeenUnlocked = icon.selectable + } + + if !hasBeenUnlocked { + withAnimation(.smooth(duration: 0.25)) { + nextUnlockCount = model.nextIconUnlockLoopCount(timesLooped: timesLooped) + loopsLeft = nextUnlockCount - timesLooped + } + } + } + } +}