diff --git a/Memola/Canvas/Tool/Core/Tool.swift b/Memola/Canvas/Tool/Core/Tool.swift index 4c837c2..a7641a4 100644 --- a/Memola/Canvas/Tool/Core/Tool.swift +++ b/Memola/Canvas/Tool/Core/Tool.swift @@ -20,8 +20,9 @@ public class Tool: NSObject, ObservableObject { @Published var draggedPen: Pen? // MARK: - Photo @Published var selectedPhotoItem: PhotoItem? + @Published var isLoadingPhoto: Bool = false - @Published var selection: ToolSelection = .none + @Published var selection: ToolSelection = .hand let scrollPublisher = PassthroughSubject() var markers: [Pen] { @@ -32,6 +33,10 @@ public class Tool: NSObject, ObservableObject { self.object = object } + func selectTool(_ selection: ToolSelection) { + self.selection = selection + } + func load() { DispatchQueue.main.async { [weak self] in guard let self else { return } @@ -116,6 +121,7 @@ public class Tool: NSObject, ObservableObject { func selectPhoto(_ image: UIImage, for canvasID: NSManagedObjectID) { guard let (resizedImage, dimension) = resizePhoto(of: image) else { return } let photoItem = bookmarkPhoto(of: resizedImage, in: dimension, with: canvasID) + isLoadingPhoto = false withAnimation { selectedPhotoItem = photoItem } diff --git a/Memola/Canvas/Tool/Core/ToolSelection.swift b/Memola/Canvas/Tool/Core/ToolSelection.swift index 76c3589..ed3fc26 100644 --- a/Memola/Canvas/Tool/Core/ToolSelection.swift +++ b/Memola/Canvas/Tool/Core/ToolSelection.swift @@ -8,7 +8,7 @@ import Foundation enum ToolSelection: Equatable { - case none + case hand case pen case photo } diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index a21f7fb..6b80f14 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -335,7 +335,7 @@ extension CanvasViewController { let enablesDrawing: Bool let enablesPhotoInsertion: Bool switch selection { - case .none: + case .hand: enablesScrolling = true enablesDrawing = false enablesPhotoInsertion = false @@ -362,7 +362,6 @@ extension CanvasViewController { } func lockModeChanged(_ state: Bool) { - scrollView.isScrollEnabled = !state scrollView.pinchGestureRecognizer?.isEnabled = !state } } diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index 362bffb..528abd8 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -32,7 +32,7 @@ struct MemoView: View { .overlay(alignment: .bottomTrailing) { switch tool.selection { case .pen: - PenDock(tool: tool, canvas: canvas) + PenDock(tool: tool, canvas: canvas, size: size) .transition(.move(edge: .trailing)) case .photo: if let photoItem = tool.selectedPhotoItem { @@ -46,7 +46,7 @@ struct MemoView: View { .overlay(alignment: .bottomLeading) { zoomControl } - .disabled(textFieldState) + .disabled(textFieldState || tool.isLoadingPhoto) .overlay(alignment: .top) { Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history) } @@ -61,6 +61,11 @@ struct MemoView: View { EmptyView() } } + .overlay { + if tool.isLoadingPhoto { + loadingIndicator("Loading photo...") + } + } } @ViewBuilder @@ -96,6 +101,7 @@ struct MemoView: View { .clipShape(.rect(cornerRadius: 8)) .padding(10) } + .hoverEffect(.lift) .transition(.move(edge: .bottom).combined(with: .blurReplace)) } } diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index cbf4432..d9af8ca 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -11,6 +11,7 @@ struct PenDock: View { @ObservedObject var tool: Tool @ObservedObject var canvas: Canvas + let size: CGFloat let width: CGFloat = 90 let height: CGFloat = 30 let factor: CGFloat = 0.9 @@ -19,15 +20,20 @@ struct PenDock: View { @State var opensColorPicker: Bool = false var body: some View { - if !canvas.locksCanvas { - VStack(alignment: .trailing) { - penPropertyTool - penItemList + ZStack(alignment: .bottomTrailing) { + if !canvas.locksCanvas { + VStack(alignment: .trailing) { + penPropertyTool + penItemList + } + .fixedSize() + .frame(maxHeight: .infinity) + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) } - .fixedSize() - .frame(maxHeight: .infinity) - .padding(10) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) + lockButton + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) } } @@ -45,7 +51,6 @@ struct PenDock: View { } } .padding(.vertical, 10) - .padding(.leading, 40) .id(refreshScrollId) } .onReceive(tool.scrollPublisher) { id in @@ -56,7 +61,7 @@ struct PenDock: View { } } } - .frame(maxHeight:( (height * factor + 10) * 6) + 20) + .frame(maxHeight: ((height * factor + 10) * 6) + 20) .fixedSize() .background(alignment: .trailing) { RoundedRectangle(cornerRadius: 8) @@ -66,7 +71,7 @@ struct PenDock: View { .clipShape(.rect(cornerRadii: .init(bottomTrailing: 8, topTrailing: 8))) .overlay(alignment: .bottomLeading) { newPenButton - .offset(x: 60, y: 10) + .offset(x: 15, y: 10) } } @@ -235,6 +240,7 @@ struct PenDock: View { .frame(width: size + 2, height: size + 2) } } + .hoverEffect(.lift) .pickerStyle(.wheel) .frame(width: width * factor - 18, height: 35) .onChange(of: pen.thickness) { _, _ in @@ -310,4 +316,20 @@ struct PenDock: View { } } } + + var lockButton: some View { + Button { + withAnimation { + canvas.locksCanvas.toggle() + } + } label: { + Image(systemName: canvas.locksCanvas ? "lock.fill" : "lock.open.fill") + .contentShape(.circle) + .frame(width: size, height: size) + .background(.regularMaterial) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + .contentTransition(.symbolEffect(.replace)) + } } diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index 980f2a9..15d2546 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -25,6 +25,8 @@ struct Toolbar: View { @FocusState var textFieldState: Bool + @Namespace var namespace + let size: CGFloat init(size: CGFloat, memo: MemoObject, tool: Tool, canvas: Canvas, history: History) { @@ -45,12 +47,13 @@ struct Toolbar: View { } } .frame(maxWidth: .infinity, alignment: .leading) - elementTool - HStack(spacing: 5) { + if !canvas.locksCanvas { + elementTool + } + Group { if !canvas.locksCanvas { historyControl } - lockButton } .frame(maxWidth: .infinity, alignment: .trailing) } @@ -59,6 +62,7 @@ struct Toolbar: View { .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 = UIImage(data: data) { tool.selectPhoto(image, for: canvas.canvasID) @@ -133,32 +137,68 @@ struct Toolbar: View { HStack(spacing: 0) { Button { withAnimation { - tool.selection = tool.selection == .pen ? .none : .pen + tool.selectTool(.hand) + } + } label: { + Image(systemName: "hand.draw.fill") + .fontWeight(.heavy) + .contentShape(.circle) + .frame(width: size, height: size) + .foregroundStyle(tool.selection == .hand ? Color.white : Color.accentColor) + .clipShape(.rect(cornerRadius: 8)) + } + .hoverEffect(.lift) + .background { + if tool.selection == .hand { + Color.accentColor + .clipShape(.rect(cornerRadius: 8)) + .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) + } + } + Button { + withAnimation { + tool.selectTool(.pen) } } label: { Image(systemName: "pencil") .fontWeight(.heavy) .contentShape(.circle) .frame(width: size, height: size) - .background(tool.selection == .pen ? Color.accentColor : Color.clear) .foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor) .clipShape(.rect(cornerRadius: 8)) } .hoverEffect(.lift) + .background { + if tool.selection == .pen { + Color.accentColor + .clipShape(.rect(cornerRadius: 8)) + .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) + } + } HStack(spacing: 0) { Button { withAnimation { - tool.selection = tool.selection == .photo ? .none : .photo + tool.selectTool(.photo) } } label: { Image(systemName: "photo") .contentShape(.circle) .frame(width: size, height: size) - .background(tool.selection == .photo ? Color.accentColor : Color.clear) .foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor) .clipShape(.rect(cornerRadius: 8)) } .hoverEffect(.lift) + .background { + if tool.selection == .photo { + Color.accentColor + .clipShape(.rect(cornerRadius: 8)) + .matchedGeometryEffect(id: "element.toolbar.bg", in: namespace) + } + if tool.selection != .photo { + Color.clear + .matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace) + } + } if tool.selection == .photo { HStack(spacing: 0) { Button { @@ -178,12 +218,15 @@ struct Toolbar: View { } .hoverEffect(.lift) } + .matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace) + .transition(.blurReplace.animation(.easeIn(duration: 0.1))) } } .background { if tool.selection == .photo { RoundedRectangle(cornerRadius: 8) .fill(Color.white.tertiary) + .transition(.move(edge: .leading).animation(.easeIn(duration: 0.1))) } } } @@ -191,6 +234,7 @@ struct Toolbar: View { RoundedRectangle(cornerRadius: 8) .fill(.regularMaterial) } + .transition(.move(edge: .top).combined(with: .blurReplace)) } var historyControl: some View { @@ -219,30 +263,6 @@ struct Toolbar: View { .transition(.move(edge: .top).combined(with: .blurReplace)) } - var lockButton: some View { - Button { - #warning("TODO: need to revisit toggale logic") - withAnimation { - canvas.locksCanvas.toggle() - } - } label: { - ZStack { - if canvas.locksCanvas { - Image(systemName: "lock.open") - .transition(.move(edge: .trailing).combined(with: .blurReplace)) - } else { - Image(systemName: "lock") - .transition(.move(edge: .leading).combined(with: .blurReplace)) - } - } - .contentShape(.circle) - .frame(width: size, height: size) - .background(.regularMaterial) - .clipShape(.rect(cornerRadius: 8)) - } - .hoverEffect(.lift) - } - func openCamera() { let status = AVCaptureDevice.authorizationStatus(for: .video) switch status {