feat: refine photo dock design for compact layout

This commit is contained in:
dscyrescotti
2024-07-21 15:47:13 +07:00
parent a8ea08f63d
commit 798beaab85
7 changed files with 106 additions and 42 deletions

View File

@@ -320,7 +320,7 @@ extension GraphicContext {
tree.insert(photo.element, in: photo.photoBox) tree.insert(photo.element, in: photo.photoBox)
let photoFileID = photoFile.objectID let photoFileID = photoFile.objectID
withPersistence(\.backgroundContext) { [weak _photo = photo, weak graphicContext = object] context in withPersistence(\.backgroundContext) { [weak _photo = photo, weak graphicContext = object] context in
guard let _photo, let photoFile = context.object(with: photoFileID) as? PhotoFileObject else { guard let _photo, let photoFile = try context.existingObject(with: photoFileID) as? PhotoFileObject else {
return return
} }
let photo = PhotoObject(\.backgroundContext) let photo = PhotoObject(\.backgroundContext)

View File

@@ -128,13 +128,13 @@ final class Tool: NSObject, ObservableObject {
} }
} }
func createFile(_ image: Platform.Image, with canvas: CanvasObject) { func createFile(_ image: Platform.Image, with canvas: CanvasObject?) {
guard let (resizedImage, dimension) = resizePhoto(of: image) else { return } guard let (resizedImage, dimension) = resizePhoto(of: image) else { return }
guard let photoItem = bookmarkPhoto(of: resizedImage, and: image, in: dimension, with: canvas.objectID) else { return } guard let objectID = canvas?.objectID, let photoItem = bookmarkPhoto(of: resizedImage, and: image, in: dimension, with: objectID) else { return }
let _dimension = photoItem.dimension let _dimension = photoItem.dimension
let graphicContext = canvas.graphicContext let graphicContext = canvas?.graphicContext
withPersistence(\.viewContext) { [weak graphicContext = graphicContext] context in withPersistenceSync(\.backgroundContext) { [weak graphicContext = graphicContext] context in
let file = PhotoFileObject(\.viewContext) let file = PhotoFileObject(\.backgroundContext)
file.imageURL = photoItem.id file.imageURL = photoItem.id
file.bookmark = photoItem.bookmark file.bookmark = photoItem.bookmark
file.dimension = [_dimension.width, _dimension.height] file.dimension = [_dimension.width, _dimension.height]
@@ -142,7 +142,6 @@ final class Tool: NSObject, ObservableObject {
file.photos = [] file.photos = []
file.graphicContext = graphicContext file.graphicContext = graphicContext
graphicContext?.files.add(file) graphicContext?.files.add(file)
try context.saveIfNeeded()
} }
} }

View File

@@ -33,9 +33,6 @@ struct ElementToolbar: View {
ZStack(alignment: .bottom) { ZStack(alignment: .bottom) {
if tool.selection == .photo { if tool.selection == .photo {
PhotoDock(tool: tool, canvas: canvas) PhotoDock(tool: tool, canvas: canvas)
.padding(.bottom, 10)
.frame(maxWidth: .infinity)
.transition(.move(edge: .bottom).combined(with: .blurReplace))
} else { } else {
compactToolbar compactToolbar
} }
@@ -164,7 +161,7 @@ struct ElementToolbar: View {
.fill(.regularMaterial) .fill(.regularMaterial)
} }
.padding(10) .padding(10)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
.transition(.move(edge: .bottom).combined(with: .blurReplace)) .transition(.move(edge: .bottom).combined(with: .blurReplace))
} }
} }

View File

@@ -75,7 +75,7 @@ struct MemoView: View {
case .pen: case .pen:
PenDock(tool: tool, canvas: canvas) PenDock(tool: tool, canvas: canvas)
case .photo: case .photo:
PhotoDock(memo: memo, tool: tool, canvas: canvas) PhotoDock(tool: tool, canvas: canvas)
default: default:
EmptyView() EmptyView()
} }

View File

@@ -13,7 +13,6 @@ struct PhotoDock: View {
@FetchRequest private var fileObjects: FetchedResults<PhotoFileObject> @FetchRequest private var fileObjects: FetchedResults<PhotoFileObject>
private let memo: MemoObject
private let size: CGFloat = 40 private let size: CGFloat = 40
@ObservedObject private var tool: Tool @ObservedObject private var tool: Tool
@@ -23,12 +22,14 @@ struct PhotoDock: View {
@State private var isCameraAccessDenied: Bool = false @State private var isCameraAccessDenied: Bool = false
@State private var photosPickerItems: [PhotosPickerItem] = [] @State private var photosPickerItems: [PhotosPickerItem] = []
init(memo: MemoObject, tool: Tool, canvas: Canvas) { init(tool: Tool, canvas: Canvas) {
self.memo = memo
self.tool = tool self.tool = tool
self.canvas = canvas self.canvas = canvas
let predicate: NSPredicate = NSPredicate(format: "graphicContext = %@", memo.canvas.graphicContext) var predicate: NSPredicate?
if let canvasObject = canvas.object {
predicate = NSPredicate(format: "graphicContext = %@", canvasObject.graphicContext)
}
let descriptors: [SortDescriptor<PhotoFileObject>] = [SortDescriptor(\.createdAt)] let descriptors: [SortDescriptor<PhotoFileObject>] = [SortDescriptor(\.createdAt)]
self._fileObjects = FetchRequest(sortDescriptors: descriptors, predicate: predicate) self._fileObjects = FetchRequest(sortDescriptors: descriptors, predicate: predicate)
} }
@@ -36,22 +37,12 @@ struct PhotoDock: View {
var body: some View { var body: some View {
Group { Group {
#if os(macOS) #if os(macOS)
GeometryReader { proxy in photoDock
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 #else
if horizontalSizeClass == .regular { if horizontalSizeClass == .regular {
photoOption photoDock
} else { } else {
compactPhotoOption compactPhotoDock
} }
#endif #endif
} }
@@ -59,10 +50,13 @@ struct PhotoDock: View {
#if os(iOS) #if os(iOS)
.fullScreenCover(isPresented: $opensCamera) { .fullScreenCover(isPresented: $opensCamera) {
let image: Binding<UIImage?> = Binding { let image: Binding<UIImage?> = Binding {
tool.selectedPhotoFile?.image .none
} set: { image in } set: { image in
guard let image else { return } 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) CameraView(image: image, canvas: canvas)
.ignoresSafeArea() .ignoresSafeArea()
@@ -87,6 +81,7 @@ struct PhotoDock: View {
for photoItem in newValue { for photoItem in newValue {
await createFile(for: photoItem) await createFile(for: photoItem)
} }
saveFile()
photosPickerItems = [] photosPickerItems = []
tool.isLoadingPhoto = false tool.isLoadingPhoto = false
} }
@@ -94,6 +89,38 @@ struct PhotoDock: View {
} }
} }
private var photoDock: some View {
GeometryReader { proxy in
VStack(alignment: .trailing, spacing: 5) {
photoOption
photoItemGrid
.frame(minHeight: proxy.size.height * 0.2, maxHeight: proxy.size.height * 0.4)
}
.fixedSize()
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
}
.padding(10)
.transition(.move(edge: .trailing).combined(with: .blurReplace))
}
private var compactPhotoDock: some View {
GeometryReader { proxy in
HStack(spacing: 0) {
compactPhotoItemList
compactPhotoOption
}
.fixedSize(horizontal: false, vertical: true)
.background {
RoundedRectangle(cornerRadius: 8)
.fill(.regularMaterial)
}
.frame(maxWidth: min(proxy.size.height, proxy.size.width), maxHeight: .infinity, alignment: .bottom)
.frame(maxWidth: .infinity)
}
.padding(10)
.transition(.move(edge: .bottom).combined(with: .blurReplace))
}
private var photoOption: some View { private var photoOption: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
#if os(iOS) #if os(iOS)
@@ -196,13 +223,6 @@ struct PhotoDock: View {
#endif #endif
} }
} }
.background {
RoundedRectangle(cornerRadius: 8)
.fill(.regularMaterial)
}
.padding(.bottom, 10)
.frame(maxWidth: .infinity)
.transition(.move(edge: .bottom).combined(with: .blurReplace))
} }
@ViewBuilder @ViewBuilder
@@ -210,7 +230,7 @@ struct PhotoDock: View {
let padding: CGFloat = 5 let padding: CGFloat = 5
let size = (self.size * 2 - (5 + padding * 2)) / 2 let size = (self.size * 2 - (5 + padding * 2)) / 2
let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: 5), count: 2) let columns: [GridItem] = .init(repeating: GridItem(.flexible(), spacing: 5), count: 2)
ScrollView { ScrollView(showsIndicators: false) {
LazyVGrid(columns: columns, spacing: 5) { LazyVGrid(columns: columns, spacing: 5) {
ForEach(fileObjects) { file in ForEach(fileObjects) { file in
Group { Group {
@@ -248,6 +268,44 @@ struct PhotoDock: View {
} }
} }
@ViewBuilder
private var compactPhotoItemList: some View {
let padding: CGFloat = 5
let size = self.size - padding * 2
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 5) {
ForEach(fileObjects) { file in
Group {
let previewSize = file.previewSize(size)
if let previewImage = file.previewImage {
Image(image: previewImage)
.resizable()
.frame(width: previewSize.width, height: previewSize.height)
.onTapGesture {
if tool.selectedPhotoFile == file {
tool.unselectPhoto()
} else {
tool.selectPhoto(file)
}
}
} else {
Color.gray.opacity(0.5)
}
}
.frame(width: size, height: size)
.clipShape(RoundedRectangle(cornerRadius: 5))
.overlay {
if tool.selectedPhotoFile == file {
RoundedRectangle(cornerRadius: 5)
.stroke(Color.accentColor, lineWidth: 2.5)
}
}
}
}
.padding(padding)
}
}
private func openCamera() { private func openCamera() {
let status = AVCaptureDevice.authorizationStatus(for: .video) let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status { switch status {
@@ -271,7 +329,16 @@ struct PhotoDock: View {
private func createFile(for photoItem: PhotosPickerItem) async { private func createFile(for photoItem: PhotosPickerItem) async {
let data = try? await photoItem.loadTransferable(type: Data.self) let data = try? await photoItem.loadTransferable(type: Data.self)
if let data, let image = Platform.Image(data: data) { if let data, let image = Platform.Image(data: data) {
tool.createFile(image, with: memo.canvas) tool.createFile(image, with: canvas.object)
}
}
private func saveFile() {
withPersistenceSync(\.backgroundContext) { context in
try context.saveIfNeeded()
withPersistenceSync(\.viewContext) { context in
try context.saveIfNeeded()
}
} }
} }
} }

View File

@@ -20,7 +20,8 @@ final class Persistence {
}() }()
lazy var backgroundContext: NSManagedObjectContext = { lazy var backgroundContext: NSManagedObjectContext = {
let context = persistentContainer.newBackgroundContext() let context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = viewContext
context.undoManager = nil context.undoManager = nil
context.automaticallyMergesChangesFromParent = true context.automaticallyMergesChangesFromParent = true
return context return context

View File

@@ -19,7 +19,7 @@ final class PhotoFileObject: NSManagedObject, Identifiable {
@NSManaged var graphicContext: GraphicContextObject? @NSManaged var graphicContext: GraphicContextObject?
var previewImage: Platform.Image? { var previewImage: Platform.Image? {
guard let imageURL else { return nil } guard let imageURL = bookmark?.getBookmarkURL() else { return nil }
guard let data = try? Data(contentsOf: imageURL, options: []) else { return nil } guard let data = try? Data(contentsOf: imageURL, options: []) else { return nil }
return Platform.Image(data: data) return Platform.Image(data: data)
} }