mirror of
https://github.com/dscyrescotti/Memola.git
synced 2026-04-25 10:08:34 +02:00
feat: refine photo dock design for compact layout
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user