mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-05-17 13:17:03 +02:00
Merge pull request #50 from dscyrescotti/feature/toolbar-fine-tune
Fine tune memo canvas tool bar items
This commit is contained in:
@@ -20,8 +20,9 @@ public class Tool: NSObject, ObservableObject {
|
|||||||
@Published var draggedPen: Pen?
|
@Published var draggedPen: Pen?
|
||||||
// MARK: - Photo
|
// MARK: - Photo
|
||||||
@Published var selectedPhotoItem: PhotoItem?
|
@Published var selectedPhotoItem: PhotoItem?
|
||||||
|
@Published var isLoadingPhoto: Bool = false
|
||||||
|
|
||||||
@Published var selection: ToolSelection = .none
|
@Published var selection: ToolSelection = .hand
|
||||||
|
|
||||||
let scrollPublisher = PassthroughSubject<String, Never>()
|
let scrollPublisher = PassthroughSubject<String, Never>()
|
||||||
var markers: [Pen] {
|
var markers: [Pen] {
|
||||||
@@ -32,6 +33,10 @@ public class Tool: NSObject, ObservableObject {
|
|||||||
self.object = object
|
self.object = object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func selectTool(_ selection: ToolSelection) {
|
||||||
|
self.selection = selection
|
||||||
|
}
|
||||||
|
|
||||||
func load() {
|
func load() {
|
||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
@@ -116,6 +121,7 @@ public class Tool: NSObject, ObservableObject {
|
|||||||
func selectPhoto(_ image: UIImage, for canvasID: NSManagedObjectID) {
|
func selectPhoto(_ image: UIImage, for canvasID: NSManagedObjectID) {
|
||||||
guard let (resizedImage, dimension) = resizePhoto(of: image) else { return }
|
guard let (resizedImage, dimension) = resizePhoto(of: image) else { return }
|
||||||
let photoItem = bookmarkPhoto(of: resizedImage, in: dimension, with: canvasID)
|
let photoItem = bookmarkPhoto(of: resizedImage, in: dimension, with: canvasID)
|
||||||
|
isLoadingPhoto = false
|
||||||
withAnimation {
|
withAnimation {
|
||||||
selectedPhotoItem = photoItem
|
selectedPhotoItem = photoItem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum ToolSelection: Equatable {
|
enum ToolSelection: Equatable {
|
||||||
case none
|
case hand
|
||||||
case pen
|
case pen
|
||||||
case photo
|
case photo
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ extension CanvasViewController {
|
|||||||
let enablesDrawing: Bool
|
let enablesDrawing: Bool
|
||||||
let enablesPhotoInsertion: Bool
|
let enablesPhotoInsertion: Bool
|
||||||
switch selection {
|
switch selection {
|
||||||
case .none:
|
case .hand:
|
||||||
enablesScrolling = true
|
enablesScrolling = true
|
||||||
enablesDrawing = false
|
enablesDrawing = false
|
||||||
enablesPhotoInsertion = false
|
enablesPhotoInsertion = false
|
||||||
@@ -362,7 +362,6 @@ extension CanvasViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func lockModeChanged(_ state: Bool) {
|
func lockModeChanged(_ state: Bool) {
|
||||||
scrollView.isScrollEnabled = !state
|
|
||||||
scrollView.pinchGestureRecognizer?.isEnabled = !state
|
scrollView.pinchGestureRecognizer?.isEnabled = !state
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ struct MemoView: View {
|
|||||||
.overlay(alignment: .bottomTrailing) {
|
.overlay(alignment: .bottomTrailing) {
|
||||||
switch tool.selection {
|
switch tool.selection {
|
||||||
case .pen:
|
case .pen:
|
||||||
PenDock(tool: tool, canvas: canvas)
|
PenDock(tool: tool, canvas: canvas, size: size)
|
||||||
.transition(.move(edge: .trailing))
|
.transition(.move(edge: .trailing))
|
||||||
case .photo:
|
case .photo:
|
||||||
if let photoItem = tool.selectedPhotoItem {
|
if let photoItem = tool.selectedPhotoItem {
|
||||||
@@ -46,7 +46,7 @@ struct MemoView: View {
|
|||||||
.overlay(alignment: .bottomLeading) {
|
.overlay(alignment: .bottomLeading) {
|
||||||
zoomControl
|
zoomControl
|
||||||
}
|
}
|
||||||
.disabled(textFieldState)
|
.disabled(textFieldState || tool.isLoadingPhoto)
|
||||||
.overlay(alignment: .top) {
|
.overlay(alignment: .top) {
|
||||||
Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history)
|
Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history)
|
||||||
}
|
}
|
||||||
@@ -61,6 +61,11 @@ struct MemoView: View {
|
|||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.overlay {
|
||||||
|
if tool.isLoadingPhoto {
|
||||||
|
loadingIndicator("Loading photo...")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -96,6 +101,7 @@ struct MemoView: View {
|
|||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
.padding(10)
|
.padding(10)
|
||||||
}
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
.transition(.move(edge: .bottom).combined(with: .blurReplace))
|
.transition(.move(edge: .bottom).combined(with: .blurReplace))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ struct PenDock: View {
|
|||||||
@ObservedObject var tool: Tool
|
@ObservedObject var tool: Tool
|
||||||
@ObservedObject var canvas: Canvas
|
@ObservedObject var canvas: Canvas
|
||||||
|
|
||||||
|
let size: CGFloat
|
||||||
let width: CGFloat = 90
|
let width: CGFloat = 90
|
||||||
let height: CGFloat = 30
|
let height: CGFloat = 30
|
||||||
let factor: CGFloat = 0.9
|
let factor: CGFloat = 0.9
|
||||||
@@ -19,15 +20,20 @@ struct PenDock: View {
|
|||||||
@State var opensColorPicker: Bool = false
|
@State var opensColorPicker: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if !canvas.locksCanvas {
|
ZStack(alignment: .bottomTrailing) {
|
||||||
VStack(alignment: .trailing) {
|
if !canvas.locksCanvas {
|
||||||
penPropertyTool
|
VStack(alignment: .trailing) {
|
||||||
penItemList
|
penPropertyTool
|
||||||
|
penItemList
|
||||||
|
}
|
||||||
|
.fixedSize()
|
||||||
|
.frame(maxHeight: .infinity)
|
||||||
|
.padding(10)
|
||||||
|
.transition(.move(edge: .trailing).combined(with: .blurReplace))
|
||||||
}
|
}
|
||||||
.fixedSize()
|
lockButton
|
||||||
.frame(maxHeight: .infinity)
|
.padding(10)
|
||||||
.padding(10)
|
.transition(.move(edge: .trailing).combined(with: .blurReplace))
|
||||||
.transition(.move(edge: .trailing).combined(with: .blurReplace))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +51,6 @@ struct PenDock: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
.padding(.leading, 40)
|
|
||||||
.id(refreshScrollId)
|
.id(refreshScrollId)
|
||||||
}
|
}
|
||||||
.onReceive(tool.scrollPublisher) { id in
|
.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()
|
.fixedSize()
|
||||||
.background(alignment: .trailing) {
|
.background(alignment: .trailing) {
|
||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
@@ -66,7 +71,7 @@ struct PenDock: View {
|
|||||||
.clipShape(.rect(cornerRadii: .init(bottomTrailing: 8, topTrailing: 8)))
|
.clipShape(.rect(cornerRadii: .init(bottomTrailing: 8, topTrailing: 8)))
|
||||||
.overlay(alignment: .bottomLeading) {
|
.overlay(alignment: .bottomLeading) {
|
||||||
newPenButton
|
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)
|
.frame(width: size + 2, height: size + 2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
.pickerStyle(.wheel)
|
.pickerStyle(.wheel)
|
||||||
.frame(width: width * factor - 18, height: 35)
|
.frame(width: width * factor - 18, height: 35)
|
||||||
.onChange(of: pen.thickness) { _, _ in
|
.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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ struct Toolbar: View {
|
|||||||
|
|
||||||
@FocusState var textFieldState: Bool
|
@FocusState var textFieldState: Bool
|
||||||
|
|
||||||
|
@Namespace var namespace
|
||||||
|
|
||||||
let size: CGFloat
|
let size: CGFloat
|
||||||
|
|
||||||
init(size: CGFloat, memo: MemoObject, tool: Tool, canvas: Canvas, history: History) {
|
init(size: CGFloat, memo: MemoObject, tool: Tool, canvas: Canvas, history: History) {
|
||||||
@@ -45,12 +47,13 @@ struct Toolbar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
elementTool
|
if !canvas.locksCanvas {
|
||||||
HStack(spacing: 5) {
|
elementTool
|
||||||
|
}
|
||||||
|
Group {
|
||||||
if !canvas.locksCanvas {
|
if !canvas.locksCanvas {
|
||||||
historyControl
|
historyControl
|
||||||
}
|
}
|
||||||
lockButton
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
}
|
}
|
||||||
@@ -59,6 +62,7 @@ struct Toolbar: View {
|
|||||||
.onChange(of: photosPickerItem) { oldValue, newValue in
|
.onChange(of: photosPickerItem) { oldValue, newValue in
|
||||||
if newValue != nil {
|
if newValue != nil {
|
||||||
Task {
|
Task {
|
||||||
|
tool.isLoadingPhoto = true
|
||||||
let data = try? await newValue?.loadTransferable(type: Data.self)
|
let data = try? await newValue?.loadTransferable(type: Data.self)
|
||||||
if let data, let image = UIImage(data: data) {
|
if let data, let image = UIImage(data: data) {
|
||||||
tool.selectPhoto(image, for: canvas.canvasID)
|
tool.selectPhoto(image, for: canvas.canvasID)
|
||||||
@@ -133,32 +137,68 @@ struct Toolbar: View {
|
|||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
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: {
|
} label: {
|
||||||
Image(systemName: "pencil")
|
Image(systemName: "pencil")
|
||||||
.fontWeight(.heavy)
|
.fontWeight(.heavy)
|
||||||
.contentShape(.circle)
|
.contentShape(.circle)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.background(tool.selection == .pen ? Color.accentColor : Color.clear)
|
|
||||||
.foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor)
|
.foregroundStyle(tool.selection == .pen ? Color.white : Color.accentColor)
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
}
|
}
|
||||||
.hoverEffect(.lift)
|
.hoverEffect(.lift)
|
||||||
|
.background {
|
||||||
|
if tool.selection == .pen {
|
||||||
|
Color.accentColor
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
.matchedGeometryEffect(id: "element.toolbar.bg", in: namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
tool.selection = tool.selection == .photo ? .none : .photo
|
tool.selectTool(.photo)
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "photo")
|
Image(systemName: "photo")
|
||||||
.contentShape(.circle)
|
.contentShape(.circle)
|
||||||
.frame(width: size, height: size)
|
.frame(width: size, height: size)
|
||||||
.background(tool.selection == .photo ? Color.accentColor : Color.clear)
|
|
||||||
.foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor)
|
.foregroundStyle(tool.selection == .photo ? Color.white : Color.accentColor)
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
}
|
}
|
||||||
.hoverEffect(.lift)
|
.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 {
|
if tool.selection == .photo {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Button {
|
Button {
|
||||||
@@ -178,12 +218,15 @@ struct Toolbar: View {
|
|||||||
}
|
}
|
||||||
.hoverEffect(.lift)
|
.hoverEffect(.lift)
|
||||||
}
|
}
|
||||||
|
.matchedGeometryEffect(id: "element.toolbar.photo.options", in: namespace)
|
||||||
|
.transition(.blurReplace.animation(.easeIn(duration: 0.1)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background {
|
.background {
|
||||||
if tool.selection == .photo {
|
if tool.selection == .photo {
|
||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
.fill(Color.white.tertiary)
|
.fill(Color.white.tertiary)
|
||||||
|
.transition(.move(edge: .leading).animation(.easeIn(duration: 0.1)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -191,6 +234,7 @@ struct Toolbar: View {
|
|||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
.fill(.regularMaterial)
|
.fill(.regularMaterial)
|
||||||
}
|
}
|
||||||
|
.transition(.move(edge: .top).combined(with: .blurReplace))
|
||||||
}
|
}
|
||||||
|
|
||||||
var historyControl: some View {
|
var historyControl: some View {
|
||||||
@@ -219,30 +263,6 @@ struct Toolbar: View {
|
|||||||
.transition(.move(edge: .top).combined(with: .blurReplace))
|
.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() {
|
func openCamera() {
|
||||||
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||||
switch status {
|
switch status {
|
||||||
|
|||||||
Reference in New Issue
Block a user