From 64012be007f6556bebe5b8574f51a85420659384 Mon Sep 17 00:00:00 2001 From: Kai Azim Date: Wed, 26 Jun 2024 23:13:46 -0600 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Animations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TODO: check that this is localization-ready --- Loop/Localizable.xcstrings | 11 +- Loop/Luminare/Theming/IconConfiguration.swift | 171 ++++++++++-------- 2 files changed, 108 insertions(+), 74 deletions(-) diff --git a/Loop/Localizable.xcstrings b/Loop/Localizable.xcstrings index 701035e8..dc9440ac 100644 --- a/Loop/Localizable.xcstrings +++ b/Loop/Localizable.xcstrings @@ -337,8 +337,15 @@ } } }, - "%lld Loops left" : { - + "%lld %@" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "%1$lld %2$@" + } + } + } }, "99 problems, updates ain't one." : { diff --git a/Loop/Luminare/Theming/IconConfiguration.swift b/Loop/Luminare/Theming/IconConfiguration.swift index 9c3f1038..ec2b6adf 100644 --- a/Loop/Luminare/Theming/IconConfiguration.swift +++ b/Loop/Luminare/Theming/IconConfiguration.swift @@ -38,6 +38,39 @@ class IconConfigurationModel: ObservableObject { } } + @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.") @@ -63,48 +96,14 @@ class IconConfigurationModel: ObservableObject { } } - func nextIconUnlockLoopCount(timesLooped: Int) -> Int? { - Icon.all.first { $0.unlockTime > timesLooped }?.unlockTime + func nextIconUnlockLoopCount(timesLooped: Int) -> Int { + Icon.all.first { $0.unlockTime > timesLooped }?.unlockTime ?? 0 } } 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) { @@ -116,43 +115,15 @@ struct IconConfigurationView: View { ), 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) - 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 - } + 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) - .alert(isPresented: $showingLockedAlert) { - Alert( - title: Text("Icon Locked"), - message: Text(selectedLockedMessage), - dismissButton: .default(Text("OK")) - ) - } } Button("Suggest new icon") { openURL(IconConfigurationModel.suggestNewIconLink) @@ -164,3 +135,59 @@ struct IconConfigurationView: View { } } } + +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 { + Group { + if hasBeenUnlocked { + Image(nsImage: NSImage(named: icon.iconName)!) + .resizable() + .aspectRatio(contentMode: .fit) + .padding(10) + } else { + VStack(alignment: .center) { + Spacer() + Image(._18PxLock) + Text(nextUnlockCount == icon.unlockTime ? "\(loopsLeft) \(loopsLeft > 1 ? "Loops left" : "Loop left")" : "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 + } + } + } + } +}