diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 53aee4e..45a81fc 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -38,6 +38,7 @@ EC7F6BEC2BE5E6E300A34A7B /* MemolaApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */; }; EC7F6BF02BE5E6E400A34A7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */; }; EC7F6BF32BE5E6E400A34A7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */; }; + EC86C5822C4010CC00C07D21 /* PhotoDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC86C5812C4010CC00C07D21 /* PhotoDock.swift */; }; EC8C9DCE2C39882500A8F3C4 /* NSSyncScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8C9DCD2C39882500A8F3C4 /* NSSyncScrollView.swift */; }; EC8F54AC2C2ACDA8001C7C74 /* GridMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8F54AB2C2ACDA8001C7C74 /* GridMode.swift */; }; EC8F54AE2C2AF5A4001C7C74 /* LineGridVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC8F54AD2C2AF5A4001C7C74 /* LineGridVertex.swift */; }; @@ -164,6 +165,7 @@ EC7F6BEB2BE5E6E300A34A7B /* MemolaApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemolaApp.swift; sourceTree = ""; }; EC7F6BEF2BE5E6E400A34A7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; EC7F6BF22BE5E6E400A34A7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + EC86C5812C4010CC00C07D21 /* PhotoDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoDock.swift; sourceTree = ""; }; EC8C9DCD2C39882500A8F3C4 /* NSSyncScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSSyncScrollView.swift; sourceTree = ""; }; EC8F54AB2C2ACDA8001C7C74 /* GridMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridMode.swift; sourceTree = ""; }; EC8F54AD2C2AF5A4001C7C74 /* LineGridVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineGridVertex.swift; sourceTree = ""; }; @@ -486,6 +488,14 @@ path = "Preview Content"; sourceTree = ""; }; + EC86C5802C4010BE00C07D21 /* PhotoDock */ = { + isa = PBXGroup; + children = ( + EC86C5812C4010CC00C07D21 /* PhotoDock.swift */, + ); + path = PhotoDock; + sourceTree = ""; + }; EC8C9DCC2C3987FD00A8F3C4 /* AppKit */ = { isa = PBXGroup; children = ( @@ -533,6 +543,7 @@ ECA7387B2BE5EF3500A4542E /* Memo */ = { isa = PBXGroup; children = ( + EC86C5802C4010BE00C07D21 /* PhotoDock */, ECDAC0792C318DAF0000ED77 /* ElementToolbar */, ECBE52942C1D58F5006BDB3D /* PhotoPreview */, EC1B783B2BFA0AAC005A34E2 /* Toolbar */, @@ -1029,6 +1040,7 @@ ECFA15262BEF224900455818 /* StrokeObject.swift in Sources */, ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */, ECA7387A2BE5EF0400A4542E /* MemosView.swift in Sources */, + EC86C5822C4010CC00C07D21 /* PhotoDock.swift in Sources */, ECA738BA2BE60DEF00A4542E /* HistoryAction.swift in Sources */, ECFA15222BEF21F500455818 /* CanvasObject.swift in Sources */, ECA738AA2BE6026D00A4542E /* Uniforms.swift in Sources */, diff --git a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift index 8a92378..9ecd486 100644 --- a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift +++ b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift @@ -10,17 +10,13 @@ import PhotosUI import AVFoundation struct ElementToolbar: View { + @Environment(\.colorScheme) var colorScheme @Environment(\.horizontalSizeClass) var horizontalSizeClass + let size: CGFloat = 40 @ObservedObject var tool: Tool @ObservedObject var canvas: Canvas - @State var opensCamera: Bool = false - @State var isCameraAccessDenied: Bool = false - @State var photosPickerItem: PhotosPickerItem? - - @Namespace var namespace - var body: some View { Group { #if os(macOS) @@ -31,10 +27,7 @@ struct ElementToolbar: View { } else { ZStack(alignment: .bottom) { if tool.selection == .photo { - photoOption - .padding(.bottom, 10) - .frame(maxWidth: .infinity) - .transition(.move(edge: .bottom).combined(with: .blurReplace)) + PhotoDock(tool: tool, canvas: canvas) } else { compactToolbar } @@ -42,42 +35,7 @@ struct ElementToolbar: View { } #endif } - #if os(iOS) - .fullScreenCover(isPresented: $opensCamera) { - let image: Binding = Binding { - tool.selectedPhotoItem?.image - } set: { image in - guard let image else { return } - tool.selectPhoto(image, for: canvas.canvasID) - } - CameraView(image: image, canvas: canvas) - .ignoresSafeArea() - } - .alert("Camera Access Denied", isPresented: $isCameraAccessDenied) { - Button { - if let url = URL(string: Platform.Application.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") { - Platform.Application.shared.open(url) - } - } label: { - Text("Open Settings") - } - Button("Cancel", role: .cancel) { } - } message: { - Text("Memola requires access to the camera to capture photos. Please open Settings and enable camera access.") - } - #endif - .onChange(of: photosPickerItem) { oldValue, newValue in - if newValue != nil { - Task { - tool.isLoadingPhoto = true - let data = try? await newValue?.loadTransferable(type: Data.self) - if let data, let image = Platform.Image(data: data) { - tool.selectPhoto(image, for: canvas.canvasID) - } - photosPickerItem = nil - } - } - } + } var regularToolbar: some View { @@ -91,7 +49,7 @@ struct ElementToolbar: View { .fontWeight(.heavy) .contentShape(.circle) .frame(width: size, height: size) - .foregroundStyle(tool.selection == .hand ? Color.white : Color.accentColor) + .foregroundStyle(tool.selection == .hand ? colorScheme == .light ? Color.white : Color.black : Color.accentColor) .clipShape(.rect(cornerRadius: 8)) .contentShape(.rect(cornerRadius: 8)) } @@ -104,7 +62,6 @@ struct ElementToolbar: View { if tool.selection == .hand { Color.accentColor .clipShape(.rect(cornerRadius: 8)) - .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) } } Button { @@ -116,7 +73,7 @@ struct ElementToolbar: View { .fontWeight(.heavy) .contentShape(.circle) .frame(width: size, height: size) - .foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor) + .foregroundStyle(tool.selection == .pen ? colorScheme == .light ? Color.white : Color.black : Color.accentColor) .clipShape(.rect(cornerRadius: 8)) .contentShape(.rect(cornerRadius: 8)) } @@ -129,7 +86,6 @@ struct ElementToolbar: View { if tool.selection == .pen { Color.accentColor .clipShape(.rect(cornerRadius: 8)) - .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) } } HStack(spacing: 0) { @@ -141,7 +97,7 @@ struct ElementToolbar: View { Image(systemName: "photo") .contentShape(.circle) .frame(width: size, height: size) - .foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor) + .foregroundStyle(tool.selection == .photo ? colorScheme == .light ? Color.white : Color.black : Color.accentColor) .clipShape(.rect(cornerRadius: 8)) .contentShape(.rect(cornerRadius: 8)) } @@ -151,22 +107,10 @@ struct ElementToolbar: View { .buttonStyle(.plain) #endif .background { - #if os(iOS) if tool.selection == .photo { Color.accentColor .clipShape(.rect(cornerRadius: 8)) - .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) } - #endif - if tool.selection != .photo { - Color.clear - .matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace) - } - } - if tool.selection == .photo { - photoOption - .matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace) - .transition(.blurReplace.animation(.easeIn(duration: 0.1))) } } } @@ -216,75 +160,5 @@ struct ElementToolbar: View { .transition(.move(edge: .bottom).combined(with: .blurReplace)) } - var photoOption: some View { - HStack(spacing: 0) { - #if os(iOS) - Button { - openCamera() - } label: { - Image(systemName: "camera.fill") - .frame(width: size, height: size) - .clipShape(.rect(cornerRadius: 8)) - .contentShape(.rect(cornerRadius: 8)) - } - .hoverEffect(.lift) - #endif - PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { - Image(systemName: "photo.fill.on.rectangle.fill") - .frame(width: size, height: size) - .clipShape(.rect(cornerRadius: 8)) - .contentShape(.rect(cornerRadius: 8)) - } - #if os(iOS) - .hoverEffect(.lift) - #else - .buttonStyle(.plain) - #endif - if horizontalSizeClass == .compact { - Divider() - .padding(.vertical, 4) - .frame(height: size) - .foregroundStyle(Color.accentColor) - Button { - withAnimation { - tool.selectTool(.hand) - } - } label: { - Image(systemName: "xmark") - .frame(width: size, height: size) - .clipShape(.rect(cornerRadius: 8)) - .contentShape(.rect(cornerRadius: 8)) - } - #if os(iOS) - .hoverEffect(.lift) - #else - .buttonStyle(.plain) - #endif - } - } - .background { - RoundedRectangle(cornerRadius: 8) - .fill(.regularMaterial) - } - } - - func openCamera() { - let status = AVCaptureDevice.authorizationStatus(for: .video) - switch status { - case .notDetermined: - AVCaptureDevice.requestAccess(for: .video) { status in - withAnimation { - if status { - opensCamera = true - } else { - isCameraAccessDenied = true - } - } - } - case .authorized: - opensCamera = true - default: - isCameraAccessDenied = true - } - } + } diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index 885baa6..6152646 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -66,14 +66,17 @@ struct MemoView: View { var canvasView: some View { CanvasView(tool: tool, canvas: canvas, history: history) .ignoresSafeArea() - .overlay(alignment: .bottomTrailing) { + .overlay(alignment: .trailing) { switch tool.selection { case .pen: PenDock(tool: tool, canvas: canvas) case .photo: - if let photoItem = tool.selectedPhotoItem { - PhotoPreview(photoItem: photoItem, tool: tool) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) + ZStack(alignment: .bottomTrailing) { + PhotoDock(tool: tool, canvas: canvas) + if let photoItem = tool.selectedPhotoItem { + PhotoPreview(photoItem: photoItem, tool: tool) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + } } default: EmptyView() diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index aa1a9b6..0f2d2e0 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -130,6 +130,7 @@ struct PenDock: View { .padding(.vertical, 5) .frame(width: width) } + .padding(.vertical, 3) .background(alignment: .trailing) { RoundedRectangle(cornerRadius: 8) .fill(.regularMaterial) diff --git a/Memola/Features/Memo/PhotoDock/PhotoDock.swift b/Memola/Features/Memo/PhotoDock/PhotoDock.swift new file mode 100644 index 0000000..5c8db9a --- /dev/null +++ b/Memola/Features/Memo/PhotoDock/PhotoDock.swift @@ -0,0 +1,198 @@ +// +// PhotoDock.swift +// Memola +// +// Created by Dscyre Scotti on 7/11/24. +// + +import SwiftUI +import PhotosUI + +struct PhotoDock: View { + @Environment(\.horizontalSizeClass) var horizontalSizeClass + + let size: CGFloat = 40 + + @ObservedObject var tool: Tool + @ObservedObject var canvas: Canvas + + @State var opensCamera: Bool = false + @State var isCameraAccessDenied: Bool = false + @State var photosPickerItem: PhotosPickerItem? + + var body: some View { + Group { + if horizontalSizeClass == .regular { + photoOption + } else { + compactPhotoOption + } + } + #if os(iOS) + .fullScreenCover(isPresented: $opensCamera) { + let image: Binding = Binding { + tool.selectedPhotoItem?.image + } set: { image in + guard let image else { return } + tool.selectPhoto(image, for: canvas.canvasID) + } + CameraView(image: image, canvas: canvas) + .ignoresSafeArea() + } + .alert("Camera Access Denied", isPresented: $isCameraAccessDenied) { + Button { + if let url = URL(string: Platform.Application.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") { + Platform.Application.shared.open(url) + } + } label: { + Text("Open Settings") + } + Button("Cancel", role: .cancel) { } + } message: { + Text("Memola requires access to the camera to capture photos. Please open Settings and enable camera access.") + } + #endif + .onChange(of: photosPickerItem) { oldValue, newValue in + if newValue != nil { + Task { + tool.isLoadingPhoto = true + let data = try? await newValue?.loadTransferable(type: Data.self) + if let data, let image = Platform.Image(data: data) { + tool.selectPhoto(image, for: canvas.canvasID) + } + photosPickerItem = nil + } + } + } + } + + var photoOption: some View { + VStack(spacing: 0) { + #if os(iOS) + Button { + openCamera() + } label: { + Image(systemName: "camera.fill") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + #endif + PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { + Image(systemName: "photo.fill.on.rectangle.fill") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + #if os(iOS) + .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif + if horizontalSizeClass == .compact { + Divider() + .padding(.vertical, 4) + .frame(height: size) + .foregroundStyle(Color.accentColor) + Button { + withAnimation { + tool.selectTool(.hand) + } + } label: { + Image(systemName: "xmark") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + #if os(iOS) + .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif + } + } + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + } + .padding(.trailing, 10) + .frame(maxHeight: .infinity) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + } + + var compactPhotoOption: some View { + HStack(spacing: 0) { + #if os(iOS) + Button { + openCamera() + } label: { + Image(systemName: "camera.fill") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + #endif + PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { + Image(systemName: "photo.fill.on.rectangle.fill") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + #if os(iOS) + .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif + if horizontalSizeClass == .compact { + Divider() + .padding(.vertical, 4) + .frame(height: size) + .foregroundStyle(Color.accentColor) + Button { + withAnimation { + tool.selectTool(.hand) + } + } label: { + Image(systemName: "xmark") + .frame(width: size, height: size) + .clipShape(.rect(cornerRadius: 8)) + .contentShape(.rect(cornerRadius: 8)) + } + #if os(iOS) + .hoverEffect(.lift) + #else + .buttonStyle(.plain) + #endif + } + } + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + } + .padding(.bottom, 10) + .frame(maxWidth: .infinity) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) + } + + func openCamera() { + let status = AVCaptureDevice.authorizationStatus(for: .video) + switch status { + case .notDetermined: + AVCaptureDevice.requestAccess(for: .video) { status in + withAnimation { + if status { + opensCamera = true + } else { + isCameraAccessDenied = true + } + } + } + case .authorized: + opensCamera = true + default: + isCameraAccessDenied = true + } + } +} diff --git a/Memola/Resources/Assets/Assets.xcassets/AccentColor.colorset/Contents.json b/Memola/Resources/Assets/Assets.xcassets/AccentColor.colorset/Contents.json index eb87897..462f7c0 100644 --- a/Memola/Resources/Assets/Assets.xcassets/AccentColor.colorset/Contents.json +++ b/Memola/Resources/Assets/Assets.xcassets/AccentColor.colorset/Contents.json @@ -1,6 +1,33 @@ { "colors" : [ { + "color" : { + "color-space" : "extended-srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF5", + "green" : "0x7E", + "red" : "0x00" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "extended-srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xCE", + "red" : "0x99" + } + }, "idiom" : "universal" } ],