feat: fine tune memo view toolbar for pen element
@@ -111,6 +111,7 @@
|
|||||||
ECD12A912C1B04EA00B96E12 /* PhotoRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */; };
|
ECD12A912C1B04EA00B96E12 /* PhotoRenderPass.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */; };
|
||||||
ECD12A932C1B062000B96E12 /* Photo.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A922C1B062000B96E12 /* Photo.metal */; };
|
ECD12A932C1B062000B96E12 /* Photo.metal in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A922C1B062000B96E12 /* Photo.metal */; };
|
||||||
ECD12A952C1B1FA200B96E12 /* PhotoVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */; };
|
ECD12A952C1B1FA200B96E12 /* PhotoVertex.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */; };
|
||||||
|
ECDAC07B2C318DBC0000ED77 /* ElementToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECDAC07A2C318DBC0000ED77 /* ElementToolbar.swift */; };
|
||||||
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BC2C00AA170045C53D /* EraserStroke.swift */; };
|
ECE883BD2C00AA170045C53D /* EraserStroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BC2C00AA170045C53D /* EraserStroke.swift */; };
|
||||||
ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BE2C00AB440045C53D /* Stroke.swift */; };
|
ECE883BF2C00AB440045C53D /* Stroke.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883BE2C00AB440045C53D /* Stroke.swift */; };
|
||||||
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */; };
|
ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */; };
|
||||||
@@ -230,6 +231,7 @@
|
|||||||
ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoRenderPass.swift; sourceTree = "<group>"; };
|
ECD12A902C1B04EA00B96E12 /* PhotoRenderPass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoRenderPass.swift; sourceTree = "<group>"; };
|
||||||
ECD12A922C1B062000B96E12 /* Photo.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Photo.metal; sourceTree = "<group>"; };
|
ECD12A922C1B062000B96E12 /* Photo.metal */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.metal; path = Photo.metal; sourceTree = "<group>"; };
|
||||||
ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoVertex.swift; sourceTree = "<group>"; };
|
ECD12A942C1B1FA200B96E12 /* PhotoVertex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoVertex.swift; sourceTree = "<group>"; };
|
||||||
|
ECDAC07A2C318DBC0000ED77 /* ElementToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementToolbar.swift; sourceTree = "<group>"; };
|
||||||
ECE883BC2C00AA170045C53D /* EraserStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserStroke.swift; sourceTree = "<group>"; };
|
ECE883BC2C00AA170045C53D /* EraserStroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EraserStroke.swift; sourceTree = "<group>"; };
|
||||||
ECE883BE2C00AB440045C53D /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = "<group>"; };
|
ECE883BE2C00AB440045C53D /* Stroke.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stroke.swift; sourceTree = "<group>"; };
|
||||||
ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeStyle.swift; sourceTree = "<group>"; };
|
ECE883C02C00C9CB0045C53D /* StrokeStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StrokeStyle.swift; sourceTree = "<group>"; };
|
||||||
@@ -506,6 +508,7 @@
|
|||||||
ECA7387B2BE5EF3500A4542E /* Memo */ = {
|
ECA7387B2BE5EF3500A4542E /* Memo */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
ECDAC0792C318DAF0000ED77 /* ElementToolbar */,
|
||||||
ECBE52942C1D58F5006BDB3D /* PhotoPreview */,
|
ECBE52942C1D58F5006BDB3D /* PhotoPreview */,
|
||||||
EC1B783B2BFA0AAC005A34E2 /* Toolbar */,
|
EC1B783B2BFA0AAC005A34E2 /* Toolbar */,
|
||||||
EC5050082BF65D0500B4D86E /* Memo */,
|
EC5050082BF65D0500B4D86E /* Memo */,
|
||||||
@@ -834,6 +837,14 @@
|
|||||||
path = Photo;
|
path = Photo;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
ECDAC0792C318DAF0000ED77 /* ElementToolbar */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
ECDAC07A2C318DBC0000ED77 /* ElementToolbar.swift */,
|
||||||
|
);
|
||||||
|
path = ElementToolbar;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
ECE883B82C009DC30045C53D /* Strokes */ = {
|
ECE883B82C009DC30045C53D /* Strokes */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1031,6 +1042,7 @@
|
|||||||
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
ECA738C92BE60EF700A4542E /* GraphicContext.swift in Sources */,
|
||||||
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
ECA738F62BE612B700A4542E /* MTLDevice++.swift in Sources */,
|
||||||
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
|
EC0D14212BF79C73009BFE5F /* ToolObject.swift in Sources */,
|
||||||
|
ECDAC07B2C318DBC0000ED77 /* ElementToolbar.swift in Sources */,
|
||||||
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */,
|
EC50500D2BF6674400B4D86E /* OnDragViewModifier.swift in Sources */,
|
||||||
EC9AB09F2C1401A40076AF58 /* EraserObject.swift in Sources */,
|
EC9AB09F2C1401A40076AF58 /* EraserObject.swift in Sources */,
|
||||||
ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */,
|
ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import Foundation
|
|||||||
|
|
||||||
protocol PenStyle {
|
protocol PenStyle {
|
||||||
var icon: (base: String, tip: String?) { get }
|
var icon: (base: String, tip: String?) { get }
|
||||||
|
var compactIcon: (base: String, tip: String?) { get }
|
||||||
var textureName: String? { get }
|
var textureName: String? { get }
|
||||||
var thickness: (min: CGFloat, max: CGFloat) { get }
|
var thickness: (min: CGFloat, max: CGFloat) { get }
|
||||||
var thicknessSteps: [CGFloat] { get }
|
var thicknessSteps: [CGFloat] { get }
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import Foundation
|
|||||||
struct EraserPenStyle: PenStyle {
|
struct EraserPenStyle: PenStyle {
|
||||||
var icon: (base: String, tip: String?) = ("eraser", nil)
|
var icon: (base: String, tip: String?) = ("eraser", nil)
|
||||||
|
|
||||||
|
var compactIcon: (base: String, tip: String?) = ("eraser-compact", nil)
|
||||||
|
|
||||||
var textureName: String? = nil
|
var textureName: String? = nil
|
||||||
|
|
||||||
var thickness: (min: CGFloat, max: CGFloat) = (0.5, 30)
|
var thickness: (min: CGFloat, max: CGFloat) = (0.5, 30)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import Foundation
|
|||||||
struct MarkerPenStyle: PenStyle {
|
struct MarkerPenStyle: PenStyle {
|
||||||
var icon: (base: String, tip: String?) = ("marker-base", "marker-tip")
|
var icon: (base: String, tip: String?) = ("marker-base", "marker-tip")
|
||||||
|
|
||||||
|
var compactIcon: (base: String, tip: String?) = ("marker-base-compact", "marker-tip-compact")
|
||||||
|
|
||||||
var textureName: String? = "point-texture"
|
var textureName: String? = "point-texture"
|
||||||
|
|
||||||
var thickness: (min: CGFloat, max: CGFloat) = (0.5, 30)
|
var thickness: (min: CGFloat, max: CGFloat) = (0.5, 30)
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
//
|
||||||
|
// ElementToolbar.swift
|
||||||
|
// Memola
|
||||||
|
//
|
||||||
|
// Created by Dscyre Scotti on 6/30/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import PhotosUI
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
struct ElementToolbar: View {
|
||||||
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||||
|
let size: CGFloat
|
||||||
|
@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 horizontalSizeClass == .regular {
|
||||||
|
regularToolbar
|
||||||
|
} else {
|
||||||
|
ZStack(alignment: .bottomLeading) {
|
||||||
|
compactToolbar
|
||||||
|
if tool.selection == .photo {
|
||||||
|
photoOption
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.transition(.move(edge: .bottom).combined(with: .blurReplace))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.fullScreenCover(isPresented: $opensCamera) {
|
||||||
|
let image: Binding<UIImage?> = 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: UIApplication.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") {
|
||||||
|
UIApplication.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.")
|
||||||
|
}
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
photosPickerItem = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var regularToolbar: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
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)
|
||||||
|
.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.selectTool(.photo)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "photo")
|
||||||
|
.contentShape(.circle)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.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 {
|
||||||
|
photoOption
|
||||||
|
.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).combined(with: .opacity).animation(.easeIn(duration: 0.1)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
}
|
||||||
|
.transition(.move(edge: .top).combined(with: .blurReplace))
|
||||||
|
}
|
||||||
|
|
||||||
|
var compactToolbar: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
tool.selectTool(.pen)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "pencil")
|
||||||
|
.fontWeight(.heavy)
|
||||||
|
.contentShape(.circle)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
tool.selectTool(.photo)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "photo")
|
||||||
|
.contentShape(.circle)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
}
|
||||||
|
.transition(.move(edge: .bottom).combined(with: .blurReplace))
|
||||||
|
.padding(10)
|
||||||
|
}
|
||||||
|
|
||||||
|
var photoOption: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Button {
|
||||||
|
openCamera()
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "camera.fill")
|
||||||
|
.contentShape(.circle)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) {
|
||||||
|
Image(systemName: "photo.fill.on.rectangle.fill")
|
||||||
|
.contentShape(.circle)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import SwiftUI
|
|||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
struct MemoView: View {
|
struct MemoView: View {
|
||||||
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||||
|
|
||||||
@StateObject var tool: Tool
|
@StateObject var tool: Tool
|
||||||
@StateObject var canvas: Canvas
|
@StateObject var canvas: Canvas
|
||||||
@StateObject var history: History
|
@StateObject var history: History
|
||||||
@@ -28,6 +30,36 @@ struct MemoView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if horizontalSizeClass == .regular {
|
||||||
|
canvasView
|
||||||
|
} else {
|
||||||
|
compactCanvasView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.overlay(alignment: .top) {
|
||||||
|
Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history)
|
||||||
|
}
|
||||||
|
.disabled(textFieldState || tool.isLoadingPhoto)
|
||||||
|
.disabled(canvas.state == .loading || canvas.state == .closing)
|
||||||
|
.overlay {
|
||||||
|
switch canvas.state {
|
||||||
|
case .loading:
|
||||||
|
loadingIndicator("Loading memo...")
|
||||||
|
case .closing:
|
||||||
|
loadingIndicator("Saving memo...")
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.overlay {
|
||||||
|
if tool.isLoadingPhoto {
|
||||||
|
loadingIndicator("Loading photo...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var canvasView: some View {
|
||||||
CanvasView(tool: tool, canvas: canvas, history: history)
|
CanvasView(tool: tool, canvas: canvas, history: history)
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.overlay(alignment: .bottomTrailing) {
|
.overlay(alignment: .bottomTrailing) {
|
||||||
@@ -47,24 +79,29 @@ struct MemoView: View {
|
|||||||
.overlay(alignment: .bottomLeading) {
|
.overlay(alignment: .bottomLeading) {
|
||||||
zoomControl
|
zoomControl
|
||||||
}
|
}
|
||||||
.disabled(textFieldState || tool.isLoadingPhoto)
|
}
|
||||||
.overlay(alignment: .top) {
|
|
||||||
Toolbar(size: size, memo: memo, tool: tool, canvas: canvas, history: history)
|
var compactCanvasView: some View {
|
||||||
}
|
CanvasView(tool: tool, canvas: canvas, history: history)
|
||||||
.disabled(canvas.state == .loading || canvas.state == .closing)
|
.ignoresSafeArea()
|
||||||
.overlay {
|
.overlay(alignment: .bottom) {
|
||||||
switch canvas.state {
|
switch tool.selection {
|
||||||
case .loading:
|
case .pen:
|
||||||
loadingIndicator("Loading memo...")
|
PenDock(tool: tool, canvas: canvas, size: size)
|
||||||
case .closing:
|
.transition(.move(edge: .bottom))
|
||||||
loadingIndicator("Saving memo...")
|
case .photo:
|
||||||
|
if let photoItem = tool.selectedPhotoItem {
|
||||||
|
PhotoPreview(photoItem: photoItem, tool: tool)
|
||||||
|
.transition(.move(edge: .trailing))
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.overlay {
|
.overlay(alignment: .bottom) {
|
||||||
if tool.isLoadingPhoto {
|
if tool.selection == .hand {
|
||||||
loadingIndicator("Loading photo...")
|
ElementToolbar(size: size, tool: tool, canvas: canvas)
|
||||||
|
.transition(.move(edge: .bottom))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,41 +8,97 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct PenDock: View {
|
struct PenDock: View {
|
||||||
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||||
@ObservedObject var tool: Tool
|
@ObservedObject var tool: Tool
|
||||||
@ObservedObject var canvas: Canvas
|
@ObservedObject var canvas: Canvas
|
||||||
|
|
||||||
let size: CGFloat
|
let size: CGFloat
|
||||||
let width: CGFloat = 90
|
var width: CGFloat {
|
||||||
let height: CGFloat = 30
|
horizontalSizeClass == .compact ? 30 : 90
|
||||||
let factor: CGFloat = 0.9
|
}
|
||||||
|
var height: CGFloat {
|
||||||
|
horizontalSizeClass == .compact ? 90 : 30
|
||||||
|
}
|
||||||
|
var factor: CGFloat {
|
||||||
|
horizontalSizeClass == .compact ? 0.9 : 0.9
|
||||||
|
}
|
||||||
|
|
||||||
@State var refreshScrollId: UUID = UUID()
|
@State var refreshScrollId: UUID = UUID()
|
||||||
@State var opensColorPicker: Bool = false
|
@State var opensColorPicker: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottomTrailing) {
|
if horizontalSizeClass == .regular {
|
||||||
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))
|
}
|
||||||
|
} else {
|
||||||
|
ZStack(alignment: .bottomTrailing) {
|
||||||
|
if !canvas.locksCanvas {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
HStack(alignment: .bottom, spacing: 10) {
|
||||||
|
newPenButton
|
||||||
|
.frame(height: height * factor - 18)
|
||||||
|
compactPenItemList
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
compactPenPropertyTool
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.clipped()
|
||||||
|
.background(alignment: .bottom) {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(.regularMaterial)
|
||||||
|
.frame(height: height * factor - 18)
|
||||||
|
}
|
||||||
|
.padding([.horizontal, .bottom], 10)
|
||||||
|
.frame(maxWidth: min(proxy.size.height, proxy.size.width), maxHeight: .infinity, alignment: .bottom)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
.overlay(alignment: .bottom) {
|
||||||
|
Button {
|
||||||
|
withAnimation {
|
||||||
|
tool.selectTool(.hand)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "chevron.compact.down")
|
||||||
|
.font(.headline)
|
||||||
|
.frame(width: 80)
|
||||||
|
.padding(10)
|
||||||
|
.background(.regularMaterial)
|
||||||
|
.clipShape(.capsule)
|
||||||
|
.contentShape(.capsule)
|
||||||
|
}
|
||||||
|
.offset(y: 5)
|
||||||
|
}
|
||||||
|
.transition(.move(edge: .bottom).combined(with: .blurReplace))
|
||||||
|
}
|
||||||
|
lockButton
|
||||||
|
.frame(maxWidth: .infinity, alignment: .bottomTrailing)
|
||||||
|
.padding(10)
|
||||||
|
.offset(y: canvas.locksCanvas ? 0 : -(height * factor - size + 20))
|
||||||
|
.transition(.move(edge: .trailing).combined(with: .blurReplace))
|
||||||
}
|
}
|
||||||
lockButton
|
|
||||||
.padding(10)
|
|
||||||
.transition(.move(edge: .trailing).combined(with: .blurReplace))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
var penItemList: some View {
|
var penItemList: some View {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
ForEach(tool.pens) { pen in
|
ForEach(tool.pens) { pen in
|
||||||
penItemRow(pen)
|
penItem(pen)
|
||||||
.id(pen.id)
|
.id(pen.id)
|
||||||
.scrollTransition { content, phase in
|
.scrollTransition { content, phase in
|
||||||
content
|
content
|
||||||
@@ -75,7 +131,34 @@ struct PenDock: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func penItemRow(_ pen: Pen) -> some View {
|
@ViewBuilder
|
||||||
|
var compactPenItemList: some View {
|
||||||
|
ScrollViewReader { proxy in
|
||||||
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
|
LazyHStack(spacing: 0) {
|
||||||
|
ForEach(tool.pens) { pen in
|
||||||
|
compactPenItem(pen)
|
||||||
|
.id(pen.id)
|
||||||
|
.scrollTransition { content, phase in
|
||||||
|
content
|
||||||
|
.scaleEffect(phase.isIdentity ? 1 : 0.04, anchor: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.id(refreshScrollId)
|
||||||
|
}
|
||||||
|
.onReceive(tool.scrollPublisher) { id in
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
|
withAnimation {
|
||||||
|
proxy.scrollTo(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func penItem(_ pen: Pen) -> some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
penShadow(pen)
|
penShadow(pen)
|
||||||
if let tip = pen.style.icon.tip {
|
if let tip = pen.style.icon.tip {
|
||||||
@@ -88,7 +171,7 @@ struct PenDock: View {
|
|||||||
.resizable()
|
.resizable()
|
||||||
}
|
}
|
||||||
.frame(width: width * factor, height: height * factor)
|
.frame(width: width * factor, height: height * factor)
|
||||||
.padding(.vertical, 5)
|
.padding(.horizontal, 5)
|
||||||
.contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10)))
|
.contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10)))
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if tool.selectedPen !== pen {
|
if tool.selectedPen !== pen {
|
||||||
@@ -148,6 +231,79 @@ struct PenDock: View {
|
|||||||
.offset(x: tool.selectedPen === pen ? 0 : 25)
|
.offset(x: tool.selectedPen === pen ? 0 : 25)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func compactPenItem(_ pen: Pen) -> some View {
|
||||||
|
ZStack {
|
||||||
|
compactPenShadow(pen)
|
||||||
|
if let tip = pen.style.compactIcon.tip {
|
||||||
|
Image(tip)
|
||||||
|
.resizable()
|
||||||
|
.renderingMode(.template)
|
||||||
|
.foregroundStyle(Color.rgba(from: pen.rgba))
|
||||||
|
}
|
||||||
|
Image(pen.style.compactIcon.base)
|
||||||
|
.resizable()
|
||||||
|
}
|
||||||
|
.frame(width: width * factor, height: height * factor)
|
||||||
|
.padding(.top, 5)
|
||||||
|
.contentShape(.rect(cornerRadii: .init(topLeading: 10, bottomLeading: 10)))
|
||||||
|
.onTapGesture {
|
||||||
|
if tool.selectedPen !== pen {
|
||||||
|
tool.selectPen(pen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 10)
|
||||||
|
.contextMenu(if: pen.strokeStyle != .eraser) {
|
||||||
|
ControlGroup {
|
||||||
|
Button {
|
||||||
|
tool.selectPen(pen)
|
||||||
|
} label: {
|
||||||
|
Label(
|
||||||
|
title: { Text("Select") },
|
||||||
|
icon: { Image(systemName: "pencil.tip.crop.circle") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
let originalPen = pen
|
||||||
|
let pen = PenObject.createObject(\.viewContext, penStyle: originalPen.style)
|
||||||
|
pen.color = originalPen.rgba
|
||||||
|
pen.thickness = originalPen.thickness
|
||||||
|
pen.isSelected = true
|
||||||
|
pen.tool = tool.object
|
||||||
|
let _pen = Pen(object: pen)
|
||||||
|
tool.duplicatePen(_pen, of: originalPen)
|
||||||
|
} label: {
|
||||||
|
Label(
|
||||||
|
title: { Text("Duplicate") },
|
||||||
|
icon: { Image(systemName: "plus.square.on.square") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Button(role: .destructive) {
|
||||||
|
tool.removePen(pen)
|
||||||
|
} label: {
|
||||||
|
Label(
|
||||||
|
title: { Text("Remove") },
|
||||||
|
icon: { Image(systemName: "trash") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.disabled(tool.markers.count <= 1)
|
||||||
|
}
|
||||||
|
.controlGroupStyle(.menu)
|
||||||
|
} preview: {
|
||||||
|
penPreview(pen)
|
||||||
|
.drawingGroup()
|
||||||
|
.contentShape(.contextMenuPreview, .rect(cornerRadius: 10))
|
||||||
|
}
|
||||||
|
.onDrag(if: pen.strokeStyle != .eraser) {
|
||||||
|
tool.draggedPen = pen
|
||||||
|
return NSItemProvider(contentsOf: URL(string: pen.id)) ?? NSItemProvider()
|
||||||
|
} preview: {
|
||||||
|
penPreview(pen)
|
||||||
|
.contentShape(.dragPreview, .rect(cornerRadius: 10))
|
||||||
|
}
|
||||||
|
.onDrop(of: [.item], delegate: PenDropDelegate(id: pen.id, tool: tool, action: { refreshScrollId = UUID() }))
|
||||||
|
.offset(y: tool.selectedPen === pen ? 0 : 25)
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
var penPropertyTool: some View {
|
var penPropertyTool: some View {
|
||||||
if let pen = tool.selectedPen {
|
if let pen = tool.selectedPen {
|
||||||
@@ -170,6 +326,23 @@ struct PenDock: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
var compactPenPropertyTool: some View {
|
||||||
|
if let pen = tool.selectedPen {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
compactPenThicknessPicker(pen)
|
||||||
|
.frame(width: width)
|
||||||
|
.rotationEffect(.degrees(-90))
|
||||||
|
if pen.strokeStyle == .marker {
|
||||||
|
penColorPicker(pen)
|
||||||
|
.frame(width: width)
|
||||||
|
.transition(.move(edge: .trailing).combined(with: .opacity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: height * factor - 18)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func penColorPicker(_ pen: Pen) -> some View {
|
func penColorPicker(_ pen: Pen) -> some View {
|
||||||
Button {
|
Button {
|
||||||
opensColorPicker = true
|
opensColorPicker = true
|
||||||
@@ -191,12 +364,13 @@ struct PenDock: View {
|
|||||||
}
|
}
|
||||||
.background(baseColor)
|
.background(baseColor)
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
.frame(height: 25)
|
.frame(height: horizontalSizeClass == .compact ? 30 : 25)
|
||||||
.overlay {
|
.overlay {
|
||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 8)
|
||||||
.stroke(Color.gray, lineWidth: 0.4)
|
.stroke(Color.gray, lineWidth: 0.4)
|
||||||
}
|
}
|
||||||
.padding(0.2)
|
.padding(0.2)
|
||||||
|
.drawingGroup()
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.hoverEffect(.lift)
|
.hoverEffect(.lift)
|
||||||
@@ -250,19 +424,41 @@ struct PenDock: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
func compactPenThicknessPicker(_ pen: Pen) -> some View {
|
||||||
|
let minimum: CGFloat = pen.style.thickness.min
|
||||||
|
let maximum: CGFloat = pen.style.thickness.max
|
||||||
|
let start: CGFloat = 4
|
||||||
|
let end: CGFloat = 7
|
||||||
|
let selection = Binding(
|
||||||
|
get: { pen.thickness },
|
||||||
|
set: {
|
||||||
|
pen.thickness = $0
|
||||||
|
tool.objectWillChange.send()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Picker("", selection: selection) {
|
||||||
|
ForEach(pen.style.thicknessSteps, id: \.self) { step in
|
||||||
|
let size = ((step - minimum) * (end - start) / (maximum - minimum)) + start - (0.5 / step)
|
||||||
|
Circle()
|
||||||
|
.fill(.black)
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.frame(width: size + 2, height: size + 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hoverEffect(.lift)
|
||||||
|
.pickerStyle(.wheel)
|
||||||
|
.frame(width: 50, height: 30)
|
||||||
|
.onChange(of: pen.thickness) { _, _ in
|
||||||
|
withPersistence(\.viewContext) { context in
|
||||||
|
try context.saveIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var newPenButton: some View {
|
var newPenButton: some View {
|
||||||
Button {
|
Button {
|
||||||
let pen = PenObject.createObject(\.viewContext, penStyle: .marker)
|
createNewPen()
|
||||||
var selectedPen = tool.selectedPen
|
|
||||||
selectedPen = (selectedPen?.strokeStyle == .marker ? (selectedPen ?? tool.pens.last) : tool.pens.last)
|
|
||||||
if let color = selectedPen?.rgba {
|
|
||||||
pen.color = color
|
|
||||||
}
|
|
||||||
pen.isSelected = true
|
|
||||||
pen.tool = tool.object
|
|
||||||
pen.orderIndex = Int16(tool.pens.count)
|
|
||||||
let _pen = Pen(object: pen)
|
|
||||||
tool.addPen(_pen)
|
|
||||||
} label: {
|
} label: {
|
||||||
Image(systemName: "plus.circle.fill")
|
Image(systemName: "plus.circle.fill")
|
||||||
.font(.title2)
|
.font(.title2)
|
||||||
@@ -317,6 +513,30 @@ struct PenDock: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func compactPenShadow(_ pen: Pen) -> some View {
|
||||||
|
ZStack {
|
||||||
|
Group {
|
||||||
|
if let tip = pen.style.compactIcon.tip {
|
||||||
|
Image(tip)
|
||||||
|
.resizable()
|
||||||
|
.renderingMode(.template)
|
||||||
|
}
|
||||||
|
Image(pen.style.compactIcon.base)
|
||||||
|
.resizable()
|
||||||
|
.renderingMode(.template)
|
||||||
|
}
|
||||||
|
.foregroundStyle(.black.opacity(0.2))
|
||||||
|
.blur(radius: 3)
|
||||||
|
if let tip = pen.style.compactIcon.tip {
|
||||||
|
Image(tip)
|
||||||
|
.resizable()
|
||||||
|
.renderingMode(.template)
|
||||||
|
.foregroundStyle(Color(red: pen.rgba[0], green: pen.rgba[1], blue: pen.rgba[2]))
|
||||||
|
.blur(radius: 0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var lockButton: some View {
|
var lockButton: some View {
|
||||||
Button {
|
Button {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
@@ -332,4 +552,18 @@ struct PenDock: View {
|
|||||||
.hoverEffect(.lift)
|
.hoverEffect(.lift)
|
||||||
.contentTransition(.symbolEffect(.replace))
|
.contentTransition(.symbolEffect(.replace))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createNewPen() {
|
||||||
|
let pen = PenObject.createObject(\.viewContext, penStyle: .marker)
|
||||||
|
var selectedPen = tool.selectedPen
|
||||||
|
selectedPen = (selectedPen?.strokeStyle == .marker ? (selectedPen ?? tool.pens.last) : tool.pens.last)
|
||||||
|
if let color = selectedPen?.rgba {
|
||||||
|
pen.color = color
|
||||||
|
}
|
||||||
|
pen.isSelected = true
|
||||||
|
pen.tool = tool.object
|
||||||
|
pen.orderIndex = Int16(tool.pens.count)
|
||||||
|
let _pen = Pen(object: pen)
|
||||||
|
tool.addPen(_pen)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,11 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import PhotosUI
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
struct Toolbar: View {
|
struct Toolbar: View {
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||||
|
|
||||||
@ObservedObject var tool: Tool
|
@ObservedObject var tool: Tool
|
||||||
@ObservedObject var canvas: Canvas
|
@ObservedObject var canvas: Canvas
|
||||||
@@ -19,14 +18,9 @@ struct Toolbar: View {
|
|||||||
|
|
||||||
@State var title: String
|
@State var title: String
|
||||||
@State var memo: MemoObject
|
@State var memo: MemoObject
|
||||||
@State var opensCamera: Bool = false
|
|
||||||
@State var photosPickerItem: PhotosPickerItem?
|
|
||||||
@State var isCameraAccessDenied: Bool = false
|
|
||||||
|
|
||||||
@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) {
|
||||||
@@ -47,8 +41,8 @@ struct Toolbar: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
if !canvas.locksCanvas {
|
if !canvas.locksCanvas, horizontalSizeClass == .regular {
|
||||||
elementTool
|
ElementToolbar(size: size, tool: tool, canvas: canvas)
|
||||||
}
|
}
|
||||||
HStack(spacing: 5) {
|
HStack(spacing: 5) {
|
||||||
if !canvas.locksCanvas {
|
if !canvas.locksCanvas {
|
||||||
@@ -60,40 +54,6 @@ struct Toolbar: View {
|
|||||||
}
|
}
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.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)
|
|
||||||
}
|
|
||||||
photosPickerItem = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.fullScreenCover(isPresented: $opensCamera) {
|
|
||||||
let image: Binding<UIImage?> = 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: UIApplication.openSettingsURLString + "&path=CAMERA/\(String(describing: Bundle.main.bundleIdentifier))") {
|
|
||||||
UIApplication.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.")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var closeButton: some View {
|
var closeButton: some View {
|
||||||
@@ -116,7 +76,7 @@ struct Toolbar: View {
|
|||||||
.focused($textFieldState)
|
.focused($textFieldState)
|
||||||
.textFieldStyle(.plain)
|
.textFieldStyle(.plain)
|
||||||
.padding(.horizontal, size / 2.5)
|
.padding(.horizontal, size / 2.5)
|
||||||
.frame(width: 140, height: size)
|
.frame(width: horizontalSizeClass == .compact ? 100 : 140, height: size)
|
||||||
.background(.regularMaterial)
|
.background(.regularMaterial)
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
.clipShape(.rect(cornerRadius: 8))
|
||||||
.onChange(of: textFieldState) { oldValue, newValue in
|
.onChange(of: textFieldState) { oldValue, newValue in
|
||||||
@@ -135,110 +95,6 @@ struct Toolbar: View {
|
|||||||
.transition(.move(edge: .top).combined(with: .blurReplace))
|
.transition(.move(edge: .top).combined(with: .blurReplace))
|
||||||
}
|
}
|
||||||
|
|
||||||
var elementTool: some View {
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
Button {
|
|
||||||
withAnimation {
|
|
||||||
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)
|
|
||||||
.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.selectTool(.photo)
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "photo")
|
|
||||||
.contentShape(.circle)
|
|
||||||
.frame(width: size, height: size)
|
|
||||||
.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 {
|
|
||||||
openCamera()
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "camera.fill")
|
|
||||||
.contentShape(.circle)
|
|
||||||
.frame(width: size, height: size)
|
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
|
||||||
}
|
|
||||||
.hoverEffect(.lift)
|
|
||||||
PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) {
|
|
||||||
Image(systemName: "photo.fill.on.rectangle.fill")
|
|
||||||
.contentShape(.circle)
|
|
||||||
.frame(width: size, height: size)
|
|
||||||
.clipShape(.rect(cornerRadius: 8))
|
|
||||||
}
|
|
||||||
.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)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background {
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.fill(.regularMaterial)
|
|
||||||
}
|
|
||||||
.transition(.move(edge: .top).combined(with: .blurReplace))
|
|
||||||
}
|
|
||||||
|
|
||||||
var historyControl: some View {
|
var historyControl: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Button {
|
Button {
|
||||||
@@ -288,26 +144,7 @@ struct Toolbar: View {
|
|||||||
}
|
}
|
||||||
.hoverEffect(.lift)
|
.hoverEffect(.lift)
|
||||||
.contentTransition(.symbolEffect(.replace))
|
.contentTransition(.symbolEffect(.replace))
|
||||||
}
|
.transition(.move(edge: .top).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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func closeMemo() {
|
func closeMemo() {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "eraser.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "eraser@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "eraser@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 29 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "marker-base.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "marker-base@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "marker-base@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "marker-tip.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "marker-tip@2x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "marker-tip@3x.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 440 B |
|
After Width: | Height: | Size: 904 B |
|
After Width: | Height: | Size: 1.4 KiB |