From d9ef99bc2237c74175f52d585f0914ba778bb3aa Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sat, 20 Jul 2024 17:32:45 +0700 Subject: [PATCH 1/4] feat: implement photo dock --- Memola.xcodeproj/project.pbxproj | 4 + Memola/Canvas/Contexts/GraphicContext.swift | 17 ++-- Memola/Canvas/Core/Canvas.swift | 4 +- Memola/Canvas/Elements/Photo/Photo.swift | 4 +- Memola/Canvas/Tool/Core/Tool.swift | 46 +++++----- .../ViewController/CanvasViewController.swift | 8 +- .../Dashboard/Details/Memos/MemosView.swift | 1 + .../Memo/ElementToolbar/ElementToolbar.swift | 2 - Memola/Features/Memo/Memo/MemoView.swift | 16 +--- Memola/Features/Memo/PenDock/PenDock.swift | 38 +++++++- .../Features/Memo/PhotoDock/PhotoDock.swift | 91 ++++++++++++++++--- .../Memo/PhotoPreview/PhotoItem.swift | 1 - .../Memo/PhotoPreview/PhotoPreview.swift | 15 ++- .../Objects/GraphicContextObject.swift | 1 + Memola/Persistence/Objects/MemoObject.swift | 4 + .../Persistence/Objects/PhotoFileObject.swift | 40 ++++++++ Memola/Persistence/Objects/PhotoObject.swift | 4 +- .../MemolaModel.xcdatamodel/contents | 10 ++ 18 files changed, 230 insertions(+), 76 deletions(-) create mode 100644 Memola/Persistence/Objects/PhotoFileObject.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 643f935..8356b79 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -103,6 +103,7 @@ ECA739082BE623F300A4542E /* PenDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDock.swift */; }; ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */; }; ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529A2C1D94A4006BDB3D /* CameraView.swift */; }; + ECC4F38C2C4B9B63007EC227 /* PhotoFileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC4F38B2C4B9B63007EC227 /* PhotoFileObject.swift */; }; ECC995A32C1E8F2800B2699A /* PhotoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC995A22C1E8F2800B2699A /* PhotoItem.swift */; }; ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A852C19EE3900B96E12 /* ElementObject.swift */; }; ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A892C19EFB000B96E12 /* Element.swift */; }; @@ -244,6 +245,7 @@ ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = ""; }; ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPreview.swift; sourceTree = ""; }; ECBE529A2C1D94A4006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; + ECC4F38B2C4B9B63007EC227 /* PhotoFileObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoFileObject.swift; sourceTree = ""; }; ECC995A22C1E8F2800B2699A /* PhotoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoItem.swift; sourceTree = ""; }; ECD12A852C19EE3900B96E12 /* ElementObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementObject.swift; sourceTree = ""; }; ECD12A892C19EFB000B96E12 /* Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = ""; }; @@ -1039,6 +1041,7 @@ EC9AB09E2C1401A40076AF58 /* EraserObject.swift */, ECD12A852C19EE3900B96E12 /* ElementObject.swift */, ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */, + ECC4F38B2C4B9B63007EC227 /* PhotoFileObject.swift */, ); path = Objects; sourceTree = ""; @@ -1162,6 +1165,7 @@ ECA738DA2BE60FF100A4542E /* CacheRenderPass.swift in Sources */, ECF7B2DA2C39169C004D2C57 /* Float++.swift in Sources */, ECA738CD2BE60F2F00A4542E /* PointGridContext.swift in Sources */, + ECC4F38C2C4B9B63007EC227 /* PhotoFileObject.swift in Sources */, EC6E3BDE2C43D5A500DD20F3 /* SidebarVisibility.swift in Sources */, ECFA15202BEF21EF00455818 /* MemoObject.swift in Sources */, ECE883C12C00C9CB0045C53D /* StrokeStyle.swift in Sources */, diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index e07f3aa..604e496 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -153,7 +153,7 @@ extension GraphicContext { } } case 1: - guard let photo = element.photo, photo.imageURL != nil else { return } + guard let photo = element.photo, photo.file?.imageURL != nil else { return } let _photo = Photo(object: photo) tree.insert(_photo.element, in: _photo.photoBox) default: @@ -312,23 +312,26 @@ extension GraphicContext { // MARK: - Photo extension GraphicContext { - func insertPhoto(at point: CGPoint, photoItem: PhotoItem) -> Photo { - let size = photoItem.getDimension() + func insertPhoto(at point: CGPoint, photoFile: PhotoFileObject) -> Photo { + let size = photoFile.photoDimension() let origin = point let bounds = [origin.x - size.width / 2, origin.y - size.height / 2, origin.x + size.width / 2, origin.y + size.height / 2] - let photo = Photo(url: photoItem.id, size: size, origin: origin, bounds: bounds, createdAt: .now, bookmark: photoItem.bookmark) + let photo = Photo(url: photoFile.imageURL, size: size, origin: origin, bounds: bounds, createdAt: .now, bookmark: photoFile.bookmark) tree.insert(photo.element, in: photo.photoBox) + let photoFileID = photoFile.objectID withPersistence(\.backgroundContext) { [weak _photo = photo, weak graphicContext = object] context in - guard let _photo else { return } + guard let _photo, let photoFile = context.object(with: photoFileID) as? PhotoFileObject else { + return + } let photo = PhotoObject(\.backgroundContext) - photo.imageURL = _photo.url photo.bounds = _photo.bounds photo.width = _photo.size.width photo.originY = _photo.origin.y photo.originX = _photo.origin.x photo.height = _photo.size.height photo.createdAt = _photo.createdAt - photo.bookmark = _photo.bookmark + photo.file = photoFile + photoFile.photos?.add(photo) let element = ElementObject(\.backgroundContext) element.createdAt = _photo.createdAt element.type = 1 diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index ddb9f3b..b7d065b 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -221,8 +221,8 @@ extension Canvas { // MARK: - Photo extension Canvas { - func insertPhoto(at point: CGPoint, photoItem: PhotoItem) -> Photo { - graphicContext.insertPhoto(at: point, photoItem: photoItem) + func insertPhoto(at point: CGPoint, photoFile: PhotoFileObject) -> Photo { + graphicContext.insertPhoto(at: point, photoFile: photoFile) } } diff --git a/Memola/Canvas/Elements/Photo/Photo.swift b/Memola/Canvas/Elements/Photo/Photo.swift index ed6a77c..9333297 100644 --- a/Memola/Canvas/Elements/Photo/Photo.swift +++ b/Memola/Canvas/Elements/Photo/Photo.swift @@ -37,12 +37,12 @@ final class Photo: @unchecked Sendable, Equatable { convenience init(object: PhotoObject) { self.init( - url: object.imageURL, + url: object.file?.imageURL, size: .init(width: object.width, height: object.height), origin: .init(x: object.originX, y: object.originY), bounds: object.bounds, createdAt: object.createdAt ?? .now, - bookmark: object.bookmark + bookmark: object.file?.bookmark ) self.object = object } diff --git a/Memola/Canvas/Tool/Core/Tool.swift b/Memola/Canvas/Tool/Core/Tool.swift index 12b1e2d..2d9c48f 100644 --- a/Memola/Canvas/Tool/Core/Tool.swift +++ b/Memola/Canvas/Tool/Core/Tool.swift @@ -19,7 +19,7 @@ final class Tool: NSObject, ObservableObject { @Published var selectedPen: Pen? @Published var draggedPen: Pen? // MARK: - Photo - @Published var selectedPhotoItem: PhotoItem? + @Published var selectedPhotoFile: PhotoFileObject? @Published var isLoadingPhoto: Bool = false @Published var selection: ToolSelection = .hand @@ -128,15 +128,32 @@ final class Tool: NSObject, ObservableObject { } } - func selectPhoto(_ image: Platform.Image, for canvasID: NSManagedObjectID) { + func createFile(_ image: Platform.Image, with canvas: CanvasObject) { guard let (resizedImage, dimension) = resizePhoto(of: image) else { return } - let photoItem = bookmarkPhoto(of: resizedImage, and: image, in: dimension, with: canvasID) - withAnimation { - selectedPhotoItem = photoItem - isLoadingPhoto = false + guard let photoItem = bookmarkPhoto(of: resizedImage, and: image, in: dimension, with: canvas.objectID) else { return } + let _dimension = photoItem.dimension + let graphicContext = canvas.graphicContext + withPersistence(\.viewContext) { [weak graphicContext = graphicContext] context in + let file = PhotoFileObject(\.viewContext) + file.imageURL = photoItem.id + file.bookmark = photoItem.bookmark + file.dimension = [_dimension.width, _dimension.height] + file.createdAt = .now + file.photos = [] + file.graphicContext = graphicContext + graphicContext?.files.add(file) + try context.saveIfNeeded() } } + func selectPhoto(_ photoFile: PhotoFileObject) { + selectedPhotoFile = photoFile + } + + func unselectPhoto() { + selectedPhotoFile = nil + } + private func resizePhoto(of image: Platform.Image) -> (Platform.Image, CGSize)? { let targetSize = CGSize(width: 512, height: 512) let size = image.size @@ -198,25 +215,10 @@ final class Tool: NSObject, ObservableObject { var photoBookmark: PhotoItem? do { let bookmark = try file.bookmarkData(options: .minimalBookmark) - photoBookmark = PhotoItem(id: file, image: image, previewImage: previewImage, dimension: dimension, bookmark: bookmark) + photoBookmark = PhotoItem(id: file, image: image, dimension: dimension, bookmark: bookmark) } catch { NSLog("[Memola] - \(error.localizedDescription)") } return photoBookmark } - - func unselectPhoto() { - guard let photoItem = selectedPhotoItem else { return } - let fileManager = FileManager.default - if let url = photoItem.bookmark.getBookmarkURL() { - do { - try fileManager.removeItem(at: url) - } catch { - NSLog("[Memola] - \(error.localizedDescription)") - } - } - withAnimation { - selectedPhotoItem = nil - } - } } diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 4aa65eb..02b96f0 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -329,17 +329,15 @@ extension CanvasViewController { } @objc private func recognizeTapGesture(_ gesture: Platform.TapGestureRecognizer) { - guard let photoItem = tool.selectedPhotoItem else { return } - withAnimation { - tool.selectedPhotoItem = nil - } + guard let photoFile = tool.selectedPhotoFile else { return } + tool.selectedPhotoFile = nil #if os(macOS) let pointInLeftBottomOrigin = gesture.location(in: drawingView) let point = CGPoint(x: pointInLeftBottomOrigin.x, y: drawingView.bounds.height - pointInLeftBottomOrigin.y) #else let point = gesture.location(in: drawingView) #endif - let photo = canvas.insertPhoto(at: point.muliply(by: drawingView.ratio), photoItem: photoItem) + let photo = canvas.insertPhoto(at: point.muliply(by: drawingView.ratio), photoFile: photoFile) history.addUndo(.photo(photo)) drawingView.draw() } diff --git a/Memola/Features/Dashboard/Details/Memos/MemosView.swift b/Memola/Features/Dashboard/Details/Memos/MemosView.swift index 95935d8..f9925af 100644 --- a/Memola/Features/Dashboard/Details/Memos/MemosView.swift +++ b/Memola/Features/Dashboard/Details/Memos/MemosView.swift @@ -237,6 +237,7 @@ struct MemosView: View { markerPenObjects.first?.isSelected = true let graphicContextObject = GraphicContextObject(\.viewContext) + graphicContextObject.files = [] graphicContextObject.elements = [] memoObject.canvas = canvasObject diff --git a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift index b8f3387..c98ceb8 100644 --- a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift +++ b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift @@ -167,6 +167,4 @@ struct ElementToolbar: View { .frame(maxWidth: .infinity) .transition(.move(edge: .bottom).combined(with: .blurReplace)) } - - } diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index 2030334..e4aaeaf 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -75,14 +75,7 @@ struct MemoView: View { case .pen: PenDock(tool: tool, canvas: canvas) case .photo: - 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)) - } - } - .frame(maxWidth: .infinity, alignment: .trailing) + PhotoDock(memo: memo, tool: tool, canvas: canvas) default: EmptyView() } @@ -99,13 +92,6 @@ struct MemoView: View { switch tool.selection { case .pen: PenDock(tool: tool, canvas: canvas) - .transition(.move(edge: .bottom).combined(with: .blurReplace)) - case .photo: - if let photoItem = tool.selectedPhotoItem { - PhotoPreview(photoItem: photoItem, tool: tool) - .frame(maxWidth: .infinity, alignment: .trailing) - .transition(.move(edge: .trailing)) - } default: EmptyView() } diff --git a/Memola/Features/Memo/PenDock/PenDock.swift b/Memola/Features/Memo/PenDock/PenDock.swift index f11f4d1..3079abe 100644 --- a/Memola/Features/Memo/PenDock/PenDock.swift +++ b/Memola/Features/Memo/PenDock/PenDock.swift @@ -38,7 +38,7 @@ struct PenDock: View { VStack(alignment: .trailing, spacing: 5) { penPropertyTool penItemList - .frame(maxWidth: proxy.size.width * 0.4) + .frame(maxHeight: proxy.size.height * 0.4) } .fixedSize() .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing) @@ -187,6 +187,40 @@ struct PenDock: View { .padding(.leading, 10) .contentShape(.rect) .contextMenu(if: pen.strokeStyle != .eraser) { + #if os(macOS) + 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) + #else ControlGroup { Button { tool.selectPen(pen) @@ -222,12 +256,12 @@ struct PenDock: View { .disabled(tool.markers.count <= 1) } .controlGroupStyle(.menu) + #endif } preview: { penPreview(pen) .drawingGroup() #if os(iOS) .contentShape(.contextMenuPreview, .rect(cornerRadius: 10)) - #else #endif } .onDrag(if: pen.strokeStyle != .eraser) { diff --git a/Memola/Features/Memo/PhotoDock/PhotoDock.swift b/Memola/Features/Memo/PhotoDock/PhotoDock.swift index 66f4116..9d1c469 100644 --- a/Memola/Features/Memo/PhotoDock/PhotoDock.swift +++ b/Memola/Features/Memo/PhotoDock/PhotoDock.swift @@ -11,6 +11,9 @@ import PhotosUI struct PhotoDock: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @FetchRequest private var fileObjects: FetchedResults + + private let memo: MemoObject private let size: CGFloat = 40 @ObservedObject private var tool: Tool @@ -20,15 +23,30 @@ struct PhotoDock: View { @State private var isCameraAccessDenied: Bool = false @State private var photosPickerItem: PhotosPickerItem? - init(tool: Tool, canvas: Canvas) { + init(memo: MemoObject, tool: Tool, canvas: Canvas) { + self.memo = memo self.tool = tool self.canvas = canvas + + let predicate: NSPredicate = NSPredicate(format: "graphicContext = %@", memo.canvas.graphicContext) + let descriptors: [SortDescriptor] = [SortDescriptor(\.createdAt)] + self._fileObjects = FetchRequest(sortDescriptors: descriptors, predicate: predicate) } var body: some View { Group { #if os(macOS) - photoOption + GeometryReader { proxy in + VStack(alignment: .trailing, spacing: 5) { + photoOption + photoItemGrid + .frame(minHeight: proxy.size.height * 0.2, maxHeight: proxy.size.height * 0.4) + } + .fixedSize() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing) + } + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) #else if horizontalSizeClass == .regular { photoOption @@ -41,7 +59,7 @@ struct PhotoDock: View { #if os(iOS) .fullScreenCover(isPresented: $opensCamera) { let image: Binding = Binding { - tool.selectedPhotoItem?.image + tool.selectedPhotoFile?.image } set: { image in guard let image else { return } tool.selectPhoto(image, for: canvas.canvasID) @@ -63,21 +81,19 @@ struct PhotoDock: View { } #endif .onChange(of: photosPickerItem) { oldValue, newValue in - if newValue != nil { + if let photoItem = newValue { 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) - } + await createFile(for: photoItem) photosPickerItem = nil + tool.isLoadingPhoto = false } } } } private var photoOption: some View { - VStack(spacing: 0) { + HStack(spacing: 0) { #if os(iOS) Button { openCamera() @@ -91,7 +107,11 @@ struct PhotoDock: View { #endif PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { Image(systemName: "photo.fill.on.rectangle.fill") + #if os(macOS) + .frame(width: size * 2, height: size) + #else .frame(width: size, height: size) + #endif .clipShape(.rect(cornerRadius: 8)) .contentShape(.rect(cornerRadius: 8)) } @@ -126,9 +146,6 @@ struct PhotoDock: View { RoundedRectangle(cornerRadius: 8) .fill(.regularMaterial) } - .padding(.trailing, 10) - .frame(maxHeight: .infinity) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) } private var compactPhotoOption: some View { @@ -186,6 +203,49 @@ struct PhotoDock: View { .transition(.move(edge: .bottom).combined(with: .blurReplace)) } + @ViewBuilder + private var photoItemGrid: some View { + let padding: CGFloat = 5 + let size = (self.size * 2 - (5 + padding * 2)) / 2 + let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: 5), count: 2) + ScrollView { + LazyVGrid(columns: columns, spacing: 5) { + ForEach(fileObjects) { file in + Group { + let previewSize = file.previewSize(size) + if let previewImage = file.previewImage { + Image(image: previewImage) + .resizable() + .frame(width: previewSize.width, height: previewSize.height) + .onTapGesture { + if tool.selectedPhotoFile == file { + tool.unselectPhoto() + } else { + tool.selectPhoto(file) + } + } + } else { + Color.gray.opacity(0.5) + } + } + .frame(width: size, height: size) + .clipShape(RoundedRectangle(cornerRadius: 5)) + .overlay { + if tool.selectedPhotoFile == file { + RoundedRectangle(cornerRadius: 5) + .stroke(Color.accentColor, lineWidth: 2.5) + } + } + } + } + .padding(padding) + } + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + } + } + private func openCamera() { let status = AVCaptureDevice.authorizationStatus(for: .video) switch status { @@ -205,4 +265,11 @@ struct PhotoDock: View { isCameraAccessDenied = true } } + + private func createFile(for photoItem: PhotosPickerItem) async { + let data = try? await photoItem.loadTransferable(type: Data.self) + if let data, let image = Platform.Image(data: data) { + tool.createFile(image, with: memo.canvas) + } + } } diff --git a/Memola/Features/Memo/PhotoPreview/PhotoItem.swift b/Memola/Features/Memo/PhotoPreview/PhotoItem.swift index 821d7fb..36ba860 100644 --- a/Memola/Features/Memo/PhotoPreview/PhotoItem.swift +++ b/Memola/Features/Memo/PhotoPreview/PhotoItem.swift @@ -11,7 +11,6 @@ import Foundation struct PhotoItem: Identifiable, Equatable { var id: URL let image: Platform.Image - let previewImage: Platform.Image let dimension: CGSize let bookmark: Data diff --git a/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift b/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift index 35e7a17..614b4b8 100644 --- a/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift +++ b/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift @@ -13,16 +13,23 @@ struct PhotoPreview: View { private let photoItem: PhotoItem @ObservedObject private var tool: Tool + private var previewWidth: CGFloat? { + horizontalSizeClass == .compact ? 80 : nil + } + + private var previewHeight: CGFloat? { + horizontalSizeClass == .compact ? nil : 100 + } + init(photoItem: PhotoItem, tool: Tool) { self.photoItem = photoItem self.tool = tool } var body: some View { - Image(image: photoItem.previewImage) + Image(image: photoItem.image) .resizable() - .scaledToFit() - .frame(width: horizontalSizeClass == .compact ? 80 : nil, height: horizontalSizeClass == .compact ? nil : 100) + .frame(width: previewWidth, height: previewHeight) .cornerRadius(5) .overlay { RoundedRectangle(cornerRadius: 5) @@ -33,7 +40,7 @@ struct PhotoPreview: View { .cornerRadius(5) .overlay(alignment: .topLeading) { Button { - tool.unselectPhoto() +// tool.unselectPhoto() } label: { Image(systemName: "xmark.circle.fill") .font(.title2) diff --git a/Memola/Persistence/Objects/GraphicContextObject.swift b/Memola/Persistence/Objects/GraphicContextObject.swift index ea26216..d2b457f 100644 --- a/Memola/Persistence/Objects/GraphicContextObject.swift +++ b/Memola/Persistence/Objects/GraphicContextObject.swift @@ -12,4 +12,5 @@ import Foundation final class GraphicContextObject: NSManagedObject { @NSManaged var canvas: CanvasObject? @NSManaged var elements: NSMutableOrderedSet + @NSManaged var files: NSMutableOrderedSet } diff --git a/Memola/Persistence/Objects/MemoObject.swift b/Memola/Persistence/Objects/MemoObject.swift index 73c2f42..5d36319 100644 --- a/Memola/Persistence/Objects/MemoObject.swift +++ b/Memola/Persistence/Objects/MemoObject.swift @@ -20,4 +20,8 @@ final class MemoObject: NSManagedObject, Identifiable { @NSManaged var preview: Data? @NSManaged var tool: ToolObject @NSManaged var canvas: CanvasObject + + var files: [PhotoFileObject] { + canvas.graphicContext.files.compactMap { $0 as? PhotoFileObject } + } } diff --git a/Memola/Persistence/Objects/PhotoFileObject.swift b/Memola/Persistence/Objects/PhotoFileObject.swift new file mode 100644 index 0000000..1ae9038 --- /dev/null +++ b/Memola/Persistence/Objects/PhotoFileObject.swift @@ -0,0 +1,40 @@ +// +// PhotoFileObject.swift +// Memola +// +// Created by Dscyre Scotti on 7/20/24. +// + +import CoreData +import Foundation + +@objc(PhotoFileObject) +final class PhotoFileObject: NSManagedObject, Identifiable { + @NSManaged var createdAt: Date? + @NSManaged var imageURL: URL? + @NSManaged var bookmark: Data? + @NSManaged var dimension: [CGFloat] + + @NSManaged var photos: NSMutableSet? + @NSManaged var graphicContext: GraphicContextObject? + + var previewImage: Platform.Image? { + guard let imageURL else { return nil } + guard let data = try? Data(contentsOf: imageURL, options: []) else { return nil } + return Platform.Image(data: data) + } + + func previewSize(_ size: CGFloat) -> (width: CGFloat, height: CGFloat) { + let minDimension = min(dimension[0], dimension[1]) + let width = size * dimension[0] / minDimension + let height = size * dimension[1] / minDimension + return (width, height) + } + + func photoDimension() -> CGSize { + let maxSize = max(dimension[0], dimension[1]) + let width = dimension[0] * 100 / maxSize + let height = dimension[1] * 100 / maxSize + return CGSize(width: width, height: height) + } +} diff --git a/Memola/Persistence/Objects/PhotoObject.swift b/Memola/Persistence/Objects/PhotoObject.swift index ac697c8..1d6baab 100644 --- a/Memola/Persistence/Objects/PhotoObject.swift +++ b/Memola/Persistence/Objects/PhotoObject.swift @@ -16,7 +16,7 @@ final class PhotoObject: NSManagedObject { @NSManaged var height: CGFloat @NSManaged var bounds: [CGFloat] @NSManaged var createdAt: Date? - @NSManaged var imageURL: URL? - @NSManaged var bookmark: Data? + + @NSManaged var file: PhotoFileObject? @NSManaged var element: ElementObject? } diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index 317085d..10ff997 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -26,6 +26,7 @@ + @@ -46,6 +47,14 @@ + + + + + + + + @@ -56,6 +65,7 @@ + From eee1cbe01be7a61780c4b13ce770dc3484b712f8 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sat, 20 Jul 2024 17:35:07 +0700 Subject: [PATCH 2/4] feat: remove unused photo preview view --- Memola.xcodeproj/project.pbxproj | 14 +---- .../PhotoItem.swift | 0 .../Memo/PhotoPreview/PhotoPreview.swift | 62 ------------------- 3 files changed, 1 insertion(+), 75 deletions(-) rename Memola/Features/Memo/{PhotoPreview => PhotoDock}/PhotoItem.swift (100%) delete mode 100644 Memola/Features/Memo/PhotoPreview/PhotoPreview.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 8356b79..26a2b03 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -101,7 +101,6 @@ ECA738E42BE6110800A4542E /* Drawable.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738E32BE6110800A4542E /* Drawable.swift */; }; ECA738FC2BE61C5200A4542E /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA738FB2BE61C5200A4542E /* Persistence.swift */; }; ECA739082BE623F300A4542E /* PenDock.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA739072BE623F300A4542E /* PenDock.swift */; }; - ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */; }; ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529A2C1D94A4006BDB3D /* CameraView.swift */; }; ECC4F38C2C4B9B63007EC227 /* PhotoFileObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC4F38B2C4B9B63007EC227 /* PhotoFileObject.swift */; }; ECC995A32C1E8F2800B2699A /* PhotoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC995A22C1E8F2800B2699A /* PhotoItem.swift */; }; @@ -243,7 +242,6 @@ ECA738E32BE6110800A4542E /* Drawable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Drawable.swift; sourceTree = ""; }; ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = ""; }; - ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPreview.swift; sourceTree = ""; }; ECBE529A2C1D94A4006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; ECC4F38B2C4B9B63007EC227 /* PhotoFileObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoFileObject.swift; sourceTree = ""; }; ECC995A22C1E8F2800B2699A /* PhotoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoItem.swift; sourceTree = ""; }; @@ -591,6 +589,7 @@ EC86C5802C4010BE00C07D21 /* PhotoDock */ = { isa = PBXGroup; children = ( + ECC995A22C1E8F2800B2699A /* PhotoItem.swift */, EC86C5812C4010CC00C07D21 /* PhotoDock.swift */, ); path = PhotoDock; @@ -647,7 +646,6 @@ children = ( EC86C5802C4010BE00C07D21 /* PhotoDock */, ECDAC0792C318DAF0000ED77 /* ElementToolbar */, - ECBE52942C1D58F5006BDB3D /* PhotoPreview */, EC1B783B2BFA0AAC005A34E2 /* Toolbar */, EC5050082BF65D0500B4D86E /* Memo */, EC5050052BF65CCD00B4D86E /* PenDock */, @@ -909,15 +907,6 @@ path = Core; sourceTree = ""; }; - ECBE52942C1D58F5006BDB3D /* PhotoPreview */ = { - isa = PBXGroup; - children = ( - ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */, - ECC995A22C1E8F2800B2699A /* PhotoItem.swift */, - ); - path = PhotoPreview; - sourceTree = ""; - }; ECBE529B2C1D94A4006BDB3D /* CameraView */ = { isa = PBXGroup; children = ( @@ -1244,7 +1233,6 @@ EC2002D72C4160EF002EBD5F /* EditCommands.swift in Sources */, ECF7B2DF2C39169C004D2C57 /* simd_float4x4++.swift in Sources */, ECF7B2D02C39169C004D2C57 /* Array++.swift in Sources */, - ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */, ECA738DE2BE610A000A4542E /* ViewPortRenderPass.swift in Sources */, EC8F54AC2C2ACDA8001C7C74 /* GridMode.swift in Sources */, EC2002D92C4161ED002EBD5F /* ViewCommands.swift in Sources */, diff --git a/Memola/Features/Memo/PhotoPreview/PhotoItem.swift b/Memola/Features/Memo/PhotoDock/PhotoItem.swift similarity index 100% rename from Memola/Features/Memo/PhotoPreview/PhotoItem.swift rename to Memola/Features/Memo/PhotoDock/PhotoItem.swift diff --git a/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift b/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift deleted file mode 100644 index 614b4b8..0000000 --- a/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// PhotoPreview.swift -// Memola -// -// Created by Dscyre Scotti on 6/15/24. -// - -import SwiftUI - -struct PhotoPreview: View { - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - - private let photoItem: PhotoItem - @ObservedObject private var tool: Tool - - private var previewWidth: CGFloat? { - horizontalSizeClass == .compact ? 80 : nil - } - - private var previewHeight: CGFloat? { - horizontalSizeClass == .compact ? nil : 100 - } - - init(photoItem: PhotoItem, tool: Tool) { - self.photoItem = photoItem - self.tool = tool - } - - var body: some View { - Image(image: photoItem.image) - .resizable() - .frame(width: previewWidth, height: previewHeight) - .cornerRadius(5) - .overlay { - RoundedRectangle(cornerRadius: 5) - .stroke(Color.gray, lineWidth: 0.2) - } - .padding(10) - .background(.regularMaterial) - .cornerRadius(5) - .overlay(alignment: .topLeading) { - Button { -// tool.unselectPhoto() - } label: { - Image(systemName: "xmark.circle.fill") - .font(.title2) - .padding(1) - .contentShape(.circle) - .background { - Circle() - .fill(.white) - } - } - .foregroundStyle(.red) - #if os(iOS) - .hoverEffect(.lift) - #endif - .offset(x: -12, y: -12) - } - .padding(10) - } -} From a8ea08f63d30b281f58ad406277334603e9436ce Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sat, 20 Jul 2024 17:42:01 +0700 Subject: [PATCH 3/4] feat: allow multiple photo selection --- Memola/Features/Memo/PhotoDock/PhotoDock.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Memola/Features/Memo/PhotoDock/PhotoDock.swift b/Memola/Features/Memo/PhotoDock/PhotoDock.swift index 9d1c469..1097606 100644 --- a/Memola/Features/Memo/PhotoDock/PhotoDock.swift +++ b/Memola/Features/Memo/PhotoDock/PhotoDock.swift @@ -21,7 +21,7 @@ struct PhotoDock: View { @State private var opensCamera: Bool = false @State private var isCameraAccessDenied: Bool = false - @State private var photosPickerItem: PhotosPickerItem? + @State private var photosPickerItems: [PhotosPickerItem] = [] init(memo: MemoObject, tool: Tool, canvas: Canvas) { self.memo = memo @@ -80,12 +80,14 @@ struct PhotoDock: View { 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 let photoItem = newValue { + .onChange(of: photosPickerItems) { oldValue, newValue in + if !newValue.isEmpty { Task { tool.isLoadingPhoto = true - await createFile(for: photoItem) - photosPickerItem = nil + for photoItem in newValue { + await createFile(for: photoItem) + } + photosPickerItems = [] tool.isLoadingPhoto = false } } @@ -105,7 +107,7 @@ struct PhotoDock: View { } .hoverEffect(.lift) #endif - PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { + PhotosPicker(selection: $photosPickerItems, matching: .images, preferredItemEncoding: .compatible) { Image(systemName: "photo.fill.on.rectangle.fill") #if os(macOS) .frame(width: size * 2, height: size) @@ -161,7 +163,7 @@ struct PhotoDock: View { } .hoverEffect(.lift) #endif - PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) { + PhotosPicker(selection: $photosPickerItems, matching: .images, preferredItemEncoding: .compatible) { Image(systemName: "photo.fill.on.rectangle.fill") .frame(width: size, height: size) .clipShape(.rect(cornerRadius: 8)) From 798beaab854c7c56dc8fae7bf1a9a28558904846 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 21 Jul 2024 15:47:13 +0700 Subject: [PATCH 4/4] feat: refine photo dock design for compact layout --- Memola/Canvas/Contexts/GraphicContext.swift | 2 +- Memola/Canvas/Tool/Core/Tool.swift | 11 +- .../Memo/ElementToolbar/ElementToolbar.swift | 5 +- Memola/Features/Memo/Memo/MemoView.swift | 2 +- .../Features/Memo/PhotoDock/PhotoDock.swift | 123 ++++++++++++++---- Memola/Persistence/Core/Persistence.swift | 3 +- .../Persistence/Objects/PhotoFileObject.swift | 2 +- 7 files changed, 106 insertions(+), 42 deletions(-) diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 604e496..f65a3a5 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -320,7 +320,7 @@ extension GraphicContext { tree.insert(photo.element, in: photo.photoBox) let photoFileID = photoFile.objectID withPersistence(\.backgroundContext) { [weak _photo = photo, weak graphicContext = object] context in - guard let _photo, let photoFile = context.object(with: photoFileID) as? PhotoFileObject else { + guard let _photo, let photoFile = try context.existingObject(with: photoFileID) as? PhotoFileObject else { return } let photo = PhotoObject(\.backgroundContext) diff --git a/Memola/Canvas/Tool/Core/Tool.swift b/Memola/Canvas/Tool/Core/Tool.swift index 2d9c48f..4ba5734 100644 --- a/Memola/Canvas/Tool/Core/Tool.swift +++ b/Memola/Canvas/Tool/Core/Tool.swift @@ -128,13 +128,13 @@ final class Tool: NSObject, ObservableObject { } } - func createFile(_ image: Platform.Image, with canvas: CanvasObject) { + func createFile(_ image: Platform.Image, with canvas: CanvasObject?) { guard let (resizedImage, dimension) = resizePhoto(of: image) else { return } - guard let photoItem = bookmarkPhoto(of: resizedImage, and: image, in: dimension, with: canvas.objectID) else { return } + guard let objectID = canvas?.objectID, let photoItem = bookmarkPhoto(of: resizedImage, and: image, in: dimension, with: objectID) else { return } let _dimension = photoItem.dimension - let graphicContext = canvas.graphicContext - withPersistence(\.viewContext) { [weak graphicContext = graphicContext] context in - let file = PhotoFileObject(\.viewContext) + let graphicContext = canvas?.graphicContext + withPersistenceSync(\.backgroundContext) { [weak graphicContext = graphicContext] context in + let file = PhotoFileObject(\.backgroundContext) file.imageURL = photoItem.id file.bookmark = photoItem.bookmark file.dimension = [_dimension.width, _dimension.height] @@ -142,7 +142,6 @@ final class Tool: NSObject, ObservableObject { file.photos = [] file.graphicContext = graphicContext graphicContext?.files.add(file) - try context.saveIfNeeded() } } diff --git a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift index c98ceb8..875e508 100644 --- a/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift +++ b/Memola/Features/Memo/ElementToolbar/ElementToolbar.swift @@ -33,9 +33,6 @@ struct ElementToolbar: View { ZStack(alignment: .bottom) { if tool.selection == .photo { PhotoDock(tool: tool, canvas: canvas) - .padding(.bottom, 10) - .frame(maxWidth: .infinity) - .transition(.move(edge: .bottom).combined(with: .blurReplace)) } else { compactToolbar } @@ -164,7 +161,7 @@ struct ElementToolbar: View { .fill(.regularMaterial) } .padding(10) - .frame(maxWidth: .infinity) + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) .transition(.move(edge: .bottom).combined(with: .blurReplace)) } } diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index e4aaeaf..4f10a26 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -75,7 +75,7 @@ struct MemoView: View { case .pen: PenDock(tool: tool, canvas: canvas) case .photo: - PhotoDock(memo: memo, tool: tool, canvas: canvas) + PhotoDock(tool: tool, canvas: canvas) default: EmptyView() } diff --git a/Memola/Features/Memo/PhotoDock/PhotoDock.swift b/Memola/Features/Memo/PhotoDock/PhotoDock.swift index 1097606..2a84429 100644 --- a/Memola/Features/Memo/PhotoDock/PhotoDock.swift +++ b/Memola/Features/Memo/PhotoDock/PhotoDock.swift @@ -13,7 +13,6 @@ struct PhotoDock: View { @FetchRequest private var fileObjects: FetchedResults - private let memo: MemoObject private let size: CGFloat = 40 @ObservedObject private var tool: Tool @@ -23,12 +22,14 @@ struct PhotoDock: View { @State private var isCameraAccessDenied: Bool = false @State private var photosPickerItems: [PhotosPickerItem] = [] - init(memo: MemoObject, tool: Tool, canvas: Canvas) { - self.memo = memo + init(tool: Tool, canvas: Canvas) { self.tool = tool self.canvas = canvas - let predicate: NSPredicate = NSPredicate(format: "graphicContext = %@", memo.canvas.graphicContext) + var predicate: NSPredicate? + if let canvasObject = canvas.object { + predicate = NSPredicate(format: "graphicContext = %@", canvasObject.graphicContext) + } let descriptors: [SortDescriptor] = [SortDescriptor(\.createdAt)] self._fileObjects = FetchRequest(sortDescriptors: descriptors, predicate: predicate) } @@ -36,22 +37,12 @@ struct PhotoDock: View { var body: some View { Group { #if os(macOS) - GeometryReader { proxy in - VStack(alignment: .trailing, spacing: 5) { - photoOption - photoItemGrid - .frame(minHeight: proxy.size.height * 0.2, maxHeight: proxy.size.height * 0.4) - } - .fixedSize() - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing) - } - .padding(10) - .transition(.move(edge: .trailing).combined(with: .blurReplace)) + photoDock #else if horizontalSizeClass == .regular { - photoOption + photoDock } else { - compactPhotoOption + compactPhotoDock } #endif } @@ -59,10 +50,13 @@ struct PhotoDock: View { #if os(iOS) .fullScreenCover(isPresented: $opensCamera) { let image: Binding = Binding { - tool.selectedPhotoFile?.image + .none } set: { image in guard let image else { return } - tool.selectPhoto(image, for: canvas.canvasID) + tool.isLoadingPhoto = true + tool.createFile(image, with: canvas.object) + saveFile() + tool.isLoadingPhoto = false } CameraView(image: image, canvas: canvas) .ignoresSafeArea() @@ -87,6 +81,7 @@ struct PhotoDock: View { for photoItem in newValue { await createFile(for: photoItem) } + saveFile() photosPickerItems = [] tool.isLoadingPhoto = false } @@ -94,6 +89,38 @@ struct PhotoDock: View { } } + private var photoDock: some View { + GeometryReader { proxy in + VStack(alignment: .trailing, spacing: 5) { + photoOption + photoItemGrid + .frame(minHeight: proxy.size.height * 0.2, maxHeight: proxy.size.height * 0.4) + } + .fixedSize() + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing) + } + .padding(10) + .transition(.move(edge: .trailing).combined(with: .blurReplace)) + } + + private var compactPhotoDock: some View { + GeometryReader { proxy in + HStack(spacing: 0) { + compactPhotoItemList + compactPhotoOption + } + .fixedSize(horizontal: false, vertical: true) + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.regularMaterial) + } + .frame(maxWidth: min(proxy.size.height, proxy.size.width), maxHeight: .infinity, alignment: .bottom) + .frame(maxWidth: .infinity) + } + .padding(10) + .transition(.move(edge: .bottom).combined(with: .blurReplace)) + } + private var photoOption: some View { HStack(spacing: 0) { #if os(iOS) @@ -196,13 +223,6 @@ struct PhotoDock: View { #endif } } - .background { - RoundedRectangle(cornerRadius: 8) - .fill(.regularMaterial) - } - .padding(.bottom, 10) - .frame(maxWidth: .infinity) - .transition(.move(edge: .bottom).combined(with: .blurReplace)) } @ViewBuilder @@ -210,7 +230,7 @@ struct PhotoDock: View { let padding: CGFloat = 5 let size = (self.size * 2 - (5 + padding * 2)) / 2 let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: 5), count: 2) - ScrollView { + ScrollView(showsIndicators: false) { LazyVGrid(columns: columns, spacing: 5) { ForEach(fileObjects) { file in Group { @@ -248,6 +268,44 @@ struct PhotoDock: View { } } + @ViewBuilder + private var compactPhotoItemList: some View { + let padding: CGFloat = 5 + let size = self.size - padding * 2 + ScrollView(.horizontal, showsIndicators: false) { + LazyHStack(spacing: 5) { + ForEach(fileObjects) { file in + Group { + let previewSize = file.previewSize(size) + if let previewImage = file.previewImage { + Image(image: previewImage) + .resizable() + .frame(width: previewSize.width, height: previewSize.height) + .onTapGesture { + if tool.selectedPhotoFile == file { + tool.unselectPhoto() + } else { + tool.selectPhoto(file) + } + } + } else { + Color.gray.opacity(0.5) + } + } + .frame(width: size, height: size) + .clipShape(RoundedRectangle(cornerRadius: 5)) + .overlay { + if tool.selectedPhotoFile == file { + RoundedRectangle(cornerRadius: 5) + .stroke(Color.accentColor, lineWidth: 2.5) + } + } + } + } + .padding(padding) + } + } + private func openCamera() { let status = AVCaptureDevice.authorizationStatus(for: .video) switch status { @@ -271,7 +329,16 @@ struct PhotoDock: View { private func createFile(for photoItem: PhotosPickerItem) async { let data = try? await photoItem.loadTransferable(type: Data.self) if let data, let image = Platform.Image(data: data) { - tool.createFile(image, with: memo.canvas) + tool.createFile(image, with: canvas.object) + } + } + + private func saveFile() { + withPersistenceSync(\.backgroundContext) { context in + try context.saveIfNeeded() + withPersistenceSync(\.viewContext) { context in + try context.saveIfNeeded() + } } } } diff --git a/Memola/Persistence/Core/Persistence.swift b/Memola/Persistence/Core/Persistence.swift index e34a888..894e9db 100644 --- a/Memola/Persistence/Core/Persistence.swift +++ b/Memola/Persistence/Core/Persistence.swift @@ -20,7 +20,8 @@ final class Persistence { }() lazy var backgroundContext: NSManagedObjectContext = { - let context = persistentContainer.newBackgroundContext() + let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType) + context.parent = viewContext context.undoManager = nil context.automaticallyMergesChangesFromParent = true return context diff --git a/Memola/Persistence/Objects/PhotoFileObject.swift b/Memola/Persistence/Objects/PhotoFileObject.swift index 1ae9038..3c7ce84 100644 --- a/Memola/Persistence/Objects/PhotoFileObject.swift +++ b/Memola/Persistence/Objects/PhotoFileObject.swift @@ -19,7 +19,7 @@ final class PhotoFileObject: NSManagedObject, Identifiable { @NSManaged var graphicContext: GraphicContextObject? var previewImage: Platform.Image? { - guard let imageURL else { return nil } + guard let imageURL = bookmark?.getBookmarkURL() else { return nil } guard let data = try? Data(contentsOf: imageURL, options: []) else { return nil } return Platform.Image(data: data) }