vOOice/VoiceInk/PowerMode/EmojiPickerView.swift

237 lines
9.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
struct EmojiPickerView: View {
@StateObject private var emojiManager = EmojiManager.shared
@Binding var selectedEmoji: String
@Binding var isPresented: Bool
@State private var newEmojiText: String = ""
@State private var isAddingCustomEmoji: Bool = false
@FocusState private var isEmojiTextFieldFocused: Bool
@State private var inputFeedbackMessage: String = ""
@State private var showingEmojiInUseAlert = false
@State private var emojiForAlert: String? = nil
private let columns: [GridItem] = [GridItem(.adaptive(minimum: 44), spacing: 10)]
var body: some View {
VStack(spacing: 12) {
ScrollView {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(emojiManager.allEmojis, id: \.self) { emoji in
EmojiButton(
emoji: emoji,
isSelected: selectedEmoji == emoji,
isCustom: emojiManager.isCustomEmoji(emoji),
removeAction: {
attemptToRemoveCustomEmoji(emoji)
}
) {
selectedEmoji = emoji
inputFeedbackMessage = ""
isPresented = false
}
}
AddEmojiButton {
isAddingCustomEmoji.toggle()
newEmojiText = ""
inputFeedbackMessage = ""
if isAddingCustomEmoji {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
isEmojiTextFieldFocused = true
}
}
}
}
}
.frame(maxHeight: 200)
if isAddingCustomEmoji {
VStack(spacing: 8) {
HStack(spacing: 8) {
TextField("", text: $newEmojiText)
.textFieldStyle(.roundedBorder)
.font(.title2)
.multilineTextAlignment(.center)
.frame(maxWidth: 70)
.focused($isEmojiTextFieldFocused)
.onChange(of: newEmojiText) { _, newValue in
inputFeedbackMessage = ""
let cleaned = newValue.firstValidEmojiCharacter()
if newEmojiText != cleaned {
newEmojiText = cleaned
}
if !newEmojiText.isEmpty && emojiManager.allEmojis.contains(newEmojiText) {
inputFeedbackMessage = "Emoji already exists!"
} else if !newEmojiText.isEmpty && !newEmojiText.isValidEmoji {
inputFeedbackMessage = "Invalid emoji."
} else {
inputFeedbackMessage = ""
}
}
.onSubmit(attemptAddCustomEmoji)
Button("Add") {
attemptAddCustomEmoji()
}
.buttonStyle(.borderedProminent)
.disabled(newEmojiText.isEmpty || !newEmojiText.isValidEmoji || emojiManager.allEmojis.contains(newEmojiText))
Button("Cancel") {
isAddingCustomEmoji = false
newEmojiText = ""
inputFeedbackMessage = ""
}
.buttonStyle(.bordered)
}
if !inputFeedbackMessage.isEmpty {
Text(inputFeedbackMessage)
.font(.caption)
.foregroundColor(inputFeedbackMessage == "Emoji already exists!" || inputFeedbackMessage == "Invalid emoji." ? .red : .secondary)
.transition(.opacity)
}
Text("Tip: Use ⌃⌘Space for emoji picker.")
.font(.caption2)
.foregroundColor(.secondary)
.padding(.top, 2)
}
.padding(.horizontal)
.padding(.bottom, 5)
}
}
.padding()
.background(.regularMaterial)
.frame(minWidth: 260, idealWidth: 300, maxWidth: 320, minHeight: 150, idealHeight: 280, maxHeight: 350)
.alert("Emoji in Use", isPresented: $showingEmojiInUseAlert, presenting: emojiForAlert) { emojiStr in
Button("OK", role: .cancel) { }
} message: { emojiStr in
Text("The emoji \"\(emojiStr)\" is currently used by one or more Power Modes and cannot be removed.")
}
}
private func attemptAddCustomEmoji() {
let trimmedEmoji = newEmojiText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedEmoji.isEmpty else {
inputFeedbackMessage = "Emoji cannot be empty."
return
}
guard trimmedEmoji.isValidEmoji else {
inputFeedbackMessage = "Invalid emoji character."
return
}
guard !emojiManager.allEmojis.contains(trimmedEmoji) else {
inputFeedbackMessage = "Emoji already exists!"
return
}
if emojiManager.addCustomEmoji(trimmedEmoji) {
selectedEmoji = trimmedEmoji
inputFeedbackMessage = ""
isAddingCustomEmoji = false
newEmojiText = ""
} else {
inputFeedbackMessage = "Could not add emoji."
}
}
private func attemptToRemoveCustomEmoji(_ emojiToRemove: String) {
guard emojiManager.isCustomEmoji(emojiToRemove) else { return }
if PowerModeManager.shared.isEmojiInUse(emojiToRemove) {
emojiForAlert = emojiToRemove
showingEmojiInUseAlert = true
} else {
if emojiManager.removeCustomEmoji(emojiToRemove) {
if selectedEmoji == emojiToRemove {
}
}
}
}
}
private struct EmojiButton: View {
let emoji: String
let isSelected: Bool
let isCustom: Bool
let removeAction: () -> Void
let selectAction: () -> Void
var body: some View {
ZStack(alignment: .topTrailing) {
Button(action: selectAction) {
Text(emoji)
.font(.largeTitle)
.frame(width: 44, height: 44)
.background(
Circle()
.fill(isSelected ? Color.accentColor.opacity(0.25) : Color.clear)
)
.overlay(
Circle()
.strokeBorder(isSelected ? Color.accentColor : Color.gray.opacity(0.3), lineWidth: isSelected ? 2 : 1)
)
}
.buttonStyle(.plain)
if isCustom {
Button(action: removeAction) {
Image(systemName: "xmark.circle.fill")
.symbolRenderingMode(.palette)
.foregroundStyle(Color.white, Color.red)
.font(.caption2)
.background(Circle().fill(Color.white.opacity(0.8)))
}
.buttonStyle(.borderless)
.offset(x: 6, y: -6)
}
}
}
}
private struct AddEmojiButton: View {
let action: () -> Void
var body: some View {
Button(action: action) {
Label("Add Emoji", systemImage: "plus.circle.fill")
.font(.title2)
.labelStyle(.iconOnly)
.foregroundColor(.accentColor)
.frame(width: 44, height: 44)
.background(
Circle()
.fill(Color.secondary.opacity(0.1))
)
.overlay(
Circle()
.strokeBorder(Color.gray.opacity(0.3), lineWidth: 1)
)
}
.buttonStyle(.plain)
.help("Add custom emoji")
}
}
extension String {
var isValidEmoji: Bool {
guard !self.isEmpty else { return false }
return self.count == 1 && self.unicodeScalars.first?.properties.isEmoji ?? false
}
func firstValidEmojiCharacter() -> String {
return self.filter { $0.unicodeScalars.allSatisfy { $0.properties.isEmoji } }.prefix(1).map(String.init).joined()
}
}
#if DEBUG
struct EmojiPickerView_Previews: PreviewProvider {
static var previews: some View {
EmojiPickerView(
selectedEmoji: .constant("😀"),
isPresented: .constant(true)
)
.environmentObject(EmojiManager.shared)
}
}
#endif