mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-01-17 14:36:38 +01:00
feat: bookmark image file for secure access
This commit is contained in:
@@ -86,6 +86,7 @@
|
||||
ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */; };
|
||||
ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529A2C1D94A4006BDB3D /* CameraView.swift */; };
|
||||
ECBE529E2C1DAB21006BDB3D /* UIImage++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529D2C1DAB21006BDB3D /* UIImage++.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 */; };
|
||||
ECD12A8C2C1AEAA900B96E12 /* PhotoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */; };
|
||||
@@ -187,6 +188,7 @@
|
||||
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>"; };
|
||||
ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage++.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>"; };
|
||||
ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoObject.swift; sourceTree = "<group>"; };
|
||||
@@ -678,6 +680,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */,
|
||||
ECC995A22C1E8F2800B2699A /* PhotoItem.swift */,
|
||||
);
|
||||
path = PhotoPreview;
|
||||
sourceTree = "<group>";
|
||||
@@ -926,6 +929,7 @@
|
||||
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */,
|
||||
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
|
||||
EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */,
|
||||
ECC995A32C1E8F2800B2699A /* PhotoItem.swift in Sources */,
|
||||
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
|
||||
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
|
||||
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,
|
||||
|
||||
@@ -304,11 +304,11 @@ extension GraphicContext {
|
||||
|
||||
// MARK: - Photo
|
||||
extension GraphicContext {
|
||||
func insertPhoto(at point: CGPoint, url: URL) {
|
||||
func insertPhoto(at point: CGPoint, photoItem: PhotoItem) {
|
||||
let size = CGSize(width: 100, height: 100)
|
||||
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: url, size: size, origin: origin, bounds: bounds, createdAt: .now)
|
||||
let photo = Photo(url: photoItem.id, size: size, origin: origin, bounds: bounds, createdAt: .now, bookmark: photoItem.bookmark)
|
||||
tree.insert(photo.element, in: photo.photoBox)
|
||||
withPersistence(\.backgroundContext) { [_photo = photo, graphicContext = object] context in
|
||||
let photo = PhotoObject(\.backgroundContext)
|
||||
@@ -319,6 +319,7 @@ extension GraphicContext {
|
||||
photo.originX = _photo.origin.x
|
||||
photo.height = _photo.size.height
|
||||
photo.createdAt = _photo.createdAt
|
||||
photo.bookmark = _photo.bookmark
|
||||
let element = ElementObject(\.backgroundContext)
|
||||
element.createdAt = _photo.createdAt
|
||||
element.type = 1
|
||||
|
||||
@@ -128,20 +128,26 @@ extension Canvas {
|
||||
|
||||
// MARK: - Photo
|
||||
extension Canvas {
|
||||
func insertPhoto(at point: CGPoint, url: URL) {
|
||||
graphicContext.insertPhoto(at: point, url: url)
|
||||
func insertPhoto(at point: CGPoint, photoItem: PhotoItem) {
|
||||
graphicContext.insertPhoto(at: point, photoItem: photoItem)
|
||||
}
|
||||
|
||||
func savePhoto(_ data: Data) -> URL? {
|
||||
func bookmarkPhoto(of image: UIImage) -> PhotoItem? {
|
||||
guard let data = image.jpegData(compressionQuality: 1) else { return nil }
|
||||
let fileManager = FileManager.default
|
||||
guard let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
|
||||
guard let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
|
||||
return nil
|
||||
}
|
||||
let fileName = "\(UUID().uuidString)-\(Date.now.timeIntervalSince1970)"
|
||||
let folder = directory.appendingPathComponent(canvasID.uriRepresentation().lastPathComponent, conformingTo: .folder)
|
||||
if !fileManager.fileExists(atPath: folder.path()) {
|
||||
|
||||
if folder.startAccessingSecurityScopedResource(), !fileManager.fileExists(atPath: folder.path()) {
|
||||
do {
|
||||
try fileManager.createDirectory(at: folder, withIntermediateDirectories: true)
|
||||
folder.stopAccessingSecurityScopedResource()
|
||||
} catch {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
folder.stopAccessingSecurityScopedResource()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -152,7 +158,14 @@ extension Canvas {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
return nil
|
||||
}
|
||||
return file
|
||||
var photoBookmark: PhotoItem?
|
||||
do {
|
||||
let bookmark = try file.bookmarkData(options: .minimalBookmark)
|
||||
photoBookmark = PhotoItem(id: file, image: image, bookmark: bookmark)
|
||||
} catch {
|
||||
NSLog("[Memola] - \(error.localizedDescription)")
|
||||
}
|
||||
return photoBookmark
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
|
||||
var url: URL?
|
||||
var bounds: [CGFloat]
|
||||
var createdAt: Date
|
||||
var bookmark: Data?
|
||||
|
||||
var object: PhotoObject?
|
||||
|
||||
@@ -24,12 +25,13 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
|
||||
var vertexCount: Int = 0
|
||||
var vertexBuffer: MTLBuffer?
|
||||
|
||||
init(url: URL?, size: CGSize, origin: CGPoint, bounds: [CGFloat], createdAt: Date) {
|
||||
init(url: URL?, size: CGSize, origin: CGPoint, bounds: [CGFloat], createdAt: Date, bookmark: Data?) {
|
||||
self.size = size
|
||||
self.origin = origin
|
||||
self.url = url
|
||||
self.bounds = bounds
|
||||
self.createdAt = createdAt
|
||||
self.bookmark = bookmark
|
||||
generateVertices()
|
||||
}
|
||||
|
||||
@@ -39,7 +41,8 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
|
||||
size: .init(width: object.width, height: object.height),
|
||||
origin: .init(x: object.originX, y: object.originY),
|
||||
bounds: object.bounds,
|
||||
createdAt: object.createdAt ?? .now
|
||||
createdAt: object.createdAt ?? .now,
|
||||
bookmark: object.bookmark
|
||||
)
|
||||
self.object = object
|
||||
}
|
||||
@@ -56,6 +59,17 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
|
||||
PhotoVertex(x: maxX, y: maxY, textCoord: CGPoint(x: 1, y: 1)),
|
||||
]
|
||||
}
|
||||
|
||||
func getBookmarkURL() -> URL? {
|
||||
var isStale = false
|
||||
guard let bookmark else {
|
||||
return nil
|
||||
}
|
||||
guard let bookmarkURL = try? URL(resolvingBookmarkData: bookmark, options: .withoutUI, relativeTo: nil, bookmarkDataIsStale: &isStale) else {
|
||||
return nil
|
||||
}
|
||||
return bookmarkURL
|
||||
}
|
||||
}
|
||||
|
||||
extension Photo: Drawable {
|
||||
@@ -64,7 +78,7 @@ extension Photo: Drawable {
|
||||
vertexCount = vertices.endIndex
|
||||
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<PhotoVertex>.stride, options: [])
|
||||
}
|
||||
if texture == nil, let url {
|
||||
if texture == nil, let url = getBookmarkURL() {
|
||||
texture = Textures.createPhotoTexture(for: url, on: device)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class Tool: NSObject, ObservableObject {
|
||||
@Published var selectedPen: Pen?
|
||||
@Published var draggedPen: Pen?
|
||||
// MARK: - Photo
|
||||
@Published var selectedImageURL: URL?
|
||||
@Published var selectedPhotoItem: PhotoItem?
|
||||
|
||||
@Published var selection: ToolSelection = .none
|
||||
|
||||
|
||||
@@ -233,12 +233,12 @@ extension CanvasViewController {
|
||||
}
|
||||
|
||||
@objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) {
|
||||
guard let url = tool.selectedImageURL else { return }
|
||||
guard let photoItem = tool.selectedPhotoItem else { return }
|
||||
withAnimation {
|
||||
tool.selectedImageURL = nil
|
||||
tool.selectedPhotoItem = nil
|
||||
}
|
||||
let point = gesture.location(in: drawingView)
|
||||
canvas.insertPhoto(at: point.muliply(by: drawingView.ratio), url: url)
|
||||
canvas.insertPhoto(at: point.muliply(by: drawingView.ratio), photoItem: photoItem)
|
||||
drawingView.draw()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CameraView: UIViewControllerRepresentable {
|
||||
@Binding var url: URL?
|
||||
@Binding var image: UIImage?
|
||||
|
||||
@ObservedObject var canvas: Canvas
|
||||
|
||||
@@ -35,10 +35,7 @@ struct CameraView: UIViewControllerRepresentable {
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
let image = (info[.originalImage] as? UIImage)?.imageWithUpOrientation()
|
||||
if let image, let data = image.jpegData(compressionQuality: 1) {
|
||||
parent.url = parent.canvas.savePhoto(data)
|
||||
}
|
||||
parent.image = (info[.originalImage] as? UIImage)?.imageWithUpOrientation()
|
||||
parent.dismiss()
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,8 @@ struct MemoView: View {
|
||||
PenDock(tool: tool, canvas: canvas)
|
||||
.transition(.move(edge: .trailing))
|
||||
case .photo:
|
||||
if let url = tool.selectedImageURL {
|
||||
PhotoPreview(url: url, tool: tool)
|
||||
if let photoItem = tool.selectedPhotoItem {
|
||||
PhotoPreview(photoItem: photoItem, tool: tool)
|
||||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
default:
|
||||
|
||||
15
Memola/Features/Memo/PhotoPreview/PhotoItem.swift
Normal file
15
Memola/Features/Memo/PhotoPreview/PhotoItem.swift
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// PhotoItem.swift
|
||||
// Memola
|
||||
//
|
||||
// Created by Dscyre Scotti on 6/16/24.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Foundation
|
||||
|
||||
struct PhotoItem: Identifiable, Equatable {
|
||||
var id: URL
|
||||
let image: UIImage
|
||||
let bookmark: Data
|
||||
}
|
||||
@@ -8,15 +8,11 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PhotoPreview: View {
|
||||
let url: URL
|
||||
let photoItem: PhotoItem
|
||||
@ObservedObject var tool: Tool
|
||||
|
||||
var data: Data {
|
||||
(try? Data(contentsOf: url)) ?? Data()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Image(uiImage: UIImage(data: data) ?? UIImage())
|
||||
Image(uiImage: photoItem.image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 100, height: 100)
|
||||
@@ -31,7 +27,7 @@ struct PhotoPreview: View {
|
||||
.overlay(alignment: .topLeading) {
|
||||
Button {
|
||||
withAnimation {
|
||||
tool.selectedImageURL = nil
|
||||
tool.selectedPhotoItem = nil
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
|
||||
@@ -19,8 +19,8 @@ struct Toolbar: View {
|
||||
|
||||
@State var title: String
|
||||
@State var memo: MemoObject
|
||||
@State var photoItem: PhotosPickerItem?
|
||||
@State var opensCamera: Bool = false
|
||||
@State var photosPickerItem: PhotosPickerItem?
|
||||
@State var isCameraAccessDenied: Bool = false
|
||||
|
||||
@FocusState var textFieldState: Bool
|
||||
@@ -56,22 +56,28 @@ struct Toolbar: View {
|
||||
}
|
||||
.font(.subheadline)
|
||||
.padding(10)
|
||||
.onChange(of: photoItem) { oldValue, newValue in
|
||||
.onChange(of: photosPickerItem) { oldValue, newValue in
|
||||
if newValue != nil {
|
||||
Task {
|
||||
let data = try? await newValue?.loadTransferable(type: Data.self)
|
||||
if let data {
|
||||
let url = canvas.savePhoto(data)
|
||||
if let data, let image = UIImage(data: data) {
|
||||
let photoItem = canvas.bookmarkPhoto(of: image)
|
||||
withAnimation {
|
||||
tool.selectedImageURL = url
|
||||
tool.selectedPhotoItem = photoItem
|
||||
}
|
||||
}
|
||||
photoItem = nil
|
||||
photosPickerItem = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $opensCamera) {
|
||||
CameraView(url: $tool.selectedImageURL, canvas: canvas)
|
||||
let image: Binding<UIImage?> = Binding {
|
||||
tool.selectedPhotoItem?.image
|
||||
} set: { image in
|
||||
guard let image else { return }
|
||||
tool.selectedPhotoItem = canvas.bookmarkPhoto(of: image)
|
||||
}
|
||||
CameraView(image: image, canvas: canvas)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
.alert("Camera Access Denied", isPresented: $isCameraAccessDenied) {
|
||||
@@ -167,7 +173,7 @@ struct Toolbar: View {
|
||||
.clipShape(.rect(cornerRadius: 8))
|
||||
}
|
||||
.hoverEffect(.lift)
|
||||
PhotosPicker(selection: $photoItem, matching: .images, preferredItemEncoding: .compatible) {
|
||||
PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) {
|
||||
Image(systemName: "photo.fill.on.rectangle.fill")
|
||||
.contentShape(.circle)
|
||||
.frame(width: size, height: size)
|
||||
|
||||
@@ -17,5 +17,6 @@ class PhotoObject: NSManagedObject {
|
||||
@NSManaged var bounds: [CGFloat]
|
||||
@NSManaged var createdAt: Date?
|
||||
@NSManaged var imageURL: URL?
|
||||
@NSManaged var bookmark: Data?
|
||||
@NSManaged var element: ElementObject?
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
<relationship name="tool" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ToolObject" inverseName="pens" inverseEntity="ToolObject"/>
|
||||
</entity>
|
||||
<entity name="PhotoObject" representedClassName="PhotoObject" syncable="YES">
|
||||
<attribute name="bookmark" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="bounds" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="height" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
|
||||
Reference in New Issue
Block a user