feat: Add support for selecting custom emojis for Power Mode

This commit is contained in:
Beingpax 2025-06-05 22:26:59 +05:45
parent eb46afeb72
commit 8024146b61
5 changed files with 318 additions and 34 deletions

View File

@ -0,0 +1,53 @@
import Foundation
class EmojiManager: ObservableObject {
static let shared = EmojiManager()
private let defaultEmojis = ["🏢", "🏠", "💼", "🎮", "📱", "📺", "🎵", "📚", "✏️", "🎨", "🧠", "⚙️", "💻", "🌐", "📝", "📊", "🔍", "💬", "📈", "🔧"]
private let customEmojisKey = "userAddedEmojis"
@Published var customEmojis: [String] = []
private init() {
loadCustomEmojis()
}
var allEmojis: [String] {
return defaultEmojis + customEmojis
}
func addCustomEmoji(_ emoji: String) -> Bool {
let trimmedEmoji = emoji.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedEmoji.isEmpty, !allEmojis.contains(trimmedEmoji) else {
return false
}
customEmojis.append(trimmedEmoji)
saveCustomEmojis()
return true
}
private func loadCustomEmojis() {
if let savedEmojis = UserDefaults.standard.array(forKey: customEmojisKey) as? [String] {
customEmojis = savedEmojis
}
}
private func saveCustomEmojis() {
UserDefaults.standard.set(customEmojis, forKey: customEmojisKey)
}
func removeCustomEmoji(_ emoji: String) -> Bool {
if let index = customEmojis.firstIndex(of: emoji) {
customEmojis.remove(at: index)
saveCustomEmojis()
return true
}
return false
}
func isCustomEmoji(_ emoji: String) -> Bool {
return customEmojis.contains(emoji)
}
}

View File

@ -0,0 +1,237 @@
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 keyboard.")
.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

View File

@ -7,12 +7,12 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
var appConfigs: [AppConfig]?
var urlConfigs: [URLConfig]?
var isAIEnhancementEnabled: Bool
var selectedPrompt: String? // UUID string of the selected prompt
var selectedWhisperModel: String? // Name of the selected Whisper model
var selectedLanguage: String? // Language code (e.g., "en", "fr")
var selectedPrompt: String?
var selectedWhisperModel: String?
var selectedLanguage: String?
var useScreenCapture: Bool
var selectedAIProvider: String? // AI provider name (e.g., "OpenAI", "Gemini")
var selectedAIModel: String? // AI model name (e.g., "gpt-4", "gemini-1.5-pro")
var selectedAIProvider: String?
var selectedAIModel: String?
init(id: UUID = UUID(), name: String, emoji: String, appConfigs: [AppConfig]? = nil,
urlConfigs: [URLConfig]? = nil, isAIEnhancementEnabled: Bool, selectedPrompt: String? = nil,
@ -28,8 +28,6 @@ struct PowerModeConfig: Codable, Identifiable, Equatable {
self.useScreenCapture = useScreenCapture
self.selectedAIProvider = selectedAIProvider ?? UserDefaults.standard.string(forKey: "selectedAIProvider")
self.selectedAIModel = selectedAIModel
// Use provided values or get from UserDefaults if nil
self.selectedWhisperModel = selectedWhisperModel ?? UserDefaults.standard.string(forKey: "CurrentModel")
self.selectedLanguage = selectedLanguage ?? UserDefaults.standard.string(forKey: "SelectedLanguage") ?? "en"
}
@ -270,4 +268,11 @@ class PowerModeManager: ObservableObject {
func getAllAvailableConfigurations() -> [PowerModeConfig] {
return [defaultConfig] + configurations
}
func isEmojiInUse(_ emoji: String) -> Bool {
if defaultConfig.emoji == emoji {
return true
}
return configurations.contains { $0.emoji == emoji }
}
}

View File

@ -157,6 +157,12 @@ struct ConfigurationView: View {
.buttonStyle(.plain)
.disabled(mode.isEditingDefault)
.opacity(mode.isEditingDefault ? 0.5 : 1)
.popover(isPresented: $isShowingEmojiPicker, arrowEdge: .bottom) {
EmojiPickerView(
selectedEmoji: $selectedEmoji,
isPresented: $isShowingEmojiPicker
)
}
TextField("Name your power mode", text: $configName)
.font(.system(size: 18, weight: .bold))
@ -176,31 +182,14 @@ struct ConfigurationView: View {
.background(CardBackground(isSelected: false))
.padding(.horizontal)
// Emoji Picker Overlay
if isShowingEmojiPicker {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 40))], spacing: 12) {
ForEach(commonEmojis, id: \.self) { emoji in
Button(action: {
selectedEmoji = emoji
isShowingEmojiPicker = false
}) {
Text(emoji)
.font(.system(size: 22))
.frame(width: 40, height: 40)
.background(
Circle()
.fill(selectedEmoji == emoji ?
Color.accentColor.opacity(0.15) :
Color.clear)
)
}
.buttonStyle(.plain)
}
}
.padding(16)
.background(CardBackground(isSelected: false))
.padding(.horizontal)
}
// Enhanced Emoji Picker with Custom Emoji Support
// if isShowingEmojiPicker { // <<< This conditional block will be removed
// EmojiPickerView(
// selectedEmoji: $selectedEmoji,
// isPresented: $isShowingEmojiPicker
// )
// .padding(.horizontal)
// }
// SECTION 1: TRIGGERS
if !mode.isEditingDefault {

View File

@ -70,10 +70,10 @@ class Recorder: ObservableObject {
if deviceID != 0 {
do {
try await configureAudioSession(with: deviceID)
try? await Task.sleep(nanoseconds: 200_000_000)
try? await Task.sleep(nanoseconds: 50_000_000)
} catch {
logger.warning("⚠️ Failed to configure audio session for device \(deviceID), attempting to continue: \(error.localizedDescription)")
try? await Task.sleep(nanoseconds: 100_000_000)
try? await Task.sleep(nanoseconds: 50_000_000)
}
}