mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-03-12 13:31:42 +01:00
feat: implement photo dock
This commit is contained in:
@@ -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 = "<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>"; };
|
||||
@@ -1039,6 +1041,7 @@
|
||||
EC9AB09E2C1401A40076AF58 /* EraserObject.swift */,
|
||||
ECD12A852C19EE3900B96E12 /* ElementObject.swift */,
|
||||
ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */,
|
||||
ECC4F38B2C4B9B63007EC227 /* PhotoFileObject.swift */,
|
||||
);
|
||||
path = Objects;
|
||||
sourceTree = "<group>";
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -237,6 +237,7 @@ struct MemosView: View {
|
||||
markerPenObjects.first?.isSelected = true
|
||||
|
||||
let graphicContextObject = GraphicContextObject(\.viewContext)
|
||||
graphicContextObject.files = []
|
||||
graphicContextObject.elements = []
|
||||
|
||||
memoObject.canvas = canvasObject
|
||||
|
||||
@@ -167,6 +167,4 @@ struct ElementToolbar: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
.transition(.move(edge: .bottom).combined(with: .blurReplace))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -11,6 +11,9 @@ import PhotosUI
|
||||
struct PhotoDock: View {
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
|
||||
@FetchRequest private var fileObjects: FetchedResults<PhotoFileObject>
|
||||
|
||||
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<PhotoFileObject>] = [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<UIImage?> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -12,4 +12,5 @@ import Foundation
|
||||
final class GraphicContextObject: NSManagedObject {
|
||||
@NSManaged var canvas: CanvasObject?
|
||||
@NSManaged var elements: NSMutableOrderedSet
|
||||
@NSManaged var files: NSMutableOrderedSet
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
40
Memola/Persistence/Objects/PhotoFileObject.swift
Normal file
40
Memola/Persistence/Objects/PhotoFileObject.swift
Normal 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 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)
|
||||
}
|
||||
}
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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]"/>
|
||||
|
||||
Reference in New Issue
Block a user