From 7696e8d32382b2e8e4ff9b2eecb73e0c6c88fd47 Mon Sep 17 00:00:00 2001 From: "tattn (Tatsuya Tanaka)" Date: Wed, 15 Feb 2023 12:20:17 +0900 Subject: [PATCH] 0.9.0 --- .../Sources/VCamLocalization/Generated.swift | 20 +++- .../en.lproj/Localizable.strings | 12 +- .../ja.lproj/Localizable.strings | 14 ++- app/xcode/Sources/VCamUI/AppUpdater+UI.swift | 22 ++-- .../VCamUI/Extensions/NSWindowKey.swift | 19 +++ .../Sources/VCamUI/MacWindowManager.swift | 64 ++++++++++ .../Sources/VCamUI/RootContentView.swift | 29 +++-- .../VCamUI/Toolbar/VCamMainToolbar.swift | 110 ++++++++++++++++++ .../VCamMainToolbarBlendShapePicker.swift | 57 +++++++++ .../Toolbar/VCamMainToolbarEmojiPicker.swift | 110 ++++++++++++++++++ .../Toolbar/VCamMainToolbarMotionPicker.swift | 80 +++++++++++++ .../Toolbar/VCamMainToolbarPhotoPicker.swift | 60 ++++++++++ .../VCamUI/UIComponent/MacHoverEffect.swift | 31 +++++ .../UIComponent/NSWindow+presentWindow.swift | 30 ----- .../UIComponent/VCamPopoverContainer.swift | 37 ++++++ app/xcode/Sources/VCamUI/VCamMenu.swift | 15 --- 16 files changed, 637 insertions(+), 73 deletions(-) create mode 100644 app/xcode/Sources/VCamUI/Extensions/NSWindowKey.swift create mode 100644 app/xcode/Sources/VCamUI/MacWindowManager.swift create mode 100644 app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbar.swift create mode 100644 app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBlendShapePicker.swift create mode 100644 app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarEmojiPicker.swift create mode 100644 app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarMotionPicker.swift create mode 100644 app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarPhotoPicker.swift create mode 100644 app/xcode/Sources/VCamUI/UIComponent/MacHoverEffect.swift delete mode 100644 app/xcode/Sources/VCamUI/UIComponent/NSWindow+presentWindow.swift create mode 100644 app/xcode/Sources/VCamUI/UIComponent/VCamPopoverContainer.swift diff --git a/app/xcode/Sources/VCamLocalization/Generated.swift b/app/xcode/Sources/VCamLocalization/Generated.swift index 9b24c08..2e9a0d3 100644 --- a/app/xcode/Sources/VCamLocalization/Generated.swift +++ b/app/xcode/Sources/VCamLocalization/Generated.swift @@ -134,6 +134,8 @@ public enum L10n { public static let enable = LocalizedString(lookupKey: "enable") /// Convert VRM 0.x to VRM 1.x public static let enableAutoConvertingToVRM1 = LocalizedString(lookupKey: "enableAutoConvertingToVRM1") + /// Enable VSync + public static let enableVSync = LocalizedString(lookupKey: "enableVSync") /// English public static let english = LocalizedString(lookupKey: "english") /// Entire Display @@ -144,6 +146,14 @@ public enum L10n { public static func existsNewAppVersion(_ p1: Any) -> ArgumentsLocalizedString { ArgumentsLocalizedString("existsNewAppVersion %@", "existsNewAppVersion \(String(describing: p1))", String(describing: p1)) } + /// Experiment + public static let experiment = LocalizedString(lookupKey: "experiment") + /// The supporter version of the app can be downloaded from the following link: + public static let experimentAdvertisement = LocalizedString(lookupKey: "experimentAdvertisement") + /// These are experimental features and may have bugs. + public static let experimentDisclaimer = LocalizedString(lookupKey: "experimentDisclaimer") + /// These features are available for our supporters, thank you for your supporting! + public static let experimentThanks = LocalizedString(lookupKey: "experimentThanks") /// Horizontal sensitivity public static let eyesHorizontalSensitivity = LocalizedString(lookupKey: "eyesHorizontalSensitivity") /// Vertical sensitivity @@ -160,12 +170,14 @@ public enum L10n { public static let filter = LocalizedString(lookupKey: "filter") /// Finger public static let finger = LocalizedString(lookupKey: "finger") - /// Flip screen + /// Flip screen of the virtual camera public static let flipScreen = LocalizedString(lookupKey: "flipScreen") /// FPS (tracking) public static let fpsCamera = LocalizedString(lookupKey: "fpsCamera") /// FPS (screen) public static let fpsScreen = LocalizedString(lookupKey: "fpsScreen") + /// General + public static let general = LocalizedString(lookupKey: "general") /// Hand public static let hand = LocalizedString(lookupKey: "hand") /// Height @@ -252,6 +264,8 @@ public enum L10n { public static let object = LocalizedString(lookupKey: "object") /// Open preferences public static let openPreference = LocalizedString(lookupKey: "openPreference") + /// Open VCam + public static let openVCam = LocalizedString(lookupKey: "openVCam") /// Optimize meshes (Recommended) public static let optimizeMeshes = LocalizedString(lookupKey: "optimizeMeshes") /// Paste @@ -296,6 +310,8 @@ public enum L10n { public static let remove = LocalizedString(lookupKey: "remove") /// Remove VCam from the capture public static let removeVCamFromCapture = LocalizedString(lookupKey: "removeVCamFromCapture") + /// Rendering + public static let rendering = LocalizedString(lookupKey: "rendering") /// Rendering Quality public static let renderingQuality = LocalizedString(lookupKey: "renderingQuality") /// Reset Avatar Position @@ -382,7 +398,7 @@ public enum L10n { public static func useEmotionBy(_ p1: Any) -> ArgumentsLocalizedString { ArgumentsLocalizedString("useEmotionBy %@", "useEmotionBy \(String(describing: p1))", String(describing: p1)) } - /// Use vowel estimation by camera [β] + /// Use vowel estimation by camera public static let useVowelEstimation = LocalizedString(lookupKey: "useVowelEstimation") /// Video Capture public static let videoCapture = LocalizedString(lookupKey: "videoCapture") diff --git a/app/xcode/Sources/VCamLocalization/VCamResources/en.lproj/Localizable.strings b/app/xcode/Sources/VCamLocalization/VCamResources/en.lproj/Localizable.strings index 282c6f9..07c14b5 100644 --- a/app/xcode/Sources/VCamLocalization/VCamResources/en.lproj/Localizable.strings +++ b/app/xcode/Sources/VCamLocalization/VCamResources/en.lproj/Localizable.strings @@ -14,6 +14,7 @@ "done" = "Done"; "edit" = "Edit"; "pick" = "Pick"; +"general" = "General"; "apply" = "Apply"; "remove" = "Remove"; "duplicate" = "Duplicate"; @@ -100,7 +101,7 @@ "optimizeMeshes" = "Optimize meshes (Recommended)"; "enableAutoConvertingToVRM1" = "Convert VRM 0.x to VRM 1.x"; "helpMesh" = "Reduce the load on the app by merging meshes, etc. If you have problems viewing the model, turn it off and reload the model."; -"flipScreen" = "Flip screen"; +"flipScreen" = "Flip screen of the virtual camera"; "addToMacOSMenuBar" = "Add icon in macOS menu bar"; "background" = "Background"; "lipSyncSensitivity" = "Lip-sync sensitivity (mic)"; @@ -116,13 +117,19 @@ "qualityGood" = "Good"; "qualityBeautiful" = "Beautiful"; "qualityFantastic" = "Fantastic"; +"rendering" = "Rendering"; +"experiment" = "Experiment"; +"experimentDisclaimer" = "These are experimental features and may have bugs."; +"experimentThanks" = "These features are available for our supporters, thank you for your supporting!"; +"experimentAdvertisement" = "The supporter version of the app can be downloaded from the following link:"; +"enableVSync" = "Enable VSync"; // MARK: - Tracking "micOrCamera" = "Mic or Camera"; "helpCalibrate" = "If the movement is out of sync with the actual movement, press this button while facing forward to the camera and looking at the camera."; "useEmotionBy %@" = "Use emotion by %@ [β] (Use high power)"; -"useVowelEstimation" = "Use vowel estimation by camera [β]"; +"useVowelEstimation" = "Use vowel estimation by camera"; "isNotFound %@" = "%@ is not found"; "lipSync" = "Lip-sync"; "trackEyes" = "Track eyes"; @@ -225,6 +232,7 @@ "help" = "Help"; "anyProblem" = "Any problem?"; "quitVCam" = "Quit VCam"; +"openVCam" = "Open VCam"; "editAvatar" = "Edit Avatar"; "recalibrate" = "Recalibrate"; diff --git a/app/xcode/Sources/VCamLocalization/VCamResources/ja.lproj/Localizable.strings b/app/xcode/Sources/VCamLocalization/VCamResources/ja.lproj/Localizable.strings index a4b9823..cd53b61 100644 --- a/app/xcode/Sources/VCamLocalization/VCamResources/ja.lproj/Localizable.strings +++ b/app/xcode/Sources/VCamLocalization/VCamResources/ja.lproj/Localizable.strings @@ -14,6 +14,7 @@ "done" = "完了"; "edit" = "編集"; "pick" = "選択"; +"general" = "一般"; "apply" = "適用"; "remove" = "削除"; "duplicate" = "複製"; @@ -100,7 +101,7 @@ "optimizeMeshes" = "モデルのメッシュを最適化する (推奨)"; "enableAutoConvertingToVRM1" = "VRM0.x系のアバターを1.0系に自動変換する"; "helpMesh" = "メッシュの結合等によりアプリの負荷を大幅に下げられます。モデルの表示に問題がある場合はオフにした後、モデルを再読込してください。"; -"flipScreen" = "画面を左右反転する"; +"flipScreen" = "仮想カメラの画面を左右反転する"; "addToMacOSMenuBar" = "macOSのメニューバーに追加する"; "background" = "背景"; "lipSyncSensitivity" = "リップシンクの感度 (マイク)"; @@ -116,13 +117,19 @@ "qualityGood" = "良品質"; "qualityBeautiful" = "高品質"; "qualityFantastic" = "最高品質"; +"rendering" = "レンダリング"; +"experiment" = "実験的な機能"; +"experimentDisclaimer" = "これらは実験的な機能のため、不具合がある可能性があります。"; +"experimentThanks" = "これらの機能はサポーター向けに提供しています。VCamのサポート、ありがとうございます!"; +"experimentAdvertisement" = "これらの機能はサポーター向けに提供しています。サポーター版は以下からDLできます。"; +"enableVSync" = "VSyncを有効にする"; // MARK: - Tracking "micOrCamera" = "マイクまたはカメラ"; "helpCalibrate" = "実際の動きとずれている場合は、カメラに対して正面を向いた状態でカメラ目線でこのボタンを押してください"; "useEmotionBy %@" = "表情を%@で推定して反映する [β] (負荷増)"; -"useVowelEstimation" = "カメラで母音を推定する [β]"; +"useVowelEstimation" = "カメラで母音を推定する"; "isNotFound %@" = "%@が見つかりません"; "lipSync" = "リップシンク"; "trackEyes" = "目をトラッキングする"; @@ -224,7 +231,8 @@ "resetAvatarPosition" = "アバターの位置をリセット"; "help" = "ヘルプ"; "anyProblem" = "困った時は?"; -"quitVCam" = "VCam を終了"; +"quitVCam" = "VCamを終了"; +"openVCam" = "VCamを開く"; "editAvatar" = "アバターの編集"; "recalibrate" = "再キャリブレーション"; diff --git a/app/xcode/Sources/VCamUI/AppUpdater+UI.swift b/app/xcode/Sources/VCamUI/AppUpdater+UI.swift index 85c112b..51aec43 100644 --- a/app/xcode/Sources/VCamUI/AppUpdater+UI.swift +++ b/app/xcode/Sources/VCamUI/AppUpdater+UI.swift @@ -11,10 +11,11 @@ import VCamLocalization struct AppUpdateInformationView: View { let release: AppUpdater.LatestRelease - let window: NSWindow @AppStorage(key: .skipThisVersion) var skipThisVersion + @Environment(\.nsWindow) var nsWindow + var body: some View { VStack(alignment: .leading) { Text(L10n.existsNewAppVersion(release.version.description).key, bundle: .localize) @@ -35,7 +36,7 @@ struct AppUpdateInformationView: View { HStack { Button { skipThisVersion = release.version.description - window.close() + nsWindow?.close() } label: { Text(L10n.skipThisVersion.key, bundle: .localize) } @@ -62,9 +63,15 @@ struct AppUpdateInformationView: View { } } .padding() + .background(.thinMaterial) + .frame(width: 600, height: 400) } } +extension AppUpdateInformationView: MacWindow { + static var windowTitle: String { L10n.update.text } +} + extension AppUpdater { @MainActor public func presentUpdateAlert() async { @@ -77,10 +84,8 @@ extension AppUpdater { _ = alert.runModal() return } - presentWindow(title: L10n.update.text, id: nil, size: .init(width: 600, height: 400)) { window in - AppUpdateInformationView(release: release, window: window) - .background(.thinMaterial) - } + + MacWindowManager.shared.open(AppUpdateInformationView(release: release)) } @MainActor @@ -88,9 +93,6 @@ extension AppUpdater { guard let release = try? await check(), UserDefaults.standard.value(for: .skipThisVersion) < release.version else { return // already latest or error } - presentWindow(title: L10n.update.text, id: nil, size: .init(width: 600, height: 400)) { window in - AppUpdateInformationView(release: release, window: window) - .background(.thinMaterial) - } + MacWindowManager.shared.open(AppUpdateInformationView(release: release)) } } diff --git a/app/xcode/Sources/VCamUI/Extensions/NSWindowKey.swift b/app/xcode/Sources/VCamUI/Extensions/NSWindowKey.swift new file mode 100644 index 0000000..bb0ddd2 --- /dev/null +++ b/app/xcode/Sources/VCamUI/Extensions/NSWindowKey.swift @@ -0,0 +1,19 @@ +// +// NSWindowKey.swift +// +// +// Created by Tatsuya Tanaka on 2023/02/14. +// + +import SwiftUI + +private struct NSWindowKey: EnvironmentKey { + static let defaultValue: NSWindow? = nil +} + +extension EnvironmentValues { + var nsWindow: NSWindow? { + get { self[NSWindowKey.self] } + set { self[NSWindowKey.self] = newValue } + } +} diff --git a/app/xcode/Sources/VCamUI/MacWindowManager.swift b/app/xcode/Sources/VCamUI/MacWindowManager.swift new file mode 100644 index 0000000..1c113ba --- /dev/null +++ b/app/xcode/Sources/VCamUI/MacWindowManager.swift @@ -0,0 +1,64 @@ +// +// MacWindowManager.swift +// +// +// Created by Tatsuya Tanaka on 2023/02/14. +// + +import AppKit +import SwiftUI +import VCamEntity + +public protocol MacWindow: View { + static var windowTitle: String { get } +} + +public final class MacWindowManager { + public static let shared = MacWindowManager() + + private var openWindows: [String: NSWindow] = [:] + + public func open(_ windowView: T) { + let id = String(describing: T.self) + if let window = openWindows[id] { + window.makeKeyAndOrderFront(nil) + return + } + + let window = NSWindow( + contentRect: .init(origin: .zero, size: .init(width: 1, height: 400)), + styleMask: [.titled, .closable, .fullSizeContentView], + backing: .buffered, + defer: false + ) + window.isReleasedWhenClosed = false + window.contentView = NSHostingView( + rootView: WindowContainer(content: windowView) + .environment(\.nsWindow, window) + ) + window.title = T.windowTitle + window.makeKeyAndOrderFront(nil) + window.center() + openWindows[id] = window + + var observation: Any? + observation = NotificationCenter.default.addObserver(forName: NSWindow.willCloseNotification, object: nil, queue: .main) { notification in + guard notification.object as? NSWindow == window else { return } + Self.shared.openWindows.removeValue(forKey: id) + if let observation { + NotificationCenter.default.removeObserver(observation) + } + } + } +} + +private struct WindowContainer: View { + let content: Content + + @AppStorage(key: .locale) var locale + + var body: some View { + content + .environment(\.locale, Locale(identifier: locale)) + } +} diff --git a/app/xcode/Sources/VCamUI/RootContentView.swift b/app/xcode/Sources/VCamUI/RootContentView.swift index 1440c82..b61565e 100644 --- a/app/xcode/Sources/VCamUI/RootContentView.swift +++ b/app/xcode/Sources/VCamUI/RootContentView.swift @@ -7,24 +7,28 @@ import SwiftUI -public struct RootContentView: View, Equatable { - public init(vcamUI: VCamUI, menuBottomView: MenuBottomView, unityView: NSView, interactable: Bool) { +public struct RootContentView: View { + public init(vcamUI: VCamUI, menuBottomView: MenuBottomView, toolbar: Toolbar, unityView: NSView, interactable: Bool) { self.vcamUI = vcamUI self.menuBottomView = menuBottomView + self.toolbar = toolbar self.unityView = unityView self.interactable = interactable } let vcamUI: VCamUI let menuBottomView: MenuBottomView + let toolbar: Toolbar let unityView: NSView let interactable: Bool + @State var isPopover = false + public var body: some View { if interactable { - HStack { + HStack(spacing: 0) { VCamMenu( - bottomView: menuBottomView.frame(height: 200) + bottomView: menuBottomView.frame(height: 280) ) .onTapGesture { unityView.window?.makeFirstResponder(nil) @@ -32,9 +36,15 @@ public struct RootContentView: View, Equatab .disabled(!interactable) VSplitView { - UnityView(unityView: unityView) - .equatable() - .layoutPriority(1) + HStack(alignment: .bottom, spacing: 0) { + toolbar + UnityView(unityView: unityView) + .equatable() + .frame(maxWidth: .infinity) + } + .frame(maxWidth: .infinity) + .layoutPriority(1) + vcamUI .onTapGesture { unityView.window?.makeFirstResponder(nil) @@ -47,10 +57,6 @@ public struct RootContentView: View, Equatab .layoutPriority(1) } } - - public static func == (lhs: Self, rhs: Self) -> Bool { - lhs.interactable == rhs.interactable - } } struct UnityView: View, Equatable { @@ -83,6 +89,7 @@ struct RootContentView_Previews: PreviewProvider { RootContentView( vcamUI: Color.red, menuBottomView: Color.blue, + toolbar: Color.yellow, unityView: NSView(), interactable: true ) diff --git a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbar.swift b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbar.swift new file mode 100644 index 0000000..c7c8a54 --- /dev/null +++ b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbar.swift @@ -0,0 +1,110 @@ +// +// VCamMainToolbar.swift +// +// +// Created by Tatsuya Tanaka on 2023/02/12. +// + +import SwiftUI + +public struct VCamMainToolbar: View { + public init(photoPicker: VCamMainToolbarPhotoPicker, emojiPicker: VCamMainToolbarEmojiPicker, motionPicker: VCamMainToolbarMotionPicker, blendShapePicker: VCamMainToolbarBlendShapePicker) { + self.photoPicker = photoPicker + self.emojiPicker = emojiPicker + self.motionPicker = motionPicker + self.blendShapePicker = blendShapePicker + } + + let photoPicker: VCamMainToolbarPhotoPicker + let emojiPicker: VCamMainToolbarEmojiPicker + let motionPicker: VCamMainToolbarMotionPicker + let blendShapePicker: VCamMainToolbarBlendShapePicker + + @State private var isPhotoPopover = false + @State private var isEmojiPickerPopover = false + @State private var isMotionPickerPopover = false + @State private var isBlendShapePickerPopover = false + + @Environment(\.locale) var locale + + public var body: some View { + VStack(spacing: 2) { + Item { + isPhotoPopover.toggle() + } label: { + Image(systemName: "photo") + } + .popover(isPresented: $isPhotoPopover) { + VCamPopoverContainer(L10n.background.key) { + photoPicker + } + .environment(\.locale, locale) + } + + Item { + isEmojiPickerPopover.toggle() + } label: { + Text("👍") + } + .popover(isPresented: $isEmojiPickerPopover) { + VCamPopoverContainer(L10n.emoji.key) { + emojiPicker + } + .environment(\.locale, locale) + } + + Item { + isMotionPickerPopover.toggle() + } label: { + Image(systemName: "figure.wave") + } + .popover(isPresented: $isMotionPickerPopover) { + VCamPopoverContainer(L10n.motion.key) { + motionPicker + } + .environment(\.locale, locale) + } + + Item { + isBlendShapePickerPopover.toggle() + } label: { + Image(systemName: "face.smiling") + } + .popover(isPresented: $isBlendShapePickerPopover) { + VCamPopoverContainer(L10n.facialExpression.key) { + blendShapePicker + } + .environment(\.locale, locale) + } + } + .frame(maxHeight: .infinity, alignment: .bottom) + .background(.thinMaterial) + } + + private struct Item: View { + let action: () -> Void + let label: () -> Label + + private let size: CGFloat = 18 + + var body: some View { + Button(action: action) { + label() + .frame(width: size, height: size) + .macHoverEffect() + } + .buttonStyle(.plain) + } + } +} + +struct VCamMainToolbar_Previews: PreviewProvider { + static var previews: some View { + VCamMainToolbar( + photoPicker: VCamMainToolbarPhotoPicker(backgroundColor: .constant(.red), loadBackgroundImage: { _ in }, removeBackgroundImage: {}), + emojiPicker: VCamMainToolbarEmojiPicker(showEmoji: { _ in }), + motionPicker: VCamMainToolbarMotionPicker(motionHello: {}, motionBye: .constant(false), motionJump: {}, motionYear: {}, motionWhat: {}, motionWin: {}, motionNod: .constant(false), motionShakeHead: .constant(false), motionShakeBody: .constant(false), motionRun: .constant(false)), + blendShapePicker: VCamMainToolbarBlendShapePicker(blendShapes: [], selectedBlendShape: .constant(nil)) + ) + } +} diff --git a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBlendShapePicker.swift b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBlendShapePicker.swift new file mode 100644 index 0000000..c95f7ba --- /dev/null +++ b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarBlendShapePicker.swift @@ -0,0 +1,57 @@ +// +// VCamMainToolbarBlendShapePicker.swift +// +// +// Created by Tatsuya Tanaka on 2023/02/12. +// + +import SwiftUI + +public struct VCamMainToolbarBlendShapePicker: View { + public init(blendShapes: [String], selectedBlendShape: Binding) { + self.blendShapes = blendShapes + self._selectedBlendShape = selectedBlendShape + } + + let blendShapes: [String] + @Binding var selectedBlendShape: String? + + public var body: some View { + ScrollView(.vertical, showsIndicators: true) { + GroupBox { + LazyVGrid(columns: Array(repeating: GridItem(.adaptive(minimum: 80), spacing: 2), count: 3)) { + ForEach(blendShapes, id: \.self) { blendShape in + HoverToggle(text: blendShape, isOn: $selectedBlendShape.map( + get: { blendShape == $0 }, + set: { $0 ? blendShape : nil } + )) + } + } + } + } + .frame(width: 280, height: 150) + } + + struct HoverToggle: View { + let text: String + @Binding var isOn: Bool + + var body: some View { + Button(action: { isOn.toggle() }) { + Text(text) + .font(.callout) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .macHoverEffect() + .background(isOn ? Color.accentColor.opacity(0.3) : nil) + .cornerRadius(4) + } + .buttonStyle(.plain) + } + } +} + +struct VCamMainToolbarBlendShapePicker_Previews: PreviewProvider { + static var previews: some View { + VCamMainToolbarBlendShapePicker(blendShapes: ["natural", "joy"], selectedBlendShape: .constant("joy")) + } +} diff --git a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarEmojiPicker.swift b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarEmojiPicker.swift new file mode 100644 index 0000000..1e7917b --- /dev/null +++ b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarEmojiPicker.swift @@ -0,0 +1,110 @@ +// +// VCamMainToolbarEmojiPicker.swift +// +// +// Created by Tatsuya Tanaka on 2023/02/12. +// + +import SwiftUI + +public struct VCamMainToolbarEmojiPicker: View { + public init(showEmoji: @escaping (URL) -> Void) { + self.showEmoji = showEmoji + } + + let showEmoji: (URL) -> Void + + static let emojis = ["👍", "🎉", "❤️", "🤣", "🥺", "😢", "👏", "🙏", "💪", "🙌", "👀", "✨", "🔥", "💦", "❌", "⭕️", "⁉️", "❓", "⚠️", "💮"] + + @Environment(\.dismiss) var dismiss + + public var body: some View { + HStack { + Button { + NotificationCenter.default.post(name: .showEmojiPicker, object: nil) + dismiss() + } label: { + Image(systemName: "face.smiling") + } + + LazyVGrid(columns: Array(repeating: GridItem(.fixed(26), spacing: 0), count: 5)) { + ForEach(Self.emojis, id: \.self) { emoji in + Button { + pickEmoji(emoji) + dismiss() + } label: { + Text(emoji) + .macHoverEffect() + } + .buttonStyle(.plain) + } + } + } + .background( + Color.clear.frame(width: 1, height: 1) + .background(HiddenTextField().opacity(0)) + .onReceive(NotificationCenter.default.publisher(for: NSControl.textDidChangeNotification)) { notification in + guard let textField = notification.object as? NSTextField, textField.tag == HiddenTextField.tag else { return } + pickEmoji(textField.stringValue) + + textField.stringValue = "" + } + ) + .fixedSize() + } + + private func pickEmoji(_ emoji: String) { + let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("vcam_emoji.png") + try? emoji.drawImage().writeAsPNG(to: url) + showEmoji(url) + } +} + +private struct HiddenTextField: NSViewRepresentable { + static let tag = 1234 + + func makeNSView(context: Context) -> NSTextField { + let view = NSTextField() + view.delegate = context.coordinator + view.tag = Self.tag + context.coordinator.observe(textField: view) + return view + } + + func updateNSView(_ nsView: NSTextField, context: Context) { + } + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + final class Coordinator: NSObject, NSTextFieldDelegate { + var key: NSObjectProtocol? + + func observe(textField: NSTextField) { + if let key = key { + NotificationCenter.default.removeObserver(key) + } + key = NotificationCenter.default.addObserver(forName: .showEmojiPicker, object: nil, queue: .main) { _ in + textField.window?.makeFirstResponder(textField) + NSApp.orderFrontCharacterPalette(textField) + } + } + + func controlTextDidChange(_ notification: Notification) { + NSApp.resignFirstResponder() + } + + deinit { + if let key = key { + NotificationCenter.default.removeObserver(key) + } + } + } +} + +struct VCamMainToolbarEmojiPicker_Previews: PreviewProvider { + static var previews: some View { + VCamMainToolbarEmojiPicker(showEmoji: { _ in }) + } +} diff --git a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarMotionPicker.swift b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarMotionPicker.swift new file mode 100644 index 0000000..55749a5 --- /dev/null +++ b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarMotionPicker.swift @@ -0,0 +1,80 @@ +// +// VCamMainToolbarMotionPicker.swift +// +// +// Created by Tatsuya Tanaka on 2023/02/12. +// + +import SwiftUI + +public struct VCamMainToolbarMotionPicker: View { + public init(motionHello: @escaping () -> Void, motionBye: Binding, motionJump: @escaping () -> Void, motionYear: @escaping () -> Void, motionWhat: @escaping () -> Void, motionWin: @escaping () -> Void, motionNod: Binding, motionShakeHead: Binding, motionShakeBody: Binding, motionRun: Binding) { + self.motionHello = motionHello + self.motionBye = motionBye + self.motionJump = motionJump + self.motionYear = motionYear + self.motionWhat = motionWhat + self.motionWin = motionWin + self.motionNod = motionNod + self.motionShakeHead = motionShakeHead + self.motionShakeBody = motionShakeBody + self.motionRun = motionRun + } + + let motionHello: () -> Void + let motionBye: Binding + let motionJump: () -> Void + let motionYear: () -> Void + let motionWhat: () -> Void + let motionWin: () -> Void + let motionNod: Binding + let motionShakeHead: Binding + let motionShakeBody: Binding + let motionRun: Binding + + public var body: some View { + GroupBox { + LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 2), count: 3)) { + button(key: L10n.hi.key, action: motionHello) + toggle(key: L10n.bye.key, isOn: motionBye) + button(key: L10n.jump.key, action: motionJump) + button(key: L10n.cheer.key, action: motionYear) + button(key: L10n.what.key, action: motionWhat) + Group { + button(key: L10n.pose.key, action: motionWin) + toggle(key: L10n.nod.key, isOn: motionNod) + toggle(key: L10n.no.key, isOn: motionShakeHead) + toggle(key: L10n.shudder.key, isOn: motionShakeBody) + toggle(key: L10n.run.key, isOn: motionRun) + } + } + } + .frame(width: 240) + } + + func button(key: LocalizedStringKey, action: @escaping () -> Void) -> some View { + Button(action: action) { + Text(key, bundle: .localize) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .macHoverEffect() + } + .buttonStyle(.plain) + } + + func toggle(key: LocalizedStringKey, isOn: Binding) -> some View { + Button(action: { isOn.wrappedValue.toggle() }) { + Text(key, bundle: .localize) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .macHoverEffect() + .background(isOn.wrappedValue ? Color.accentColor.opacity(0.3) : nil) + .cornerRadius(4) + } + .buttonStyle(.plain) + } +} + +struct VCamMainToolbarMotionPicker_Previews: PreviewProvider { + static var previews: some View { + VCamMainToolbarMotionPicker(motionHello: {}, motionBye: .constant(false), motionJump: {}, motionYear: {}, motionWhat: {}, motionWin: {}, motionNod: .constant(false), motionShakeHead: .constant(false), motionShakeBody: .constant(false), motionRun: .constant(false)) + } +} diff --git a/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarPhotoPicker.swift b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarPhotoPicker.swift new file mode 100644 index 0000000..0070201 --- /dev/null +++ b/app/xcode/Sources/VCamUI/Toolbar/VCamMainToolbarPhotoPicker.swift @@ -0,0 +1,60 @@ +// +// VCamMainToolbarPhotoPicker.swift +// +// +// Created by Tatsuya Tanaka on 2023/02/12. +// + +import SwiftUI +import VCamUIFoundation + +public struct VCamMainToolbarPhotoPicker: View { + public init(backgroundColor: Binding, loadBackgroundImage: @escaping (URL) -> Void, removeBackgroundImage: @escaping () -> Void) { + self._backgroundColor = backgroundColor + self.loadBackgroundImage = loadBackgroundImage + self.removeBackgroundImage = removeBackgroundImage + } + + @Binding var backgroundColor: Color + + let loadBackgroundImage: (URL) -> Void + let removeBackgroundImage: () -> Void + + public var body: some View { + GroupBox { + Form { + HStack { + Text(L10n.color.key, bundle: .localize) + .fixedSize(horizontal: true, vertical: false) + ColorEditField(L10n.color.key, value: $backgroundColor) + .labelsHidden() + } + HStack { + Text(L10n.image.key, bundle: .localize) + .fixedSize(horizontal: true, vertical: false) + Button { + if let url = FileUtility.openFile(type: .image) { + loadBackgroundImage(url) + } + } label: { + Image(systemName: "photo") + } + Button { + removeBackgroundImage() + } label: { + HStack { + Image(systemName: "trash.fill") + } + .foregroundColor(.red) + } + } + } + } + } +} + +struct VCamMainToolbarPhotoPicker_Previews: PreviewProvider { + static var previews: some View { + VCamMainToolbarPhotoPicker(backgroundColor: .constant(.red), loadBackgroundImage: { _ in }, removeBackgroundImage: {}) + } +} diff --git a/app/xcode/Sources/VCamUI/UIComponent/MacHoverEffect.swift b/app/xcode/Sources/VCamUI/UIComponent/MacHoverEffect.swift new file mode 100644 index 0000000..e41f12b --- /dev/null +++ b/app/xcode/Sources/VCamUI/UIComponent/MacHoverEffect.swift @@ -0,0 +1,31 @@ +// +// MacHoverEffect.swift +// +// +// Created by Tatsuya Tanaka on 2023/02/12. +// + +import SwiftUI + +public struct HoverEffectButtonViewModifier: ViewModifier { + public init() {} + + @State var isHovered = false + + public func body(content: Content) -> some View { + content + .padding(4) + .background(isHovered ? Color.white.opacity(0.1) : nil) + .cornerRadius(4) + .onHover { + self.isHovered = $0 + } + } +} + +public extension View { + @inlinable + func macHoverEffect() -> some View { + modifier(HoverEffectButtonViewModifier()) + } +} diff --git a/app/xcode/Sources/VCamUI/UIComponent/NSWindow+presentWindow.swift b/app/xcode/Sources/VCamUI/UIComponent/NSWindow+presentWindow.swift deleted file mode 100644 index 288e8e5..0000000 --- a/app/xcode/Sources/VCamUI/UIComponent/NSWindow+presentWindow.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// NSWindow+presentWindow.swift -// -// -// Created by Tatsuya Tanaka on 2022/04/23. -// - -import Foundation -import SwiftUI -import VCamLocalization - -public func presentWindow(title: String, id: String?, size: NSSize? = nil, content: (NSWindow) -> NSView) { - let windowRef = NSWindow() - windowRef.styleMask = [.titled, .closable, .resizable] - windowRef.backingType = .buffered - let view = content(windowRef) - windowRef.setContentSize(size ?? view.fittingSize) - windowRef.contentView = view - windowRef.title = title - if let id = id { - windowRef.setFrameAutosaveName(id) - } - windowRef.makeKeyAndOrderFront(nil) -} - -public func presentWindow(title: String, id: String?, size: NSSize? = nil, content: (NSWindow) -> Content) { - presentWindow(title: title, id: id, size: size) { window in - NSHostingView(rootView: content(window).environment(\.locale, LocalizationEnvironment.language.locale)) - } -} diff --git a/app/xcode/Sources/VCamUI/UIComponent/VCamPopoverContainer.swift b/app/xcode/Sources/VCamUI/UIComponent/VCamPopoverContainer.swift new file mode 100644 index 0000000..a0e1fed --- /dev/null +++ b/app/xcode/Sources/VCamUI/UIComponent/VCamPopoverContainer.swift @@ -0,0 +1,37 @@ +// +// VCamPopoverContainer.swift +// +// +// Created by Tatsuya Tanaka on 2023/02/14. +// + +import SwiftUI + +public struct VCamPopoverContainer: View { + public init(_ title: LocalizedStringKey, @ViewBuilder content: @escaping () -> Content) { + self.title = title + self.content = content + } + + let title: LocalizedStringKey + let content: () -> Content + + public var body: some View { + VStack(spacing: 1) { + Text(title, bundle: .localize) + .font(.caption) + + content() + } + .padding([.horizontal, .bottom], 8) + .padding(.top, 4) + } +} + +struct VCamMainToolbarContainer_Previews: PreviewProvider { + static var previews: some View { + VCamPopoverContainer("hello") { + Text("world") + } + } +} diff --git a/app/xcode/Sources/VCamUI/VCamMenu.swift b/app/xcode/Sources/VCamUI/VCamMenu.swift index ec4c152..2f81acc 100644 --- a/app/xcode/Sources/VCamUI/VCamMenu.swift +++ b/app/xcode/Sources/VCamUI/VCamMenu.swift @@ -9,11 +9,8 @@ import SwiftUI public enum VCamMenuItem: Identifiable, CaseIterable { case main - case preference - case tracking case screenEffect case recording - case integration public var id: Self { self } @@ -21,16 +18,10 @@ public enum VCamMenuItem: Identifiable, CaseIterable { switch self { case .main: return L10n.main.key - case .preference: - return L10n.preference.key - case .tracking: - return L10n.tracking.key case .screenEffect: return L10n.screenEffect.key case .recording: return L10n.recording.key - case .integration: - return L10n.integration.key } } @@ -38,16 +29,10 @@ public enum VCamMenuItem: Identifiable, CaseIterable { switch self { case .main: return "person.fill" - case .preference: - return "gearshape.fill" - case .tracking: - return "face.dashed" case .screenEffect: return "sparkles" case .recording: return "camera.fill" - case .integration: - return "app.connected.to.app.below.fill" } } }