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() .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) .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) .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