Fix UI issues: preview sizing, debounce, corners, color picker
Fixes: 1. Camera preview rounded corners - clipShape now applied before padding so the preview itself has rounded corners, not the container 2. Debounced slider saves - ringSize and customColor now use debouncing - Immediate UI update via cached values - 300ms debounce before cloud save - Prevents excessive save operations during slider drag 3. Simplified custom color picker to one-step - ColorPicker styled as a circle button - Shows current custom color always (no rainbow) - Tapping opens iOS native color picker directly - Color applies immediately on selection - No Apply/Cancel sheet needed
This commit is contained in:
parent
95377c5950
commit
9066635a4d
@ -120,16 +120,16 @@ struct ContentView: View {
|
|||||||
isMirrorFlipped: settings.isMirrorFlipped,
|
isMirrorFlipped: settings.isMirrorFlipped,
|
||||||
zoomFactor: settings.currentZoomFactor
|
zoomFactor: settings.currentZoomFactor
|
||||||
)
|
)
|
||||||
.padding(ringSize)
|
|
||||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
||||||
|
.padding(ringSize)
|
||||||
.animation(.easeInOut(duration: Design.Animation.quick), value: ringSize)
|
.animation(.easeInOut(duration: Design.Animation.quick), value: ringSize)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Show placeholder while requesting permission
|
// Show placeholder while requesting permission
|
||||||
Rectangle()
|
Rectangle()
|
||||||
.fill(.black)
|
.fill(.black)
|
||||||
.padding(ringSize)
|
|
||||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.large))
|
||||||
|
.padding(ringSize)
|
||||||
.animation(.easeInOut(duration: Design.Animation.quick), value: ringSize)
|
.animation(.easeInOut(duration: Design.Animation.quick), value: ringSize)
|
||||||
.overlay {
|
.overlay {
|
||||||
if viewModel.captureSession == nil {
|
if viewModel.captureSession == nil {
|
||||||
|
|||||||
@ -5,8 +5,6 @@ struct SettingsView: View {
|
|||||||
@Bindable var viewModel: SettingsViewModel
|
@Bindable var viewModel: SettingsViewModel
|
||||||
@Binding var showPaywall: Bool
|
@Binding var showPaywall: Bool
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@State private var showColorPicker = false
|
|
||||||
@State private var tempCustomColor: Color = .white
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
@ -165,30 +163,17 @@ struct SettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom color button (premium)
|
// Custom color picker (premium) - one-step: opens picker, selects on change
|
||||||
CustomColorButton(
|
CustomColorPickerButton(
|
||||||
currentColor: viewModel.customColor,
|
customColor: Binding(
|
||||||
|
get: { viewModel.customColor },
|
||||||
|
set: { viewModel.selectCustomColor($0) }
|
||||||
|
),
|
||||||
isSelected: viewModel.isCustomColorSelected
|
isSelected: viewModel.isCustomColorSelected
|
||||||
) {
|
)
|
||||||
tempCustomColor = viewModel.customColor
|
|
||||||
showColorPicker = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, Design.Spacing.xSmall)
|
.padding(.vertical, Design.Spacing.xSmall)
|
||||||
.sheet(isPresented: $showColorPicker) {
|
|
||||||
CustomColorPickerSheet(
|
|
||||||
selectedColor: $tempCustomColor,
|
|
||||||
onApply: {
|
|
||||||
viewModel.selectCustomColor(tempCustomColor)
|
|
||||||
showColorPicker = false
|
|
||||||
},
|
|
||||||
onCancel: {
|
|
||||||
showColorPicker = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.presentationDetents([.medium])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Timer Picker
|
// MARK: - Timer Picker
|
||||||
@ -368,136 +353,58 @@ private struct ColorPresetButton: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Custom Color Button
|
// MARK: - Custom Color Picker Button
|
||||||
|
|
||||||
private struct CustomColorButton: View {
|
/// One-step custom color picker: always shows current color, tapping opens iOS color picker
|
||||||
let currentColor: Color
|
private struct CustomColorPickerButton: View {
|
||||||
|
@Binding var customColor: Color
|
||||||
let isSelected: Bool
|
let isSelected: Bool
|
||||||
let action: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: action) {
|
VStack(spacing: Design.Spacing.xxSmall) {
|
||||||
VStack(spacing: Design.Spacing.xxSmall) {
|
// ColorPicker styled as a circle
|
||||||
// Rainbow gradient circle to indicate custom picker
|
ColorPicker(
|
||||||
ZStack {
|
selection: $customColor,
|
||||||
// Show rainbow gradient when not selected, custom color when selected
|
supportsOpacity: false
|
||||||
if isSelected {
|
) {
|
||||||
Circle()
|
EmptyView()
|
||||||
.fill(currentColor)
|
|
||||||
} else {
|
|
||||||
Circle()
|
|
||||||
.fill(
|
|
||||||
AngularGradient(
|
|
||||||
colors: [.red, .orange, .yellow, .green, .blue, .purple, .red],
|
|
||||||
center: .center
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Circle()
|
|
||||||
.strokeBorder(
|
|
||||||
isSelected ? Color.Accent.primary : Color.Border.subtle,
|
|
||||||
lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall)
|
|
||||||
.shadow(
|
|
||||||
color: currentColor.opacity(Design.Opacity.light),
|
|
||||||
radius: isSelected ? Design.Shadow.radiusSmall : 0
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(String(localized: "Custom"))
|
|
||||||
.font(.system(size: Design.BaseFontSize.xSmall))
|
|
||||||
.foregroundStyle(.white.opacity(isSelected ? 1.0 : Design.Opacity.accent))
|
|
||||||
.lineLimit(1)
|
|
||||||
.minimumScaleFactor(Design.MinScaleFactor.tight)
|
|
||||||
|
|
||||||
Image(systemName: "crown.fill")
|
|
||||||
.font(.system(size: Design.BaseFontSize.xxSmall))
|
|
||||||
.foregroundStyle(Color.Status.warning)
|
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.xSmall)
|
.labelsHidden()
|
||||||
.background(
|
.frame(width: Design.Size.avatarSmall, height: Design.Size.avatarSmall)
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
|
.clipShape(.circle)
|
||||||
.fill(isSelected ? Color.Accent.primary.opacity(Design.Opacity.subtle) : Color.clear)
|
.overlay(
|
||||||
|
Circle()
|
||||||
|
.strokeBorder(
|
||||||
|
isSelected ? Color.Accent.primary : Color.Border.subtle,
|
||||||
|
lineWidth: isSelected ? Design.LineWidth.thick : Design.LineWidth.thin
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
.shadow(
|
||||||
|
color: customColor.opacity(Design.Opacity.light),
|
||||||
|
radius: isSelected ? Design.Shadow.radiusSmall : 0
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(String(localized: "Custom"))
|
||||||
|
.font(.system(size: Design.BaseFontSize.xSmall))
|
||||||
|
.foregroundStyle(.white.opacity(isSelected ? 1.0 : Design.Opacity.accent))
|
||||||
|
.lineLimit(1)
|
||||||
|
.minimumScaleFactor(Design.MinScaleFactor.tight)
|
||||||
|
|
||||||
|
Image(systemName: "crown.fill")
|
||||||
|
.font(.system(size: Design.BaseFontSize.xxSmall))
|
||||||
|
.foregroundStyle(Color.Status.warning)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.padding(Design.Spacing.xSmall)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
|
||||||
|
.fill(isSelected ? Color.Accent.primary.opacity(Design.Opacity.subtle) : Color.clear)
|
||||||
|
)
|
||||||
.accessibilityLabel(String(localized: "Custom color"))
|
.accessibilityLabel(String(localized: "Custom color"))
|
||||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||||
.accessibilityHint(String(localized: "Opens color picker. Premium feature."))
|
.accessibilityHint(String(localized: "Opens color picker. Premium feature."))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Custom Color Picker Sheet
|
|
||||||
|
|
||||||
private struct CustomColorPickerSheet: View {
|
|
||||||
@Binding var selectedColor: Color
|
|
||||||
let onApply: () -> Void
|
|
||||||
let onCancel: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationStack {
|
|
||||||
VStack(spacing: Design.Spacing.large) {
|
|
||||||
// Color preview
|
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
||||||
.fill(selectedColor)
|
|
||||||
.frame(height: 120)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
|
|
||||||
.strokeBorder(Color.Border.subtle, lineWidth: Design.LineWidth.thin)
|
|
||||||
)
|
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
|
||||||
.padding(.top, Design.Spacing.medium)
|
|
||||||
|
|
||||||
// SwiftUI ColorPicker
|
|
||||||
ColorPicker(
|
|
||||||
selection: $selectedColor,
|
|
||||||
supportsOpacity: false
|
|
||||||
) {
|
|
||||||
Text(String(localized: "Select Color"))
|
|
||||||
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
|
||||||
|
|
||||||
// Tips
|
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
|
|
||||||
Label(
|
|
||||||
String(localized: "Lighter colors work best as ring lights"),
|
|
||||||
systemImage: "lightbulb.fill"
|
|
||||||
)
|
|
||||||
.font(.system(size: Design.BaseFontSize.caption))
|
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.horizontal, Design.Spacing.large)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.background(Color.Surface.overlay)
|
|
||||||
.navigationTitle(String(localized: "Custom Color"))
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
.toolbar {
|
|
||||||
ToolbarItem(placement: .topBarLeading) {
|
|
||||||
Button(String(localized: "Cancel")) {
|
|
||||||
onCancel()
|
|
||||||
}
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
}
|
|
||||||
|
|
||||||
ToolbarItem(placement: .topBarTrailing) {
|
|
||||||
Button(String(localized: "Apply")) {
|
|
||||||
onApply()
|
|
||||||
}
|
|
||||||
.foregroundStyle(Color.Accent.primary)
|
|
||||||
.bold()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false))
|
SettingsView(viewModel: SettingsViewModel(), showPaywall: .constant(false))
|
||||||
.preferredColorScheme(.dark)
|
.preferredColorScheme(.dark)
|
||||||
|
|||||||
@ -87,12 +87,27 @@ final class SettingsViewModel: RingLightConfigurable {
|
|||||||
/// Manages iCloud sync for settings across all devices
|
/// Manages iCloud sync for settings across all devices
|
||||||
private let cloudSync = CloudSyncManager<SyncedSettings>()
|
private let cloudSync = CloudSyncManager<SyncedSettings>()
|
||||||
|
|
||||||
|
/// Debounce task for slider values
|
||||||
|
private var debounceTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
/// Debounce delay for continuous slider updates (in seconds)
|
||||||
|
private static let debounceDelay: Duration = .milliseconds(300)
|
||||||
|
|
||||||
|
/// Cached ring size for immediate UI updates (before debounced save)
|
||||||
|
private var _cachedRingSize: CGFloat?
|
||||||
|
|
||||||
// MARK: - Observable Properties (Synced)
|
// MARK: - Observable Properties (Synced)
|
||||||
|
|
||||||
/// Ring border size in points
|
/// Ring border size in points (debounced save)
|
||||||
var ringSize: CGFloat {
|
var ringSize: CGFloat {
|
||||||
get { cloudSync.data.ringSize }
|
get { _cachedRingSize ?? cloudSync.data.ringSize }
|
||||||
set { updateSettings { $0.ringSize = newValue } }
|
set {
|
||||||
|
_cachedRingSize = newValue
|
||||||
|
debouncedSave(key: "ringSize") {
|
||||||
|
self._cachedRingSize = nil
|
||||||
|
self.updateSettings { $0.ringSize = newValue }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ID of the selected light color preset
|
/// ID of the selected light color preset
|
||||||
@ -101,21 +116,28 @@ final class SettingsViewModel: RingLightConfigurable {
|
|||||||
set { updateSettings { $0.lightColorId = newValue } }
|
set { updateSettings { $0.lightColorId = newValue } }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Custom color for ring light (premium feature)
|
/// Cached custom color for immediate UI updates
|
||||||
|
private var _cachedCustomColor: Color?
|
||||||
|
|
||||||
|
/// Custom color for ring light (premium feature, debounced save)
|
||||||
var customColor: Color {
|
var customColor: Color {
|
||||||
get {
|
get {
|
||||||
Color(
|
_cachedCustomColor ?? Color(
|
||||||
red: cloudSync.data.customColorRed,
|
red: cloudSync.data.customColorRed,
|
||||||
green: cloudSync.data.customColorGreen,
|
green: cloudSync.data.customColorGreen,
|
||||||
blue: cloudSync.data.customColorBlue
|
blue: cloudSync.data.customColorBlue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
|
_cachedCustomColor = newValue
|
||||||
let rgb = CustomColorRGB(from: newValue)
|
let rgb = CustomColorRGB(from: newValue)
|
||||||
updateSettings {
|
debouncedSave(key: "customColor") {
|
||||||
$0.customColorRed = rgb.red
|
self._cachedCustomColor = nil
|
||||||
$0.customColorGreen = rgb.green
|
self.updateSettings {
|
||||||
$0.customColorBlue = rgb.blue
|
$0.customColorRed = rgb.red
|
||||||
|
$0.customColorGreen = rgb.green
|
||||||
|
$0.customColorBlue = rgb.blue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -227,7 +249,7 @@ final class SettingsViewModel: RingLightConfigurable {
|
|||||||
|
|
||||||
// MARK: - Private Methods
|
// MARK: - Private Methods
|
||||||
|
|
||||||
/// Updates settings and saves to cloud
|
/// Updates settings and saves to cloud immediately
|
||||||
private func updateSettings(_ transform: (inout SyncedSettings) -> Void) {
|
private func updateSettings(_ transform: (inout SyncedSettings) -> Void) {
|
||||||
cloudSync.update { settings in
|
cloudSync.update { settings in
|
||||||
transform(&settings)
|
transform(&settings)
|
||||||
@ -235,6 +257,21 @@ final class SettingsViewModel: RingLightConfigurable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Debounces save operations for continuous values like sliders
|
||||||
|
private func debouncedSave(key: String, action: @escaping () -> Void) {
|
||||||
|
// Cancel any pending debounce
|
||||||
|
debounceTask?.cancel()
|
||||||
|
|
||||||
|
// Schedule debounced save
|
||||||
|
debounceTask = Task {
|
||||||
|
try? await Task.sleep(for: Self.debounceDelay)
|
||||||
|
|
||||||
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Sync Actions
|
// MARK: - Sync Actions
|
||||||
|
|
||||||
/// Forces a sync with iCloud
|
/// Forces a sync with iCloud
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
"%lld percent" : {
|
|
||||||
"comment" : "The value of the slider is shown as a percentage.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"%lld points" : {
|
"%lld points" : {
|
||||||
"comment" : "The value of the ring size slider, displayed in parentheses.",
|
"comment" : "The value of the ring size slider, displayed in parentheses.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -29,10 +25,6 @@
|
|||||||
"comment" : "Description of a timer option when the user selects \"10 seconds\".",
|
"comment" : "Description of a timer option when the user selects \"10 seconds\".",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Adjusts the opacity/intensity of the ring light" : {
|
|
||||||
"comment" : "A description of the light intensity slider in the settings view.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Adjusts the size of the light ring around the camera preview" : {
|
"Adjusts the size of the light ring around the camera preview" : {
|
||||||
"comment" : "A description of the ring size slider in the settings view.",
|
"comment" : "A description of the ring size slider in the settings view.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -53,6 +45,10 @@
|
|||||||
"comment" : "Accessibility hint for the \"Skin Smoothing\" toggle in the Settings view.",
|
"comment" : "Accessibility hint for the \"Skin Smoothing\" toggle in the Settings view.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Apply" : {
|
||||||
|
"comment" : "The text for a button that applies the selected color.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Auto-Save" : {
|
"Auto-Save" : {
|
||||||
"comment" : "Title of a toggle that enables automatic saving of captured photos and videos to the user's Photo Library.",
|
"comment" : "Title of a toggle that enables automatic saving of captured photos and videos to the user's Photo Library.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -105,6 +101,18 @@
|
|||||||
"comment" : "Name of a ring light color preset.",
|
"comment" : "Name of a ring light color preset.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Custom" : {
|
||||||
|
"comment" : "A label displayed below the rainbow gradient circle in the custom color button.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Custom color" : {
|
||||||
|
"comment" : "An accessibility label for the custom color button.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Custom Color" : {
|
||||||
|
"comment" : "The title of a sheet where a user can select a custom color.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Debug mode: Purchase simulated!" : {
|
"Debug mode: Purchase simulated!" : {
|
||||||
"comment" : "Announcement posted to VoiceOver when a premium purchase is simulated in debug mode.",
|
"comment" : "Announcement posted to VoiceOver when a premium purchase is simulated in debug mode.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -152,12 +160,8 @@
|
|||||||
"comment" : "A label displayed above a section of the settings view related to light colors.",
|
"comment" : "A label displayed above a section of the settings view related to light colors.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Light intensity" : {
|
"Lighter colors work best as ring lights" : {
|
||||||
"comment" : "An accessibility label for the light intensity slider in the settings view. The value is dynamically set based on the slider's current value.",
|
"comment" : "A tip explaining that lighter colors are better for ring lights.",
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Light Intensity" : {
|
|
||||||
"comment" : "A label describing the slider that adjusts the intensity of the ring light.",
|
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"No Watermarks • Ad-Free" : {
|
"No Watermarks • Ad-Free" : {
|
||||||
@ -176,6 +180,14 @@
|
|||||||
"comment" : "A button label that opens the device settings when tapped.",
|
"comment" : "A button label that opens the device settings when tapped.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Opens color picker. Premium feature." : {
|
||||||
|
"comment" : "An accessibility hint for the custom color button, describing its function.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Opens upgrade options" : {
|
||||||
|
"comment" : "An accessibility hint for the \"Upgrade to Pro\" button that indicates it opens upgrade options.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Photo" : {
|
"Photo" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
@ -191,10 +203,6 @@
|
|||||||
"comment" : "An accessibility hint for a premium color option in the color preset button.",
|
"comment" : "An accessibility hint for a premium color option in the color preset button.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
"Pro unlocked" : {
|
|
||||||
"comment" : "An accessibility label for the \"crown.fill\" system icon when premium is unlocked.",
|
|
||||||
"isCommentAutoGenerated" : true
|
|
||||||
},
|
|
||||||
"Purchase successful! Pro features unlocked." : {
|
"Purchase successful! Pro features unlocked." : {
|
||||||
"comment" : "Announcement read out to the user when a premium purchase is successful.",
|
"comment" : "Announcement read out to the user when a premium purchase is successful.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -231,6 +239,10 @@
|
|||||||
"comment" : "Text shown as a toast message when a photo is successfully saved to Photos.",
|
"comment" : "Text shown as a toast message when a photo is successfully saved to Photos.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Select Color" : {
|
||||||
|
"comment" : "A label for the color picker in the custom color picker sheet.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Select self-timer duration" : {
|
"Select self-timer duration" : {
|
||||||
"comment" : "A label describing the segmented control for selecting the duration of the self-timer.",
|
"comment" : "A label describing the segmented control for selecting the duration of the self-timer.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
@ -337,6 +349,10 @@
|
|||||||
"comment" : "A teaser text that appears below the capture edit view, promoting a premium feature.",
|
"comment" : "A teaser text that appears below the capture edit view, promoting a premium feature.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"Unlock premium colors, video, and more" : {
|
||||||
|
"comment" : "A description of the benefits of upgrading to the Pro version of the app.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Upgrade to Pro" : {
|
"Upgrade to Pro" : {
|
||||||
"comment" : "A button label that prompts users to upgrade to the premium version of the app.",
|
"comment" : "A button label that prompts users to upgrade to the premium version of the app.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user