Merge pull request #69 from dscyrescotti/feature/photo-dock

Implement photo dock
This commit is contained in:
Aye Chan
2024-07-21 16:54:01 +08:00
committed by GitHub
19 changed files with 300 additions and 154 deletions

View File

@@ -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 = "<group>"; };
ECA738FB2BE61C5200A4542E /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = "<group>"; };
ECA739072BE623F300A4542E /* PenDock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PenDock.swift; sourceTree = "<group>"; };
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPreview.swift; sourceTree = "<group>"; };
ECBE529A2C1D94A4006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
ECC4F38B2C4B9B63007EC227 /* PhotoFileObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoFileObject.swift; sourceTree = "<group>"; };
ECC995A22C1E8F2800B2699A /* PhotoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoItem.swift; sourceTree = "<group>"; };
ECD12A852C19EE3900B96E12 /* ElementObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementObject.swift; sourceTree = "<group>"; };
ECD12A892C19EFB000B96E12 /* Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = "<group>"; };
@@ -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 = "<group>";
};
ECBE52942C1D58F5006BDB3D /* PhotoPreview */ = {
isa = PBXGroup;
children = (
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */,
ECC995A22C1E8F2800B2699A /* PhotoItem.swift */,
);
path = PhotoPreview;
sourceTree = "<group>";
};
ECBE529B2C1D94A4006BDB3D /* CameraView */ = {
isa = PBXGroup;
children = (
@@ -1039,6 +1030,7 @@
EC9AB09E2C1401A40076AF58 /* EraserObject.swift */,
ECD12A852C19EE3900B96E12 /* ElementObject.swift */,
ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */,
ECC4F38B2C4B9B63007EC227 /* PhotoFileObject.swift */,
);
path = Objects;
sourceTree = "<group>";
@@ -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 */,

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}
}
}

View File

@@ -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()
}

View File

@@ -237,6 +237,7 @@ struct MemosView: View {
markerPenObjects.first?.isSelected = true
let graphicContextObject = GraphicContextObject(\.viewContext)
graphicContextObject.files = []
graphicContextObject.elements = []
memoObject.canvas = canvasObject

View File

@@ -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))
}
}

View File

@@ -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()
}

View File

@@ -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) {

View File

@@ -11,6 +11,8 @@ import PhotosUI
struct PhotoDock: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@FetchRequest private var fileObjects: FetchedResults<PhotoFileObject>
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<PhotoFileObject>] = [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<UIImage?> = 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()
}
}
}
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -12,4 +12,5 @@ import Foundation
final class GraphicContextObject: NSManagedObject {
@NSManaged var canvas: CanvasObject?
@NSManaged var elements: NSMutableOrderedSet
@NSManaged var files: NSMutableOrderedSet
}

View File

@@ -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 }
}
}

View File

@@ -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)
}
}

View File

@@ -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?
}

View File

@@ -26,6 +26,7 @@
<entity name="GraphicContextObject" representedClassName="GraphicContextObject" syncable="YES">
<relationship name="canvas" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="CanvasObject" inverseName="graphicContext" inverseEntity="CanvasObject"/>
<relationship name="elements" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="ElementObject" inverseName="graphicContext" inverseEntity="ElementObject"/>
<relationship name="files" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="PhotoFileObject" inverseName="graphicContext" inverseEntity="PhotoFileObject"/>
</entity>
<entity name="MemoObject" representedClassName="MemoObject" syncable="YES">
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
@@ -46,6 +47,14 @@
<attribute name="thickness" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="tool" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ToolObject" inverseName="pens" inverseEntity="ToolObject"/>
</entity>
<entity name="PhotoFileObject" representedClassName="PhotoFileObject" syncable="YES">
<attribute name="bookmark" optional="YES" attributeType="Binary"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="dimension" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
<attribute name="imageURL" optional="YES" attributeType="URI"/>
<relationship name="graphicContext" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GraphicContextObject" inverseName="files" inverseEntity="GraphicContextObject"/>
<relationship name="photos" toMany="YES" deletionRule="Cascade" destinationEntity="PhotoObject" inverseName="file" inverseEntity="PhotoObject"/>
</entity>
<entity name="PhotoObject" representedClassName="PhotoObject" syncable="YES">
<attribute name="bookmark" optional="YES" attributeType="Binary"/>
<attribute name="bounds" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
@@ -56,6 +65,7 @@
<attribute name="originY" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="width" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<relationship name="element" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ElementObject" inverseName="photo" inverseEntity="ElementObject"/>
<relationship name="file" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="PhotoFileObject" inverseName="photos" inverseEntity="PhotoFileObject"/>
</entity>
<entity name="QuadObject" representedClassName="QuadObject" syncable="YES">
<attribute name="color" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>