diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index 643f935..26a2b03 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -101,8 +101,8 @@ 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 */; }; ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A852C19EE3900B96E12 /* ElementObject.swift */; }; ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A892C19EFB000B96E12 /* Element.swift */; }; @@ -242,8 +242,8 @@ 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 = ""; }; ECD12A852C19EE3900B96E12 /* ElementObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementObject.swift; sourceTree = ""; }; ECD12A892C19EFB000B96E12 /* Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = ""; }; @@ -589,6 +589,7 @@ EC86C5802C4010BE00C07D21 /* PhotoDock */ = { isa = PBXGroup; children = ( + ECC995A22C1E8F2800B2699A /* PhotoItem.swift */, EC86C5812C4010CC00C07D21 /* PhotoDock.swift */, ); path = PhotoDock; @@ -645,7 +646,6 @@ children = ( EC86C5802C4010BE00C07D21 /* PhotoDock */, ECDAC0792C318DAF0000ED77 /* ElementToolbar */, - ECBE52942C1D58F5006BDB3D /* PhotoPreview */, EC1B783B2BFA0AAC005A34E2 /* Toolbar */, EC5050082BF65D0500B4D86E /* Memo */, EC5050052BF65CCD00B4D86E /* PenDock */, @@ -907,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 = ( @@ -1039,6 +1030,7 @@ EC9AB09E2C1401A40076AF58 /* EraserObject.swift */, ECD12A852C19EE3900B96E12 /* ElementObject.swift */, ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */, + ECC4F38B2C4B9B63007EC227 /* PhotoFileObject.swift */, ); path = Objects; sourceTree = ""; @@ -1162,6 +1154,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 */, @@ -1240,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/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index e07f3aa..f65a3a5 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 = try context.existingObject(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..4ba5734 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,31 @@ 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 objectID = canvas?.objectID, let photoItem = bookmarkPhoto(of: resizedImage, and: image, in: dimension, with: objectID) else { return } + let _dimension = photoItem.dimension + 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] + file.createdAt = .now + file.photos = [] + file.graphicContext = graphicContext + graphicContext?.files.add(file) } } + 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 +214,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..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,9 +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 2030334..4f10a26 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(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..2a84429 100644 --- a/Memola/Features/Memo/PhotoDock/PhotoDock.swift +++ b/Memola/Features/Memo/PhotoDock/PhotoDock.swift @@ -11,6 +11,8 @@ import PhotosUI struct PhotoDock: View { @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @FetchRequest private var fileObjects: FetchedResults + private let size: CGFloat = 40 @ObservedObject private var tool: Tool @@ -18,22 +20,29 @@ 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(tool: Tool, canvas: Canvas) { self.tool = tool self.canvas = canvas + + 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) } var body: some View { Group { #if os(macOS) - photoOption + photoDock #else if horizontalSizeClass == .regular { - photoOption + photoDock } else { - compactPhotoOption + compactPhotoDock } #endif } @@ -41,10 +50,13 @@ struct PhotoDock: View { #if os(iOS) .fullScreenCover(isPresented: $opensCamera) { let image: Binding = Binding { - tool.selectedPhotoItem?.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() @@ -62,22 +74,55 @@ 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 newValue != nil { + .onChange(of: photosPickerItems) { oldValue, newValue in + if !newValue.isEmpty { 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) + for photoItem in newValue { + await createFile(for: photoItem) } - photosPickerItem = nil + saveFile() + photosPickerItems = [] + tool.isLoadingPhoto = false } } } } + 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 { - VStack(spacing: 0) { + HStack(spacing: 0) { #if os(iOS) Button { openCamera() @@ -89,9 +134,13 @@ 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) + #else .frame(width: size, height: size) + #endif .clipShape(.rect(cornerRadius: 8)) .contentShape(.rect(cornerRadius: 8)) } @@ -126,9 +175,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 { @@ -144,7 +190,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)) @@ -177,13 +223,87 @@ struct PhotoDock: View { #endif } } + } + + @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(showsIndicators: false) { + 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) } - .padding(.bottom, 10) - .frame(maxWidth: .infinity) - .transition(.move(edge: .bottom).combined(with: .blurReplace)) + } + + @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() { @@ -205,4 +325,20 @@ 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: canvas.object) + } + } + + private func saveFile() { + withPersistenceSync(\.backgroundContext) { context in + try context.saveIfNeeded() + withPersistenceSync(\.viewContext) { context in + try context.saveIfNeeded() + } + } + } } diff --git a/Memola/Features/Memo/PhotoPreview/PhotoItem.swift b/Memola/Features/Memo/PhotoDock/PhotoItem.swift similarity index 93% rename from Memola/Features/Memo/PhotoPreview/PhotoItem.swift rename to Memola/Features/Memo/PhotoDock/PhotoItem.swift index 821d7fb..36ba860 100644 --- a/Memola/Features/Memo/PhotoPreview/PhotoItem.swift +++ b/Memola/Features/Memo/PhotoDock/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 deleted file mode 100644 index 35e7a17..0000000 --- a/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift +++ /dev/null @@ -1,55 +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 - - init(photoItem: PhotoItem, tool: Tool) { - self.photoItem = photoItem - self.tool = tool - } - - var body: some View { - Image(image: photoItem.previewImage) - .resizable() - .scaledToFit() - .frame(width: horizontalSizeClass == .compact ? 80 : nil, height: horizontalSizeClass == .compact ? nil : 100) - .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) - } -} 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/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..3c7ce84 --- /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 = bookmark?.getBookmarkURL() 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 @@ +